Skip to content

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.

  1. Open Settings → Webhooks and choose Add webhook.
  2. Enter the URL of your receiver.
  3. Tick the Events it should receive: entry.created, entry.updated, entry.published, entry.unpublished, entry.deleted.
  4. Choose a Content type to limit it to one type, or leave it on all.
  5. 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.

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.

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.

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);
}
  • 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 2xx response 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.