Outbound Delivery
Better Blog sends publish and update events to your endpoint with signed payloads.
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.
Better Blog sends publish and update events to your endpoint with signed payloads.
Signature uses HMAC-SHA256 in X-BetterBlog-Signature.
Retry policy: up to 3 attempts with exponential backoff for retryable failures.
User connects Custom Webhook from Dashboard > Article > Integrations and runs Test & Connect.
Backend pings your endpoint with { event: "ping" } and requires a 2xx response to save integration.
On publish, Better Blog sends signed JSON payload with content, media, SEO, and metadata fields.
Your endpoint verifies HMAC signature from X-BetterBlog-Signature using the shared secret.
Your endpoint enforces idempotency with X-BetterBlog-Delivery-ID and saves/updates your post.
Your endpoint returns 2xx. Optional JSON response can include id/url to improve mapping.
Headers below apply to 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 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
}
}Before saving integration, Better Blog calls your endpoint with a ping. Return any 2xx response to pass setup verification.
{
"event": "ping",
"timestamp": "2026-02-18T14:10:00.000Z",
"message": "Verifying connection from Better Blog AI"
}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;
}
}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" });
});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
Verify you compute HMAC over raw body exactly as received and use the same secret shown during integration setup.
Make sure endpoint is public HTTPS, reachable from the internet, and responds within 10 seconds.
Store and check X-BetterBlog-Delivery-ID and source_blog_id before writing new records.
Upsert by source_blog_id first, then slug/external_id fallback. Do not rely on slug only.
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.
Use this second prompt to audit and production-harden an existing receiver after initial implementation.
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.