Webhooks
Fire-and-wait-for-ack notifications when stories publish, unpublish or move — with retries, the SSRF guard and a signed payload.
What triggers a webhook
story.published — publish button hit, or a scheduled publish fired.
story.unpublished — story taken offline.
story.deleted — soft-deleted via the admin or API.
story.moved — slug changed (folder move, rename).
payload.json
{
"action": "story.published",
"space_id": 1,
"story_id": 42,
"full_slug": "blog/first-post",
"uuid": "c3c7e0f3-...",
"delivery_id": "whd_01HZ...",
"timestamp": "2026-04-23T14:11:02.500Z"
}/api/v1/spaces/{spaceId}/webhooksBearer (Mgmt)Register a new webhook endpoint.
Request
POST /api/v1/spaces/1/webhooks
Authorization: Bearer sbmgmt_...
{
"name": "Vercel rebuild",
"url": "https://api.vercel.com/v1/deploy-hooks/abc",
"events": ["story.published", "story.unpublished"],
"secret": "<opaque, used for signature>"
}Signing & retries
Every request carries X-Webhook-Signature — HMAC-SHA256 over the raw body using the webhook's secret. Verify it before trusting any field. Failed deliveries (non-2xx or timeout after 10 s) retry with exponential backoff up to 5 attempts; after that the delivery is dead-lettered and visible in the audit log.
verify.ts
import { createHmac, timingSafeEqual } from 'node:crypto'
export function verify(rawBody: string, signature: string, secret: string): boolean {
const expected = createHmac('sha256', secret).update(rawBody).digest('hex')
const a = Buffer.from(expected, 'hex')
const b = Buffer.from(signature, 'hex')
return a.length === b.length && timingSafeEqual(a, b)
}