Webhooks
Estøkad POSTs to a URL you own when content changes. The primary use is cache revalidation — publish an entry, and your statically-generated site updates in seconds instead of on the next rebuild or ISR window. The audit stream (audit.event.created) can also feed a SIEM.
Events
Subscribe a webhook to any of:
| Event | Fires when |
|---|---|
| entry.published | an entry (or locale variant) goes live |
| entry.unpublished | an entry is withdrawn |
| entry.updated | a published entry's draft is saved |
| entry.created | a new entry is created |
| entry.deleted | an entry is deleted |
| entry.scheduled / entry.schedule_cleared | a publish/unpublish is scheduled or cancelled |
| entry.promoted | published state is copied between environments |
| entry.review_requested / entry.approved / entry.rejected | approval workflow transitions |
| entry.draft_discarded | a working draft is discarded |
| asset.created | an asset is uploaded |
| audit.event.created | any audit event is chained (workspace-wide; for SIEM streaming) |
For a publish-driven revalidation flow, subscribe to entry.published, entry.unpublished, and usually entry.updated.
Create one
From the Studio (Settings → Webhooks) or the management API: give a target URL and the events to receive. Estøkad returns a signing secret — store it; you verify deliveries with it.
Delivery contract
Each delivery is a POST with a JSON body and these headers:
| Header | Value |
|---|---|
| Content-Type | application/json |
| X-Estokad-Event | the event name, e.g. entry.published |
| X-Estokad-Signature | sha256=<hex> — HMAC-SHA256 of the raw request body keyed by your webhook secret |
| X-Estokad-Delivery | unique delivery id |
| X-Estokad-Attempt | attempt counter (deliveries retry with backoff) |
There is no bearer token — authenticity is proven by the signature. The body envelope:
{
"event": "entry.published",
"createdAt": "2026-09-11T10:00:00.000Z",
"workspaceId": "…",
"spaceId": "…",
"data": { "…": "event-specific fields, e.g. entry id / type / slug" }
}
Constraints. The target must be a public HTTPS host — private, loopback, link-local, and cloud-metadata addresses are refused at delivery time (re-resolved to catch DNS rebinding). Redirects are not followed. Requests time out at 5s. Response bodies are never stored — only the status code is kept in delivery history.
Next.js revalidation
A route handler that verifies the signature over the raw body, then revalidates. Any 2xx marks the delivery successful.
// app/api/revalidate/route.ts
import { createHmac, timingSafeEqual } from 'node:crypto'
import { revalidateTag, revalidatePath } from 'next/cache'
const SECRET = process.env.ESTOKAD_WEBHOOK_SECRET!
export async function POST(req: Request) {
const raw = await req.text() // verify against the RAW body, not parsed JSON
const sent = req.headers.get('x-estokad-signature') ?? ''
const expected = 'sha256=' + createHmac('sha256', SECRET).update(raw).digest('hex')
const ok =
sent.length === expected.length &&
timingSafeEqual(Buffer.from(sent), Buffer.from(expected))
if (!ok) return new Response('bad signature', { status: 401 })
const { event, data } = JSON.parse(raw) as {
event: string
data?: { type?: string; slug?: string }
}
// Revalidate as coarsely or finely as you tag your fetches.
if (data?.type) revalidateTag(data.type)
if (data?.slug && data.type) revalidatePath(`/${data.type}/${data.slug}`)
revalidatePath('/') // home / indexes
return Response.json({ revalidated: true, event })
}
Pair this with tagged fetches so a single publish invalidates exactly the affected pages. Because the site is statically served, if Estøkad is unreachable the pages keep serving — only publishing pauses, which is the failure mode you want for a public site on an early SDK.