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