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

EventMeaning
content.savedA draft was saved
content.publishedOne or more locales went live
content.unpublishedOne or more locales were removed from the published snapshot
content.archivedA live entry moved to the recoverable archive
content.restoredAn archived entry returned to live storage
content.deletedAn entry was permanently deleted
routing.publishedPublished 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.

jsoncode
{
  "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.

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:

httpcode
X-Blobify-Signature: t=1783953000,v1=HMAC_HEX
X-Blobify-Event: content.published
X-Blobify-Delivery: DELIVERY_UUID

The 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

tscode
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

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

Register the route

A dedicated application route keeps the integration isolated:

textcode
https://www.example.com/api/blobify/webhook

Register the full public URL in Settings > Webhooks and subscribe only to the events the application handles.