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.