Documentation
Webhooks
Blobify sends signed HTTP webhooks after content lifecycle changes and routing publication. Content lifecycle webhooks are emitted after the queue worker has converged summaries and indexes, so receivers can fetch the new derived output.
Events
| Event | Meaning |
|---|---|
content.saved | A draft was saved |
content.published | One or more locales went live |
content.unpublished | One or more locales were removed from the published snapshot |
content.archived | A live entry moved to the recoverable archive |
content.restored | An archived entry returned to live storage |
content.deleted | An entry was permanently deleted |
routing.published | Published routing changed |
Treat content.archived like a delete for public caches. Restore reintroduces the entry, including its published snapshot when one existed before archiving.
Payload
Every delivery includes resolved urls and stable revalidationTags along with the changed model, content ID, locale information, and configured summary fields.
{
"event": "content.published",
"timestamp": "2026-07-13T14:30:00.000Z",
"org": "org_abc",
"space": "main",
"model": "article",
"contentId": "art_123",
"locales": ["en", "is"],
"urls": ["/en/blog/my-post", "/is/blogg/greinin-min"],
"summaryFields": {
"slug": { "en": "my-post", "is": "greinin-min" },
"category": "news"
},
"revalidationTags": [
"blobify:org:org_abc",
"blobify:space:main",
"blobify:model:main:article",
"blobify:content:main:article:art_123",
"blobify:locale:main:article:en",
"blobify:locale:main:article:is"
],
"publishedLocales": ["en", "is"]
}urls can be empty when the model has no published route or a route cannot be resolved. publishedLocales is the complete set of locales that remain live, while locales is the set affected by this event. An optional actor identifies who triggered the change. An optional test field is set to true only on test deliveries sent from the webhook settings test action, and is never present on real events, so you can allowlist test pings.
Blobify does not keep a reverse-reference graph that enumerates every page containing a changed entry. Tag dependent cache reads with the referenced content or model tag, then invalidate those tags when its webhook arrives.
For routing.published, Blobify emits the organization tag and blobify:routing:{orgId}.
Signature headers
Each request includes:
X-Blobify-Signature: t=1783953000,v1=HMAC_HEX
X-Blobify-Event: content.published
X-Blobify-Delivery: DELIVERY_UUIDThe signature is HMAC-SHA256 over {timestamp}.{rawBody} using the webhook secret. Verify the raw request body before parsing JSON. Also reject old timestamps to limit replay.
The delivery ID is unique per attempt. If Blobify retries once, the retry has a new delivery ID.
Next.js handler
import { createHmac, timingSafeEqual } from 'node:crypto';
import { revalidatePath, revalidateTag } from 'next/cache';
type BlobifyWebhookPayload = {
event: string;
urls: string[];
revalidationTags: string[];
};
function verifySignature(
rawBody: string,
header: string,
secret: string,
nowSeconds = Math.floor(Date.now() / 1000),
): boolean {
const parts = Object.fromEntries(
header.split(',').map((part) => part.split('=', 2)),
);
const timestamp = Number(parts.t);
const supplied = parts.v1;
if (!Number.isFinite(timestamp) || !supplied) return false;
if (Math.abs(nowSeconds - timestamp) > 300) return false;
const expected = createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`)
.digest('hex');
const suppliedBytes = Buffer.from(supplied, 'hex');
const expectedBytes = Buffer.from(expected, 'hex');
if (suppliedBytes.length !== expectedBytes.length) return false;
return timingSafeEqual(suppliedBytes, expectedBytes);
}
export async function POST(request: Request) {
const rawBody = await request.text();
const signature = request.headers.get('x-blobify-signature');
const secret = process.env.BLOBIFY_WEBHOOK_SECRET;
if (!signature || !secret || !verifySignature(rawBody, signature, secret)) {
return Response.json({ error: 'Invalid signature' }, { status: 401 });
}
const payload = JSON.parse(rawBody) as BlobifyWebhookPayload;
for (const path of payload.urls) {
revalidatePath(path);
}
for (const tag of payload.revalidationTags) {
revalidateTag(tag, 'max');
}
return Response.json({ ok: true });
}Keep the webhook secret in server-side environment configuration. The secret is returned once when the webhook is created and is masked in later list responses.
Tag cached reads
const response = await fetch(articleUrl, {
next: {
tags: [
'blobify:model:main:article',
`blobify:content:main:article:${articleId}`,
'blobify:locale:main:article:en',
],
revalidate: 300,
},
});Use path revalidation for the direct URLs Blobify can resolve. Use tag revalidation for model lists, shared dependencies, and referenced content.
Delivery behavior
Each attempt has a 5-second timeout. If the first attempt fails, Blobify waits one second and retries once. Blobify stores the latest delivery success, status code, and timestamp for the badge in Settings > Webhooks.
There is no persistent retry queue and no manual replay queue. Your receiver should return a successful status only after it has accepted the event, and it should be safe to process duplicate events.
Use the synchronous test action in webhook settings, or POST /v1/orgs/{orgId}/webhooks/{id}/test, to verify reachability and signature handling. Test deliveries are production-shaped: they carry canonical revalidationTags and a test: true marker, so your receiver processes them the same way it processes real events. The test result includes the receiver's response body (truncated to 1 KB), so a strict receiver's rejection message is visible for debugging.
Register the route
A dedicated application route keeps the integration isolated:
https://www.example.com/api/blobify/webhookRegister the full public URL in Settings > Webhooks and subscribe only to the events the application handles.