DUMA/ API

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

  1. 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 → Developers
  2. Step 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.

  3. Step 3: Send a test

    “Send a test” delivers a signed ping event 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.

Request body
{
  "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. id is the document id the Content API returns; entryId is this language’s copy. type is the content type key, so you can rebuild only what changed.
data.sourcestring
editor or schedule, on entry.published and entry.unpublished.
editorschedule
data.assetobject
asset.changed only: the asset’s id, with data.change saying what happened.
createdupdateddeleted

Headers

X-Duma-Signaturestring
v1= 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-Typestring
application/json.
User-Agentstring
DUMA-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.
Verify a webhook
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.
Deliveries

Send any event again

A webhook’s Deliveries list shows each attempt with the status your endpoint returned. Redeliver sends one again now, with the same id.
URL rules

Public HTTPS only

The URL must use HTTPS, carry no username or password, and resolve to a public internet address. It is checked when you save and before every send.

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.