ProductsFor your stack
WebhooksKnow the moment anything changes.
Capa signs a POST to your endpoint for every publish, edit, upload and scheduled run. Failed deliveries retry for more than eight hours, and every attempt is kept, so you can see what happened and send it again.
https://example.com/hooks/capainstance.unpublishedWine list 2024200Attempt 2, same event idpublish.batch.completed31 entries, 0 failed200Attempt 1instance.unpublishedWine list 2024503Retry in 1 minmedia.uploadedhero-autumn.jpg200Attempt 1instance.publishedAutumn menu200Attempt 1Eighteen events. Pick the ones you need.
Content, models, media and publishing runs, each with a description and a sample payload in the admin's subscription checklist.
instance.createdinstance.updatedinstance.publishedinstance.unpublishedinstance.deleted
model.createdmodel.updatedmodel.publishedmodel.deleted
media.uploadedmedia.updatedmedia.deleted
publish.scheduledpublish.rescheduledpublish.cancelledpublish.failedpublish.batch.completed
webhook.test
Subscribe to exact types or to a wildcard like instance.*, which also matches types added later. A test event only ever goes to the endpoint you are testing.
Proof it came from Capa. In one function.
Signed with HMAC-SHA256
Every delivery signs its timestamp and exact body with your endpoint's secret. The SDK's check also refuses anything older than five minutes.
The same id on every retry
The event id is also the Idempotency-Key header, unchanged across retries and redeliveries. Store it and skip a repeat.
Identity and links, not your content
The body says what changed and where to read it. Turn on includeData for receivers that cannot call back.
Rotate without dropping a request
For 24 hours after a rotation every delivery is signed with both secrets, so you can update your receiver whenever suits you.
import { verifyWebhookSignature } from "@capacms/sdk";
export async function POST(request: Request) {
const body = await request.text(); // the raw body, as it arrived
const ok = await verifyWebhookSignature({
payload: body,
header: request.headers.get("capa-signature") ?? "",
secret: process.env.CAPA_WEBHOOK_SECRET!,
});
if (!ok) return new Response("bad signature", { status: 400 });
const event = JSON.parse(body);
if (await alreadyHandled(event.id)) return new Response("ok");
await handle(event);
return new Response("ok");
}Your server was down. Nothing was lost.
Anything but a 2xx is a failure, redirects included. Capa tries again on a ladder that stretches across a working day.
- NowAttempt 1
- +1 minAttempt 2
- +5 minAttempt 3
- +30 minAttempt 4
- +2 hAttempt 5
- +6 hAttempt 6
8 h 36 min from the first attempt to the last, so a receiver that comes back inside a working day catches up on its own.
25 failures in a row, the first of them a day old, pause the endpoint. A burst of failures during an outage never costs you the endpoint.
Built for the night something breaks.
- Transport
HTTPS only
A webhook body carries your titles and a signature. Plain HTTP to a public host is refused when you save the endpoint.
- Network
Private addresses refused, twice
The host is checked when you save it and resolved again at every delivery, so a DNS change can never point a webhook inside a network.
- Secrets
Shown once
The secret appears when you create or rotate an endpoint. Revealing it later needs its own permission, and every reveal is audited.
- Redeliver
Send it again
Redeliver one delivery, everything that failed, or everything missed during a pause, with the count shown before anything is sent.
- Log
Every attempt kept
Each delivery keeps the request and the response for 30 days. Quote the delivery id and the exact attempt is found.
- Access
People manage endpoints, keys do not
Adding an endpoint takes a signed-in developer or admin. An API key can never forward your content somewhere new.
Your stack, in step with your content.
Add an endpoint in the admin, send a test event, and watch it land.