webhooks.create, or under API > Webhooks at sanity.io/manage, point its URL at your Corsair HTTP handler, and give it a secret. Each Sanity webhook has its own secret. Store the same value with corsair.sanity.keys.set_webhook_signature(secret). Corsair keeps one secret per tenant, so all of one tenant’s webhooks must share it, and different tenants should use different secrets. sanity({ webhookSecret }) covers accounts with no stored secret, and a stored one takes precedence. Use that option only in a single-tenant app. Every tenant without a stored secret falls back to it, and then a delivery signed for one tenant also verifies under another’s tenantId.
Only signed deliveries are accepted. If no secret is set, or a delivery fails verification, processWebhook reports a failure and no hook runs after it. The handler below then answers 500, and Sanity retries twice, 30 seconds apart. Signatures more than 15 minutes from now are refused. Deliveries can repeat, so deduplicate by idempotencyKey.
On Corsair’s cloud, Hub routes each delivery by its sanity-project-id header to the account that stored that project id. In your own handler, processWebhook takes the tenant from ?tenantId= and uses default without it. In a multi-tenant app, put ?tenantId=<tenant> on each tenant’s webhook URL. The handler below forwards the query to processWebhook. The signature covers the body but not the URL or the headers, so give every tenant’s webhook its own secret. A delivery replayed under another tenantId then fails that tenant’s check. A before hook runs ahead of the signature check, so it can receive an unsigned or forged delivery. Keep side effects in after.
Webhook map
documentschanged(documents.changed)
HTTP handler setup
app/api/webhook/route.ts
Events
Documents
Changed
documents.changed
A GROQ-powered webhook fired: a document matching its filter was created, updated or deleted. The body is the webhook’s projection; the operation, document id and dataset come from its headers. Only signed deliveries are accepted.
Payload: any
Response data full type
Response data full type
webhookHooks example