Webhooks

Notify external apps when content changes. Every delivery is a signed POST so receivers can verify both origin and body integrity.

Configuration

FieldDescription
endpointHTTP URL to deliver events to
eventsJSON array to listen for, e.g. ["page.created", "form_entry.created", "page.*"]. null, [], or ["*"] subscribes to everything.
is_active0 or 1 — pause without deleting

Subscription matching supports exact names (page.created), namespace wildcards (page.*), and a catch-all (*).

Events

Events fire automatically on content changes through the REST API, MCP, or public form submissions. Custom events can be fired from handlers via ctx.trigger.

EventFired when
page.*, layout.*, component.*CRUD on pages, layouts, components
form.*, webhook.*CRUD on forms and webhooks
form_entry.createdPublic form submission
form_entry.deletedEntry deleted via API
setting.upserted / setting.deletedSettings changed via API
customctx.trigger("my.event", data)

Payload

{
  "id": "8f3a...-delivery-id",
  "event": "page.created",
  "created_at": "2026-08-05T12:00:00.000Z",
  "data": { "id": "...", "path": "/blog/hello", "title": "Hello" },
  "app": { "url": "https://cms.example.com", "name": "My CMS" }
}

Verifying deliveries

Every delivery carries an X-CMS-Signature header containing a short-lived RS256 JWT (5-minute TTL), scoped to webhook.verify only — it cannot authenticate against /api or /mcp.

ClaimDescription
issApp URL (APP_URL)
scopeAlways "webhook.verify"
event / delivery_idEvent name; matches payload id
sigSHA-256 hash of the raw request body (hex)
// Node.js
const jwks = createLocalJWKSet(
  await fetch("https://<your-cms>/.well-known/jwks.json").then(r => r.json())
);
const { payload } = await jwtVerify(req.headers["x-cms-signature"], jwks, {
  algorithms: ["RS256"],
});
const hash = crypto.createHash("sha256").update(rawBody).digest("hex");
if (hash !== payload.sig) throw new Error("body tampered");