Integrations
Custom Webhook Integration
If your platform isn't listed in our native integrations, you can connect Blogree to any system that can receive HTTP POST requests. This guide shows you how to build a custom webhook handler in multiple languages.
💡
Your endpoint must: (1) accept HTTPS POST requests, (2) verify the HMAC-SHA256 signature, (3) return 200 OK within 30 seconds, (4) handle duplicate deliveries idempotently.
Webhook Payload Structure
Every Blogree webhook delivery sends this JSON payload:
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"
}
Express.js (Node.js)
const express = require('express');
const crypto = require('crypto');
const app = express();
// ⚠️ Must use raw body for HMAC verification
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 {
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' });
}
const { post } = JSON.parse(req.body.toString());
// Do something with the post
console.log('New post received:', post.title);
// db.posts.upsert({ where: { slug: post.slug }, ... })
res.status(200).json({ received: true });
});
app.listen(3000);
Flask (Python)
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()
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}), 200
Laravel (PHP)
<?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');
$expected = 'sha256=' . hash_hmac('sha256', $request->getContent(), $secret);
if (!hash_equals($expected, $signature)) {
return response()->json(['error' => 'Unauthorized'], 401);
}
$post = $request->input('post');
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
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
)
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("New post: %s\n", post["title"])
w.WriteHeader(200)
w.Write([]byte(`{"received":true}`))
}
func main() {
http.HandleFunc("/webhooks/blogree", blogreeWebhook)
http.ListenAndServe(":8080", nil)
}
Requirements Checklist
✅HTTPS endpoint — Blogree only delivers to secure HTTPS URLs
✅Returns 200 OK within 30 seconds
✅Verifies X-Blogree-Signature before processing
✅Handles idempotency — same post_id can arrive multiple times (retries)
✅Uses constant-time comparison for signature verification
⚠️Do NOT return 200 before finishing processing — Blogree marks as delivered on 200