Developer Documentation

Webhook Integration Guide

This page documents the exact webhook behavior used by Better Blog AI today: connection handshake, signed payload delivery, retries, and idempotent processing. Use this as your implementation reference for custom CMS or internal publishing pipelines.

Outbound Delivery

Better Blog sends publish and update events to your endpoint with signed payloads.

Security

Signature uses HMAC-SHA256 in X-BetterBlog-Signature.

Reliability

Retry policy: up to 3 attempts with exponential backoff for retryable failures.

End-to-End Flow

  1. 01

    User connects Custom Webhook from Dashboard > Article > Integrations and runs Test & Connect.

  2. 02

    Backend pings your endpoint with { event: "ping" } and requires a 2xx response to save integration.

  3. 03

    On publish, Better Blog sends signed JSON payload with content, media, SEO, and metadata fields.

  4. 04

    Your endpoint verifies HMAC signature from X-BetterBlog-Signature using the shared secret.

  5. 05

    Your endpoint enforces idempotency with X-BetterBlog-Delivery-ID and saves/updates your post.

  6. 06

    Your endpoint returns 2xx. Optional JSON response can include id/url to improve mapping.

Request Headers

Headers below apply to publish/update deliveries.

Authorization
Bearer <webhook_secret>
X-BetterBlog-Signature
sha256=<hex-hmac-of-raw-body>
X-BetterBlog-Delivery-ID
Unique UUID per delivery. Use as idempotency key.
X-BetterBlog-Event
publish or update.
X-Project-ID
Project id of the source workspace.
User-Agent
BetterBlogAI-Webhook/2.0 on publish/update deliveries.

During Test & Connect, Better Blog sends a ping payload and expects a successful 2xx response. Ping does not include signature headers.

Payload Contract

Payload includes content HTML, media, SEO, computed read-time/word-count, and references like source_blog_id.

{
  "event": "publish",
  "version": "2.0",
  "timestamp": "2026-02-18T14:10:00.000Z",
  "data": {
    "title": "How to Automate SaaS SEO in 2026",
    "slug": "how-to-automate-saas-seo-2026",
    "excerpt": "A comprehensive guide on leveraging AI and webhooks...",
    "content": "<h1>How to Automate...</h1><p>The full HTML string, prepared for direct injection...</p>",
    "featured_image": {
      "url": "https://storage.googleapis.com/.../featured-image.png",
      "alt": "How to Automate SaaS SEO in 2026"
    },
    "inline_images": [
      {
        "url": "https://storage.googleapis.com/.../chart-1.png",
        "alt": "SEO metrics growth chart"
      }
    ],
    "seo": {
      "meta_title": "Automate SaaS SEO: The 2026 AI Guide",
      "meta_description": "Learn how to use webhooks and AI agents...",
      "focus_keyword": "SaaS SEO automation",
      "canonical_url": null
    },
    "published_at": "2026-02-18T14:10:00.000Z",
    "scheduled_date": null,
    "reading_time_minutes": 6,
    "word_count": 1420,
    "external_id": null,
    "source_blog_id": "abc123xyz",
    "source_platform": "webhook",
    "featuredImage": "https://storage.googleapis.com/.../featured-image.png",
    "metaDescription": "Learn how to use webhooks and AI agents...",
    "externalId": null,
    "tags": ["SaaS SEO automation"],
    "author": null
  }
}

Handshake Contract (Test & Connect)

Before saving integration, Better Blog calls your endpoint with a ping. Return any 2xx response to pass setup verification.

Authorization
Bearer <webhook_secret>
Content-Type
application/json
User-Agent
BetterBlogAI-Webhook/1.0 during Test & Connect.
{
  "event": "ping",
  "timestamp": "2026-02-18T14:10:00.000Z",
  "message": "Verifying connection from Better Blog AI"
}

Signature Verification Example (Node.js)

import crypto from "crypto";

function verifySignature(rawBody, headerSignature, secret) {
  if (!headerSignature || !secret) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");

  const provided = headerSignature.startsWith("sha256=")
    ? headerSignature.slice(7)
    : headerSignature;

  try {
    return crypto.timingSafeEqual(
      Buffer.from(expected, "hex"),
      Buffer.from(provided, "hex")
    );
  } catch {
    return false;
  }
}

Minimal Receiver Example (Express)

app.post("/api/webhooks/betterblog", express.text({ type: "*/*" }), async (req, res) => {
  const rawBody = req.body;
  const deliveryId = req.get("X-BetterBlog-Delivery-ID");
  const signature = req.get("X-BetterBlog-Signature");
  const secret = process.env.BETTERBLOG_WEBHOOK_SECRET;

  // 1) Parse JSON
  let payload;
  try {
    payload = JSON.parse(rawBody);
  } catch {
    return res.status(400).json({ error: "Invalid JSON" });
  }

  // 2) Optional ping support (used by Test & Connect)
  if (payload.event === "ping") {
    return res.status(200).json({ status: "ok", message: "Pong!" });
  }

  // 3) Verify signature
  if (!verifySignature(rawBody, signature, secret)) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  // 4) Idempotency guard
  const alreadyProcessed = await hasDeliveryId(deliveryId);
  if (alreadyProcessed) {
    return res.status(200).json({ message: "Already processed (idempotent)" });
  }

  const { event, data } = payload;
  if (!event || !data) return res.status(400).json({ error: "Missing event or data" });

  // 5) Upsert / delete content
  if (event === "publish" || event === "update") {
    const savedPost = await upsertPost({
      slug: data.slug,
      sourceBlogId: data.source_blog_id,
      title: data.title,
      html: data.content,
      seo: data.seo,
      featuredImage: data.featured_image,
      inlineImages: data.inline_images
    });

    await markDeliveryId(deliveryId);

    return res.status(200).json({
      id: savedPost.id,              // optional but recommended
      url: savedPost.publicUrl       // optional but recommended
    });
  }

  if (event === "delete") {
    await deletePostBySourceIdOrSlug(data.source_blog_id, data.slug || data.external_id);
    await markDeliveryId(deliveryId);
    return res.status(200).json({ message: "Deleted" });
  }

  return res.status(400).json({ error: "Unknown event" });
});

