Custom Webhook Integration
Welcome! If your favorite platform isn't listed in our native integrations, don't worry. You can seamlessly connect Blogree to any system that can receive HTTP POST requests by building a custom webhook.
In this guide, you will learn how the webhook payload is structured and how to securely verify and handle these requests in Node.js, Python, PHP, and Go.
Key Requirements: Your endpoint must accept HTTPS POST requests, verify the HMAC-SHA256 signature, return a 200 OK status within 30 seconds, and handle duplicate deliveries gracefully (idempotency).
Understanding the Webhook Payload
Whenever an event occurs (like a post being published), Blogree securely sends a JSON payload to your server. Here is an example of what that request looks like:
POST /your-webhook-endpoint
Content-Type: application/json
X-Blogree-Signature: sha256=abc123...
X-Blogree-Timestamp: 1680432000
{
"event": "post.published",
"post": {
"id": "post_xyz789",
"version": 3,
"slug": "my-blog-post",
"title": "My Blog Post Title",
"excerpt": "A short summary of the post...",
"body": {
"html": "<h1>Title</h1><p>Content...</p>",
"markdown": "# Title\n\nContent...",
"json": {}
},
"meta": {
"title": "SEO Title | My Site",
"description": "Meta description for search engines",
"og_image": "https://cdn.example.com/og.jpg"
},
"tags": ["AI", "blogging", "automation"],
"status": "published",
"published_at": "2026-04-02T09:00:00Z"
},
"site": {
"id": "site_abc123",
"name": "My Blog"
},
"delivered_at": "2026-04-02T09:00:03Z"
}Implementation Examples
To ensure the webhook is actually coming from Blogree and hasn't been tampered with, you must verify the X-Blogree-Signature header. Choose your preferred language below to see how to securely handle the payload.
Express.js (Node.js)
If you are using Node.js, Express is a great choice. Crucially, you must use express.raw() to capture the raw body before parsing it, as the signature is generated from the exact raw byte string.
const express = require('express');
const crypto = require('crypto');
const app = express();
// ⚠️ We need the raw body to properly verify the HMAC signature
app.use('/webhooks/blogree', express.raw({ type: 'application/json' }));
function verifySignature(rawBody, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
try {
// Use timingSafeEqual to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
} catch {
return false;
}
}
app.post('/webhooks/blogree', (req, res) => {
const signature = req.headers['x-blogree-signature'] || '';
if (!verifySignature(req.body, signature, process.env.BLOGREE_WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Once verified, we can safely parse the JSON
const { post } = JSON.parse(req.body.toString());
console.log('Successfully received post:', post.title);
// Example: db.posts.upsert({ where: { slug: post.slug }, ... })
res.status(200).json({ received: true });
});
app.listen(3000, () => console.log('Listening for webhooks...'));Flask (Python)
Python's built-in hmac and hashlib libraries make it incredibly simple to verify incoming requests in a Flask application.
from flask import Flask, request, jsonify, abort
import hmac, hashlib, os
app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get('BLOGREE_WEBHOOK_SECRET', '')
def verify(raw_body: bytes, signature: str) -> bool:
expected = 'sha256=' + hmac.new(
WEBHOOK_SECRET.encode(), raw_body, hashlib.sha256
).hexdigest()
# compare_digest prevents timing attacks
return hmac.compare_digest(expected, signature)
@app.route('/webhooks/blogree', methods=['POST'])
def blogree_webhook():
signature = request.headers.get('X-Blogree-Signature', '')
if not verify(request.get_data(), signature):
abort(401)
payload = request.get_json(force=True)
post = payload['post']
# Store post in your database...
# db.session.merge(Post(slug=post['slug'], title=post['title'], ...))
# db.session.commit()
return jsonify({'received': True}), 200Laravel (PHP)
For Laravel applications, you can handle the webhook inside a controller. We use PHP's native hash_equals function for secure string comparison.
<?php
// routes/api.php
Route::post('/webhooks/blogree', [BlogreeWebhookController::class, 'handle']);
// app/Http/Controllers/BlogreeWebhookController.php
namespace AppHttpControllers;
use IlluminateHttpRequest;
use AppModelsPost;
class BlogreeWebhookController extends Controller {
public function handle(Request $request) {
$signature = $request->header('X-Blogree-Signature', '');
$secret = config('services.blogree.webhook_secret');
// Generate the expected signature
$expected = 'sha256=' . hash_hmac('sha256', $request->getContent(), $secret);
if (!hash_equals($expected, $signature)) {
return response()->json(['error' => 'Unauthorized'], 401);
}
$post = $request->input('post');
// Safely create or update the post in your database
Post::updateOrCreate(
['slug' => $post['slug']],
[
'title' => $post['title'],
'content' => $post['body']['html'],
'excerpt' => $post['excerpt'] ?? '',
'meta_title' => $post['meta']['title'] ?? '',
'meta_description' => $post['meta']['description'] ?? '',
'published_at' => $post['published_at'],
]
);
return response()->json(['received' => true]);
}
}Go
Go's strong typing and standard library provide an excellent foundation for a high-performance webhook processor.
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
func verifySignature(body []byte, signature, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signature))
}
func blogreeWebhook(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Bad request", 400)
return
}
sig := r.Header.Get("X-Blogree-Signature")
if !verifySignature(body, sig, os.Getenv("BLOGREE_WEBHOOK_SECRET")) {
http.Error(w, "Unauthorized", 401)
return
}
var payload map[string]interface{}
json.Unmarshal(body, &payload)
post := payload["post"].(map[string]interface{})
fmt.Printf("Successfully processed post: %s\n", post["title"])
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"received":true}`))
}
func main() {
http.HandleFunc("/webhooks/blogree", blogreeWebhook)
fmt.Println("Server running on port 8080...")
http.ListenAndServe(":8080", nil)
}Requirements Checklist
Before deploying your webhook handler to production, make sure you've covered these best practices:
- ✅HTTPS endpoint — Blogree only delivers to secure HTTPS URLs.
- ✅Returns 200 OK within 30 seconds to prevent timeouts.
- ✅Verifies the X-Blogree-Signature before processing any data.
- ✅Handles idempotency — the same post_id can occasionally arrive multiple times (retries).
- ✅Uses constant-time comparison for signature verification to avoid timing attacks.
- ⚠️Do NOT return 200 before finishing processing — Blogree marks the webhook as successfully delivered immediately upon receiving a 200 OK.