Webhooks
Every time you publish, update or unpublish a post, we tell your site, so it can refresh straight away instead of waiting for its cache to expire.
Set these up on your dashboard's "Connect your website" page. Use one or both:
- Webhook: a signed
POSTto your endpoint. Best for sites with a server. - Deploy hook: an empty
POSTto your host's rebuild URL (Netlify, Vercel, Cloudflare Pages and others). Best for static sites.
Events
| Event | When |
|---|---|
post.published |
A post goes live |
post.updated |
A live post changes, possibly including its slug |
post.unpublished |
A post is taken down |
test |
You pressed Send test in the dashboard |
Payload
{
"event": "post.updated",
"occurredAt": "2026-10-12T09:30:00.000Z",
"siteKey": "967ac21239c54a8e9f2d094657c0ae28",
"post": { "slug": "what-is-llms-txt", "title": "…" },
"previousSlug": "llms-txt-explained"
}
post: the full post, the same asGET /posts/{slug}returns, forpost.publishedandpost.updated. Forpost.unpublishedit's just{ slug }.previousSlug: appears only when the slug changed. Clear the old URL too.
Verify the signature
Every webhook carries an x-blogsforseo-signature header in the form t=<unix seconds>,v1=<hex>. Before trusting a request:
- Compute the HMAC-SHA256 of
t + "." + rawBody, using your signing secret as the key. Use the raw body, before any JSON parsing. - Compare the result with
v1in constant time. - Reject with 401 if they don't match, or if
tis more than 300 seconds from now. The time check blocks replayed requests.
Keep the signing secret in an environment variable, e.g. BLOGSFORSEO_WEBHOOK_SECRET. Rotate it from the dashboard if it ever leaks.
Node.js
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody, header) {
const parts = Object.fromEntries((header ?? "").split(",").map((part) => part.split("=")));
const expected = createHmac("sha256", process.env.BLOGSFORSEO_WEBHOOK_SECRET).update(`${parts.t}.${rawBody}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) <= 300;
return fresh && typeof parts.v1 === "string" && parts.v1.length === expected.length && timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}
// In your POST handler:
// const body = await request.text();
// if (!verify(body, request.headers.get("x-blogsforseo-signature"))) return new Response("Invalid signature", { status: 401 });
// const { event, post, previousSlug } = JSON.parse(body);
// Clear the cache for the blog index, post.slug and previousSlug, then return 200.
PHP
$body = file_get_contents("php://input");
parse_str(str_replace(",", "&", $_SERVER["HTTP_X_BLOGSFORSEO_SIGNATURE"] ?? ""), $sig);
$expected = hash_hmac("sha256", ($sig["t"] ?? "") . "." . $body, getenv("BLOGSFORSEO_WEBHOOK_SECRET"));
if (!isset($sig["v1"]) || !hash_equals($expected, $sig["v1"]) || abs(time() - (int) $sig["t"]) > 300) {
http_response_code(401);
exit;
}
$event = json_decode($body, true);
// Clear the cache for the blog index, $event["post"]["slug"] and $event["previousSlug"].
Python
import hashlib, hmac, os, time
def verify(raw_body: bytes, header: str) -> bool:
parts = dict(part.split("=", 1) for part in header.split(",") if "=" in part)
t, v1 = parts.get("t", ""), parts.get("v1", "")
expected = hmac.new(os.environ["BLOGSFORSEO_WEBHOOK_SECRET"].encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return t.isdigit() and abs(time.time() - int(t)) <= 300 and hmac.compare_digest(expected, v1)
Delivery
- Timing: we send within seconds of a publish, wait up to 10 seconds for a reply, and treat any 2xx as success.
- Retries: a failed delivery is retried up to 3 times.
- Log: every attempt is listed under Recent deliveries on your dashboard.
- Respond fast: return 200 straight away, then do any slow work afterwards.
- Our CDN: we clear our cached copy of your API before sending, so if your handler fetches the API right away, it gets the new version.
IndexNow
Once your site serves /{INDEXNOW_KEY}.txt (the body is just the key), we notify IndexNow on every publish. IndexNow passes the news to Bing, Yandex and others, and ChatGPT search uses Bing. Until the file is in place, we skip this step and log it.