Recommended Success Response

Better Blog considers any 2xx response a success. For best mapping, return id and url.

{
  "id": "cms-post-id-or-slug",
  "url": "https://yourdomain.com/blog/cms-post-id-or-slug"
}

Better Blog reads response JSON fields in this order: id | postId | articleId | slug | externalId and URL from: url | link | permalink

Edge Cases to Handle

  • Signature verification must use the raw request body bytes before any JSON re-serialization.
  • Do not require HMAC signature for ping; setup ping request includes Authorization but no X-BetterBlog-Signature.
  • Better Blog retries only on network/5xx style failures. 4xx errors are treated as final.
  • Connection setup uses ping and a 10s timeout. Publishing uses up to 30s per attempt.
  • Use X-BetterBlog-Delivery-ID for idempotency to avoid duplicate inserts during retries.
  • publish/update payloads include source_blog_id; use it as a stable upsert key.
  • If response body is not JSON, publishing still succeeds if status is 2xx.

Troubleshooting Matrix

401 invalid signature

Verify you compute HMAC over raw body exactly as received and use the same secret shown during integration setup.

Connection timeout during Test & Connect

Make sure endpoint is public HTTPS, reachable from the internet, and responds within 10 seconds.

Unexpected duplicate posts

Store and check X-BetterBlog-Delivery-ID and source_blog_id before writing new records.

Update arrives but creates new post

Upsert by source_blog_id first, then slug/external_id fallback. Do not rely on slug only.

Internal Better Blog Receiver (Optional)

If your project uses Better Blog's own receiver endpoint /api/webhooks/cms, the system verifies signature, enforces idempotency with delivery_id, and upserts posts into projects/<projectId>/published_posts.

That internal receiver also supports delete events. Outbound adapter currently sends publish and update.

AI-Ready Hardening Prompt

Use this second prompt to audit and production-harden an existing receiver after initial implementation.

Optimized for Next.js
You are a senior backend + platform reliability engineer. Audit and harden an existing Better Blog AI webhook receiver for production.

Target stack:
- Use App Router route handlers.
- Implement in app/api/webhooks/betterblog/route.ts.
- Read raw request body via await request.text().
- Verify HMAC using Node crypto against raw body.

Webhook contract you must implement exactly:
- Test & Connect handshake:
  - Request body:
    {
      "event": "ping",
      "timestamp": "<iso>",
      "message": "Verifying connection from Better Blog AI"
    }
  - Headers include Authorization: Bearer <secret> and Content-Type.
  - Ping DOES NOT include X-BetterBlog-Signature.
  - Must return 2xx quickly (<= 10s target).

- Publish/Update delivery:
  - Headers:
    - Authorization: Bearer <secret>
    - X-BetterBlog-Signature: sha256=<hmac-hex>
    - X-BetterBlog-Delivery-ID: <uuid>
    - X-BetterBlog-Event: publish | update
    - X-Project-ID: <project-id>
  - Verify signature using HMAC SHA-256 over RAW request body bytes.
  - Do not re-stringify JSON before verification.
  - Enforce idempotency via X-BetterBlog-Delivery-ID.
  - Upsert strategy: prefer source_blog_id, then slug/external_id fallback.
  - Accept event values publish and update (delete optional if your system supports it).
  - Return 2xx on success.
  - Recommended success payload:
    {
      "id": "<cms-id-or-slug>",
      "url": "https://yourdomain.com/blog/<slug>"
    }

- Failure behavior to support:
  - 4xx means caller will treat as final (no retry).
  - 5xx/network timeout may be retried by caller.
  - Keep logs structured for signature failure, validation failure, and idempotency skip.

Audit goals:
1) Verify existing implementation against the exact webhook contract.
2) Patch mismatches without breaking current behavior.
3) Add safe defaults, clear error handling, and structured logs.
4) Add or improve automated tests for:
   - ping handshake pass/fail
   - signature valid/invalid
   - idempotency duplicate delivery
   - publish upsert by source_blog_id
   - update fallback by slug/external_id
5) Add short operations notes for environment variables and rotation process.

Required output:
- Brief findings list (severity ordered).
- Exact file-by-file patch plan.
- Final code patches.
- A runnable verification checklist.
- Example curl commands for ping + signed publish request.

Constraints:
- No breaking API changes.
- Keep response format backward compatible.
- Do not touch unrelated app features.
- Keep secrets out of logs.

Paste this into your agentic IDE and ask it to implement exactly as specified.