Skip to content

Webhooks

Subscribe your backend to signed InlineCMS publish events.

Webhooks let your backend react the moment content is published in InlineCMS. When an editor (or an API key) publishes a change, InlineCMS sends a signed POST to every subscribed endpoint. Deliveries are HMAC-signed, retried with backoff, and logged so you can see exactly what happened.

This is the read-side complement to the SDK: instead of polling for changes, you get pushed a verifiable event whenever something goes live.

In the dashboard, go to Settings → Webhooks → Add webhook:

  1. Enter your endpoint URL (it must accept POST).
  2. Choose which events to subscribe to (or All events).
  3. Copy the signing secret — it’s shown once. Store it next to your endpoint code; you’ll use it to verify every delivery.

You can pause a webhook (the dot toggle), edit its URL/events, rotate its secret, send a test event, and inspect recent deliveries — all from the same card.

EventFires when
content.publishedA page content entry is published
object_field.publishedA scalar object field is published
collection_item.publishedA collection item (e.g. a gallery image) is published
collection_item.removedA collection item is removed and published

Draft saves do not fire — publishing is the signal. Subscribe to specific events or to * (all, including events added in future releases).

Every delivery is a JSON body of this shape:

{
"id": "evt_8f3c…", // idempotency key — dedupe on this
"type": "object_field.published",
"projectId": "…",
"occurredAt": "2026-06-08T12:00:00.000Z",
"version": 1749384000000, // monotonic clock (epoch ms) for last-write-wins
"data": {
"objectType": "property",
"objectId": "prop_123",
"fieldPath": "description",
"address": "property:prop_123#description", // canonical id of what changed
"value": "New copy…",
"valueType": "string"
}
}
  • address is the canonical identifier of the changed value: type:id#field for scalars, type:id#field[itemId] for collection items.
  • version is a monotonic clock. If you mirror values into your own database, apply an event only when its version is greater than the last one you applied for that address — that makes retries and out-of-order deliveries safe.
  • content.published carries { entryId, pageId, pagePath, componentId, instanceKey, fields }.

Each request carries an X-InlineCMS-Signature header:

X-InlineCMS-Signature: t=1749384000,v1=9f86d0818…

v1 is the HMAC-SHA256 of the string "<t>.<rawBody>", keyed with your webhook secret. To verify: recompute it over the raw request body, constant-time compare, and reject if the timestamp is more than 5 minutes old (replay protection).

import { createHmac, timingSafeEqual } from 'node:crypto'
export function verifyInlineCMSWebhook(rawBody: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(header.split(',').map((kv) => kv.split('=')))
const timestamp = Number(parts.t)
if (!Number.isFinite(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > 300) return false
const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex')
const a = Buffer.from(expected, 'hex')
const b = Buffer.from(parts.v1, 'hex')
return a.length === b.length && timingSafeEqual(a, b)
}

Example handler (Express):

app.post('/hooks/inlinecms', express.raw({ type: 'application/json' }), (req, res) => {
const raw = req.body.toString('utf8')
const sig = req.header('X-InlineCMS-Signature') ?? ''
if (!verifyInlineCMSWebhook(raw, sig, process.env.INLINECMS_WEBHOOK_SECRET!)) {
return res.status(400).send('bad signature')
}
const event = JSON.parse(raw)
// … handle event.type / event.data …
res.sendStatus(200)
})

Verify over the raw bytes. Frameworks that parse JSON before you see it will re-serialize the body and change the bytes, breaking the signature. Read the raw body (as above) for the check.

  • A delivery succeeds on any 2xx response. Anything else (or a timeout/connection error) is a failure and is retried.
  • Backoff: ~30s, 2m, 10m, 1h, then 6h, up to 8 attempts, after which the delivery is marked dead. You can re-send any failed/dead delivery from the delivery log (“Retry”).
  • Idempotency: the same logical event keeps the same id. Your handler should be idempotent — dedupe on id.
  • Ordering is not guaranteed on the wire. Use version, not arrival order, for correctness.
  • Respond quickly (within ~10s) and do slow work asynchronously, or InlineCMS will treat the delivery as failed and retry it.

Use Test on any webhook to send a synthetic ping event immediately and see the response code. The delivery log shows the recent attempts with status, response code, attempt count, and a Retry action for failures.

The webhook engine ships in the OSS server (no Cloud required). It runs an in-process delivery worker — right for a single instance. Set WEBHOOKS_ENABLED=false to turn the engine off entirely.

Was this page helpful? Your feedback goes straight to the docs team.