Webhooks
Webhooks let you POST to external URLs when content events happen: publishing posts, updating documents, deleting items, and so on. Use them to trigger deploys, sync to search indexes, post Slack notifications, or call any other HTTP endpoint.
This page covers outbound notifications. For receiving webhooks from external services — and for durable background jobs in general — see Background Tasks.
Configuration
Section titled “Configuration”Add webhooks to the admin section of your CMS config:
import { defineConfig } from "@/cms/core";
export default defineConfig({ admin: { webhooks: [ { name: "Slack notify", url: "https://hooks.slack.com/services/xxx/yyy/zzz", events: ["publish"], collections: ["posts"], payload: (doc, context) => ({ text: `📝 ${doc.title} published by ${context.user?.email}`, }), }, ], }, collections: [...],});Options
Section titled “Options”| Option | Type | Description |
|---|---|---|
name |
string |
Shown in logs, and routes durable deliveries back to this config — must be unique and non-empty |
url |
string |
URL to send the request to |
events |
WebhookEvent[] |
One or more of "create", "update", "delete", "publish", "unpublish" |
collections |
string[] |
Restrict to specific collection slugs (omit to fire on all collections) |
method |
"POST" | "PUT" | "PATCH" |
HTTP method (default: POST) |
headers |
Record<string, string> |
Custom headers (e.g. Authorization) |
payload |
(doc, context) => any |
Transform the payload (default: { event, collection, doc, user, timestamp }) |
Default payload
Section titled “Default payload”If you don’t define payload, the webhook receives this JSON body:
{ "event": "publish", "collection": "posts", "doc": { "_id": "...", "title": "Hello World", "...": "..." }, "timestamp": "2026-04-09T12:34:56.789Z"}Custom payloads
Section titled “Custom payloads”Pass a payload function to shape the request body for the receiving service:
{ name: "Discord notify", url: "https://discord.com/api/webhooks/xxx/yyy", events: ["publish"], payload: (doc, context) => ({ embeds: [ { title: `New post: ${doc.title}`, description: doc.excerpt, color: 5814783, footer: { text: `by ${context.user?.email}` }, }, ], }),}Authentication
Section titled “Authentication”Use the headers option to add Bearer tokens or API keys:
{ name: "Internal CMS sync", url: "https://internal.example.com/api/webhook", events: ["create", "update", "delete"], headers: { Authorization: `Bearer ${process.env.WEBHOOK_TOKEN}`, },}Reliability
Section titled “Reliability”Webhooks don’t block the operation that triggered them. Dispatch enqueues a durable task (the same cms_outbox queue background tasks use — see Background Tasks) rather than delivering immediately; a cron drain then attempts delivery with the outbox’s own retry/backoff, up to 5 attempts by default (30s × 2^(n-1) backoff). Only the enqueue itself is fire-and-forget: if that write fails (rare — a DB error at the moment of dispatch), the event is lost and logged to the server console; once enqueued, delivery is durable and survives process restarts.
The outbox row stores the event context needed to rebuild the payload — the document snapshot, actor, event, and collection — but never the webhook’s URL, headers, or secrets. Delivery re-resolves those from the live config at drain time, so a header change takes effect immediately and no credential is ever persisted to the database. The document/actor data does persist in cms_outbox until the row is pruned (see Background Tasks).
| Failure mode | Behavior |
|---|---|
| Non-2xx response | Retry per the outbox schedule, then marked failed |
| Network error | Retry per the outbox schedule, then marked failed |
| Timeout (5s) | Retry per the outbox schedule, then marked failed |
| Webhook removed from config since enqueue | Delivery is skipped quietly (no retry) |
Hooks vs webhooks
Section titled “Hooks vs webhooks”Both can react to content events. Use webhooks when:
- You’re calling an external HTTP service
- The same logic applies to multiple collections
- You want built-in retries, timeouts, and error logging
- You want to enable/disable integrations by editing config
Use collection hooks when:
- The logic isn’t an HTTP call (writing files, querying other DBs, mutating documents)
- You need to transform data before save
- The behavior is specific to one collection and complex enough to warrant custom code