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"
}
Example story.published payload.
POST/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)
}
Verify the signature in Node / Bun.