Webhooks
Webhooks notify an external endpoint when an entry changes, so you can trigger a rebuild, purge another cache or sync a system. They are managed under Settings → Webhooks and need the Admin role or above.
Create one
Section titled “Create one”- Open Settings → Webhooks and choose Add webhook.
- Enter the URL of your receiver.
- Tick the Events it should receive:
entry.created,entry.updated,entry.published,entry.unpublished,entry.deleted. - Choose a Content type to limit it to one type, or leave it on all.
- Save. The signing secret is shown once, with a copy button. Store it now: it is never shown again.
Each row has an enabled switch, and actions to view the delivery log, edit, regenerate the secret (the old secret stops working immediately) and delete.
entry.updated also fires alongside entry.published and entry.unpublished on a status-changing update, so a subscriber that wants “anything changed” can listen to entry.updated alone. Only entry events exist; pages do not send webhooks.
Private destinations
Section titled “Private destinations”By default a webhook cannot point at localhost, a private network, link-local or cloud-metadata addresses, and redirects are checked the same way. Allow private destinations turns that off for one webhook. Enable it only if you are deliberately calling something on your own private network.
What is sent
Section titled “What is sent”POST with Content-Type: application/json and two headers:
| Header | Value |
|---|---|
X-Kenresoft-Event |
The event name, for example entry.published |
X-Kenresoft-Signature |
Hex HMAC-SHA256 of the raw request body, keyed with the signing secret |
{ "event": "entry.published", "data": { "entryId": "…", "contentTypeId": "…", "slug": "hello-world", "status": "published" }, "timestamp": "2026-09-21T12:00:00.000Z"}The payload is the entry’s identity and status only, never its content. Fetch the content from the public API if you need it.
Verify the signature
Section titled “Verify the signature”Compute the HMAC over the raw body, before any JSON parsing, and compare in constant time:
import { createHmac, timingSafeEqual } from 'node:crypto';
export function isValid(rawBody: string, signature: string, secret: string): boolean { const expected = createHmac('sha256', secret).update(rawBody).digest('hex'); const a = Buffer.from(expected); const b = Buffer.from(signature); return a.length === b.length && timingSafeEqual(a, b);}Delivery, timeouts and retries
Section titled “Delivery, timeouts and retries”- Deliveries are sent in the background, so a slow or dead receiver never delays an editor’s save.
- Each attempt times out after 10 seconds. A
2xxresponse is a success. - A failed delivery is retried on the 5-minute Cron Trigger, up to 5 attempts in total.
- Every attempt is recorded in the webhook’s Delivery log with the event, HTTP status and result.
Return a 2xx quickly and do any real work after responding. Delivery is at-least-once, so make your handler safe to run twice for the same event.