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.
Creating a webhook
Section titled “Creating a webhook”In the dashboard, go to Settings → Webhooks → Add webhook:
- Enter your endpoint URL (it must accept
POST). - Choose which events to subscribe to (or All events).
- 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.
Events
Section titled “Events”| Event | Fires when |
|---|---|
content.published | A page content entry is published |
object_field.published | A scalar object field is published |
collection_item.published | A collection item (e.g. a gallery image) is published |
collection_item.removed | A 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).
Payload
Section titled “Payload”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" }}addressis the canonical identifier of the changed value:type:id#fieldfor scalars,type:id#field[itemId]for collection items.versionis a monotonic clock. If you mirror values into your own database, apply an event only when itsversionis greater than the last one you applied for thataddress— that makes retries and out-of-order deliveries safe.content.publishedcarries{ entryId, pageId, pagePath, componentId, instanceKey, fields }.
Verifying the signature
Section titled “Verifying the signature”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.
Delivery & retries
Section titled “Delivery & retries”- A delivery succeeds on any
2xxresponse. 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 onid. - 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.
Testing
Section titled “Testing”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.
Self-hosting notes
Section titled “Self-hosting notes”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.