Know the moment content changes.
DUMA sends a signed POST to your URL when an entry is published, unpublished or deleted, or when media changes — so your site rebuilds or clears its cache without a redeploy.
4 eventsSigned with HMAC-SHA256Retried up to 8 times
Set up in three steps
Step 1: Add a webhook
In Settings → Developers → Webhooks, choose New webhook. Give it a name, your HTTPS URL and the events to send. You can limit it to some content types — a blog rebuild only needs blog posts.
Open Settings → DevelopersStep 2: Keep the signing secret
The secret starts
whsec_and is shown once. Store it as an environment variable on your server. “New signing secret” replaces it, and the old one stops working straight away.Step 3: Send a test
“Send a test” delivers a signed
pingevent now and shows what your endpoint answered. Reply with any 2xx.
Events
A webhook receives only the events you picked. Changes to drafts send nothing.
entry.publishedAn entry goes live — from the editor, or when its scheduled time comes.entry.unpublishedA live entry is taken down, by hand or on its schedule.entry.deletedA published entry, or a content type that held published entries, is deleted.asset.changedA media file is added, replaced, edited or deleted.pingOnly from “Send a test”. Never sent on its own.
Payload
Every request is a JSON object with the same four fields. It says what changed, not the new content — fetch that from the Content API.
{
"id": "0f6b2c9e-4d1a-4b7e-9a51-2f3c8d7e6a10",
"event": "entry.published",
"createdAt": "2026-10-30T09:41:00.000Z",
"data": {
"entry": {
"id": "b7d1e0a2-5c3f-4e8a-91d4-6a2f0c9e8b17",
"entryId": "4c2a9f1e-7b6d-4e3a-8f05-1d9c2b7a6e43",
"type": "blog-post",
"locale": "en",
"slug": "spring-menu",
"version": 4
},
"source": "editor"
}
}idstring- The event. The same on every retry and redelivery — use it to ignore duplicates.
eventstring- One of the events above.
createdAtstring- When the change happened, ISO 8601 in UTC — not when this request was sent.
data.entryobject- Entry events.
idis the document id the Content API returns;entryIdis this language’s copy.typeis the content type key, so you can rebuild only what changed. data.sourcestringeditororschedule, onentry.publishedandentry.unpublished.editorscheduledata.assetobjectasset.changedonly: the asset’sid, withdata.changesaying what happened.createdupdateddeleted
Headers
X-Duma-Signaturestringv1=and the hex HMAC-SHA256 of{timestamp}.{body}, keyed with your signing secret.X-Duma-Timestampstring- Unix seconds when this attempt was signed. Signed with the body, so it can’t be swapped.
X-Duma-Eventstring- The event name, so you can route before parsing the body.
X-Duma-Deliverystring- This webhook’s delivery of the event. The same across its retries; shown in the delivery history.
Content-Typestringapplication/json.User-AgentstringDUMA-CMS-Webhooks/1.
Verify signatures
Anyone can POST to your URL, so check every request before acting on it. Sign {timestamp}.{body} with your secret and compare it with X-Duma-Signature:
- Use the raw body, exactly as received. Parsing and re-serialising the JSON changes the bytes and the signature.
- Refuse timestamps more than five minutes from your clock, so an old request can’t be replayed.
- Compare in constant time.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyDumaWebhook(secret, timestamp, body, signature) {
if (!timestamp || !signature) return false;
// Older than five minutes: a replayed request.
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = Buffer.from(
'v1=' + createHmac('sha256', secret).update(`${timestamp}.${body}`).digest('hex'),
);
const actual = Buffer.from(signature);
return expected.length === actual.length && timingSafeEqual(expected, actual);
}Retries
Each webhook tracks its own deliveries, so when one endpoint is down only that one is retried. What happens depends on your reply:
- 2xx within 10 secondsDelivered. The body of your reply is ignored.
- 429, 5xx, a timeout or no connectionRetried, waiting longer each time — 8 attempts in all, then marked failed.
- Any other 4xxFailed at once, not retried. A 401 for a bad signature is final.
- A redirectNot followed — it counts as no connection. Point the webhook at the final URL.
Send any event again
Public HTTPS only
Troubleshooting
The signature never matches
Check that you sign the raw request body, not a parsed and re-serialised copy, and that the secret is the current one — a new signing secret replaces the old immediately. Then check your server’s clock: a timestamp more than five minutes off is refused.
The same event arrives twice
Retries and redeliveries resend the same event. Store the payload id, or X-Duma-Delivery, and skip ones you have already handled.
The test works but real events never arrive
Check the webhook is switched on, subscribed to the event and not limited to other content types. Saving a draft sends nothing; publishing does.
The URL is refused when I save it
It must be HTTPS and resolve to a public address — localhost, private networks and URLs with credentials are refused. To test locally, use a tunnel that gives you a public HTTPS URL.