Integrations

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:

typescript
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.

typescript
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.

typescript
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}), 200

Laravel (PHP)

For Laravel applications, you can handle the webhook inside a controller. We use PHP's native hash_equals function for secure string comparison.

typescript
<?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.

typescript
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.