Webhooks

The Webhooks collection (packages/payload-preset) is the integration mechanism itself, not an integration with any one third-party service - register any URL and it gets notified when content changes. No vendor is hardcoded, so it works equally well for a Slack incoming webhook, a Zapier catch hook, a static-site rebuild trigger, or your own custom endpoint.

Configuring one

In Payload admin, go to Webhooks and create an entry with a name, the target url, a secret, and which events it should fire for. Both read and write access require a logged-in user, since a webhook's secret is sensitive.

Events

Both are dispatched from the Pages collection's afterChange/afterDelete hooks (see packages/payload-preset/src/webhooks/dispatch.ts), so they fire no matter how the change happens - the admin panel, the REST/GraphQL API, or the Local API - not just through this app's own server actions.

Payload shape

{
  "event": "page.published",
  "slug": "home",
  "locale": "en",
  "title": "Home",
  "timestamp": "2026-01-01T12:00:00.000Z"
}

Verifying the signature

Every request carries an X-Olgax-Signature header - sha256=<hex>, an HMAC-SHA256 of the raw request body keyed with that webhook's own secret (the same convention GitHub and Stripe use for their webhooks). Verify it before trusting the payload:

import { createHmac, timingSafeEqual } from "crypto";

function isValid(secret: string, rawBody: string, header: string | null) {
  if (!header) return false;
  const expected = `sha256=${createHmac("sha256", secret).update(rawBody).digest("hex")}`;
  return (
    header.length === expected.length &&
    timingSafeEqual(Buffer.from(header), Buffer.from(expected))
  );
}

A slow or unreachable receiver never blocks or fails the publish/delete that triggered it - dispatch happens in parallel with a 5-second timeout per request, and a failure is swallowed rather than surfaced to the editor.