Content Lake (Datastore)

Webhook best practices

Best practices for configuring webhooks and handling them in your system.

This article describes best practices for configuring Sanity webhooks and for handling them in your system.

Configuration

GROQ webhooks should be configured to trigger on the narrowest possible set of changes. Make sure the filter is as specific as possible and avoid triggering webhooks on draft changes unless absolutely necessary. Drafts can change frequently as content is being edited, which could result in a high volume of webhooks that may be costly or overwhelming for your systems. The same applies to version documents from Content Releases. Documents in the drafts. and versions. ID namespaces are ignored by default and are controlled by separate settings: enable "Trigger webhook when versions are modified" in sanity.io/manage, or set includeAllVersions: true via the Webhooks API, only when your integration genuinely needs unpublished release content. Version support requires webhook API version v2025-02-19 or later.

Delays

In rare circumstances there can be delays in the delivery of webhooks. If receiving timely updates is critical to your app, this should be considered in webhook handling. For example, you could check the sanity-transaction-time header and compare this to the current date and time — if you see times over a certain age, you might trigger a catch-up using API calls.

Delays could also mean webhooks can potentially be received out of order. Therefore it can be useful to check the _updatedAt value on a document to ensure you're using the latest data. It can sometimes be worth considering whether you're best to use the data in a webhook or use the webhook to trigger a query.

Recovery from downtime

It's important to consider that downtime can occur with any webhook setup, whether it's on the side of your application or the provider itself.

To mitigate the impact of potential downtime, implement a mechanism for recovering missed data through API calls. This ensures your application can stay up to date even if webhooks are temporarily unavailable. Plan for a short retry window: Sanity retries a failed delivery twice at 30-second intervals, then marks the message failed — roughly one minute of tolerance, not hours. Only 429 and 500-range responses are retried; a 400-range response is treated as undeliverable and is never retried. If your queue is saturated, return 429 or a 500-range status rather than a 4xx. Because the retry window is this short, a reconciliation path via API calls is required, not optional, for any outage longer than about a minute.

Idempotence

Idempotence in the context of webhooks refers to the ability to process the same webhook payload multiple times without adverse effects.

For example, if a webhook is delivered and processed successfully, but the acknowledgment response fails to reach the sender due to a network issue, the sender might retry sending the same payload. In an idempotent system, receiving and processing the same payload again would not result in duplicate data or unintended side effects.

Sanity provides an idempotency-key header which you can use to ignore messages that might be in a state of being processed or that have been processed already. By checking the idempotency-key, you can ensure that your application processes each unique webhook payload only once, even if it is delivered multiple times.

Reconciliation

Relying solely on webhooks isn't recommended in any application — delivery can't always be guaranteed due to network issues, application downtime, or other factors.

You might want to run regular sync jobs — at hourly, daily, or other intervals — to make sure everything updated between syncs is reconciled. This sync could filter using the _updatedAt field on Sanity documents to find everything which has changed since the last sync.

Scalability

As the volume of webhooks received by your application increases, it can become challenging to process all of them in real time. Sanity sends webhook requests for a given webhook one at a time — deliveries are limited to one concurrent request, so your endpoint is never hit in parallel. But if your handler takes longer to respond than the interval at which triggering changes occur, the delivery queue falls behind and never clears. Check the message log (GET https://<your-project-id>.api.sanity.io/v2021-10-04/hooks/WEBHOOK_ID/messages) — messages stuck at status queued mean your processing has fallen behind, or your filter is too broad. To handle this, implement a queuing system for incoming webhooks.

When a webhook is received, instead of processing it immediately, your application should add it to a queue for asynchronous processing. This allows your webhook endpoint to quickly acknowledge receipt of the webhook and return a response within the 30-second timeout window Sanity implements.

It's important to note that the response returned by your webhook endpoint should indicate that the webhook was received successfully, not that it was fully processed. This distinction is crucial because the actual processing of the webhook happens asynchronously through the queue.

By decoupling the receipt and processing of webhooks using a queue, you can ensure that your application remains responsive and can handle a high volume of incoming webhooks without overwhelming your system. The queue acts as a buffer, letting you process webhooks at a pace that your application can handle, while still acknowledging their receipt in a timely manner.

Implementing a robust queuing system for webhook processing is a best practice for building scalable and reliable applications that can handle increasing webhook traffic as your system grows.

Security

When setting up webhooks, consider the security measures that protect your application and data. Your webhook endpoint is publicly reachable, so anyone who finds its URL can send requests to it. Verify that every payload you receive comes from Sanity.

Here are a few key points to keep in mind:

  • Secrets: Sanity lets you configure a secret token for your webhooks. This secret should be a unique, random string that is only known to your application and Sanity. Sanity never sends the secret itself. Instead, it uses the secret to sign each payload and sends the result in a sanity-webhook-signature header, formatted t=<timestamp>,v1=<signature>, where the signature is an HMAC-SHA256 of <timestamp>.<raw request body> encoded as base64url. Your application should recompute the signature from the raw, unparsed request body and compare it to the header value. Parsing the body before verifying will produce mismatches.
  • Webhook toolkit: Sanity offers a webhook toolkit, which is a set of utilities for handling webhooks in a secure and reliable manner. The toolkit includes features like signature verification, which helps ensure the integrity and authenticity of the webhook payloads you receive. Although the toolkit is written in TypeScript, the concepts and principles it promotes are language-agnostic.
  • IP allowlisting: Sanity provides a specific set of IP addresses from which webhooks are sent. You can configure your application to only accept webhook requests originating from these trusted IP addresses. This adds an extra layer of security by preventing unauthorized sources from sending fake webhook payloads to your endpoint.

By implementing these security measures, you can protect your application from potential threats and ensure that the webhooks you receive are genuine and trustworthy.

Was this page helpful?