POST to your URL whenever something you subscribed to happens in a project: a record is published, a field changes, an asset is deleted. Use it to rebuild a site, clear a cache or sync another system, instead of polling the API.
Create a webhook
In the web app, open your project’s Webhooks page and choose New webhook. Enter the URL and an optional Description. Turn on Published content only to send only changes to published content (see below). Then choose the events to send: turn on All events, or tick a whole group (such as all records,record.*) or single event types. Choose Create webhook, then copy the Signing secret. It is shown only once.
Through the API, create one with an API key that has the webhooks:write scope. The response holds the secret, and it is the only one that does:
$PROJECT_ID is your key’s project (prj_…): GET /v1/me returns it as api_key.project_id. The SDKs fill it in for you:
https, without a username or password, and on the public internet: addresses on private networks are refused. How many webhooks you can have depends on your organization’s plan.
To check your endpoint, use Send test event from the webhook’s menu, or POST /v1/webhooks/{webhook_id}/test. It sends a webhook.test event to that webhook only.
Event types
Event types are the activity log’s, by resource:
Merging a draft sends
record.updated, with the merged draft’s draft_id, since it changes the record’s content.
A subscription lists event types, a resource’s wildcard (record.*) or * for everything. Wildcards include event types added later.
Published content only
A site or app that reads published content only needs to hear about changes to it. Withpublished_only: true, a webhook sends only the events that change what a published-only API key reads:
- Record events when the record was published before or after the change: publishing, unpublishing, editing a live record (or merging a draft into it), and deleting or restoring a published record. Edits to unpublished records are skipped.
- Every collection and field event, since published-only keys read the schema.
- Asset events for assets that published records use. A new upload is skipped until a published record uses it.
asset.created) or purges (asset.purged), and a published-only webhook can’t subscribe to them by name. Wildcards and * are fine. When a scheduled publish happens, it sends record.published like any other. Test events are always sent.
To rebuild a static site, for example through a build service’s deploy hook:
PATCH /v1/webhooks/{webhook_id} and {"published_only": true}, or with the webhook’s Edit in the web app.
The payload
Each request’s body is one JSON event:actoris who made the change (a user, an API key, or the system), andviathe client it came through (ui,api,cli,sdk,mcp,systemorscheduler). An integration that writes back can skip changes made with its own API key.- Record events carry the record as the change left it, with the
revisionit made and the revision’s ID. Events without a revision (record.scheduled,record.unscheduled,record.schedule_failed,record.redacted) carry the record as it is now. Once a record is purged, every event about it hasrecord: null. - Draft events carry the draft as it is now (
nullonce merged or discarded), collection and field events the collection and the schema change, and asset events the asset (only itsidonce it is purged, as inasset.purged).
Delivery
Every request hasContent-Type: application/json and three headers: webhook-id (the event’s ID), webhook-timestamp and webhook-signature.
- Success is any
2xxresponse within 10 seconds. Any other status, a timeout or a connection error is a failed attempt. Redirects aren’t followed, so a3xxfails too. - Retries follow a failed attempt after 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours. After 8 attempts, about a day, the delivery fails.
- At least once. The same event can arrive more than once, for example when your endpoint was slow to answer. Deduplicate by
webhook-id, which stays the same on every attempt. - No ordering. A retried event can arrive after later ones. Compare
created_at, or a record’srevision, to tell which change is newer.
Delivery history and retries
Choose Deliveries on a webhook to see its deliveries for the last 30 days: Delivered, Retrying or Failed, with each attempt’s status code, error, duration and the start of your response, and the payload. Retry now sends a delivery again at once; on a delivered one, Send again does the same. Through the API,GET /v1/webhooks/{webhook_id}/deliveries lists them (newest first; filter[status] takes pending, succeeded or failed, which the web app shows as Retrying, Delivered and Failed), and POST /v1/webhook-deliveries/{delivery_id}/retry sends one again:
Disabling and deleting
There is no automatic disabling: a failing endpoint keeps getting new events, and each one fails on its own after its retries. To pause a webhook, choose Disable from its menu (or setenabled to false). A disabled webhook gets no new events, events from while it was off are not sent later, and its pending retries fail.
Delete stops a webhook the same way. A deleted webhook can be restored for 30 days, with its settings, secrets and delivery history (POST /v1/webhooks/{webhook_id}/restore).
Verify signatures
Deliveries are signed per Standard Webhooks, so its libraries verify them too. Always verify before you act on an event. With an SDK,unwrap (unwrapWebhook in TypeScript) checks the signature and the timestamp and returns the typed event: see Verify webhooks. Pass it the raw body, before any JSON parsing, since parsing and serializing again changes the bytes.
To verify by hand:
- Build the signed string:
{webhook-id}.{webhook-timestamp}.{raw body}. - Compute its HMAC-SHA256, keyed with the base64-decoded part of your secret after
whsec_, and base64-encode it. webhook-signatureholds one or more space-separated signatures, eachv1,<base64>. Accept the request if any of them equals yours, compared in constant time.- Reject the request if
webhook-timestamp(Unix seconds, new on each attempt) is more than five minutes from your clock.
Rotate the secret
Rotate a webhook’s secret through the API, for example when it may have leaked.POST /v1/webhooks/{webhook_id}/secrets (rotateSecret in TypeScript, rotate_secret in Python and Ruby) returns the new secret, shown only then.
For 24 hours after a rotation, each delivery carries a signature for the old secret and one for the new, so you can switch your receiver to the new secret without rejecting any. To stop signing with the old one sooner, find its ID with GET /v1/webhooks/{webhook_id}/secrets and retire it with DELETE /v1/webhook-secrets/{secret_id}. The newest secret can’t be retired: rotate again instead.