Skip to content

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.

Add webhooks to the admin section of your CMS config:

import { defineConfig } from "@kidecms/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: [...],
});
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 })

If you don’t define payload, the webhook receives this JSON body:

{
"event": "publish",
"collection": "posts",
"doc": { "_id": "...", "title": "Hello World", "...": "..." },
"user": { "id": "...", "email": "[email protected]", "role": "editor" },
"timestamp": "2026-04-09T12:34:56.789Z"
}

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}` },
},
],
}),
}

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}`,
},
}

Webhooks don’t block the operation that triggered them. Dispatch enqueues a durable task in the cms_outbox queue (the same 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, usually 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. It never stores 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)

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