Webhooks
Notify external apps when content changes. Every delivery is a signed POST so receivers can verify both origin and body integrity.
Configuration
| Field | Description |
|---|---|
endpoint | HTTP URL to deliver events to |
events | JSON array to listen for, e.g. ["page.created", "form_entry.created", "page.*"]. null, [], or ["*"] subscribes to everything. |
is_active | 0 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.
| Event | Fired when |
|---|---|
page.*, layout.*, component.* | CRUD on pages, layouts, components |
form.*, webhook.* | CRUD on forms and webhooks |
form_entry.created | Public form submission |
form_entry.deleted | Entry deleted via API |
setting.upserted / setting.deleted | Settings changed via API |
| custom | ctx.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.
| Claim | Description |
|---|---|
iss | App URL (APP_URL) |
scope | Always "webhook.verify" |
event / delivery_id | Event name; matches payload id |
sig | SHA-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");