Implementation Patterns6 min read

What the Best Headless CMSes Do With Webhooks

Your content team hits publish, and forty minutes later the new pricing page is still showing the old numbers because the CDN never got the signal to purge.

Published June 30, 2026

Your content team hits publish, and forty minutes later the new pricing page is still showing the old numbers because the CDN never got the signal to purge. Or worse: a webhook fired on every nested patch during a bulk import, hammered your build pipeline with three hundred near-simultaneous deploys, and you blew through your hosting platform's concurrency limit before lunch. Webhooks are the connective tissue between a headless CMS and everything downstream, and when they misfire, the failure is always visible to someone who matters.

Sanity treats webhooks as a first-class, queryable part of the Content Operating System rather than a fire-and-forget afterthought bolted onto a publish event. The difference between a CMS that broadcasts dumb "something changed" pings and one that lets you filter, project, and shape the payload before it ever leaves the platform is the difference between a brittle integration and a durable one.

This article walks the implementation patterns the best headless platforms use: filtering at the source, projecting the exact payload a consumer needs, signing and verifying delivery, handling retries and idempotency, and choosing between webhooks and event-driven Functions. The goal is a delivery layer you can reason about, not one you babysit.

Why naive webhooks break at scale

The default webhook in most systems is a notification: an object changed, here is its ID, go figure out the rest. That model is fine for a blog. It collapses the moment you have volume, fan-out, or strict consumers. The classic failure modes are predictable. First, over-firing: a bulk import or a scripted migration touches ten thousand documents, and your endpoint receives ten thousand events in a burst, each one triggering a rebuild or an API call you are billed for. Second, under-specifying: the payload is just an ID and a type, so every consumer has to turn around and query the CMS to learn what actually changed, doubling your request volume and adding latency to the very pipeline you were trying to make reactive. Third, no contract: the receiver cannot tell a legitimate delivery from a forged POST, so either it trusts everything or you bolt on a shared secret nobody rotates.

The consequence is that teams quietly stop trusting their webhooks. They add a nightly cron that re-syncs everything just in case, which defeats the point of having events at all, and now you are paying for both the event infrastructure and the polling that backstops it. The pattern that scales is the opposite of fire-and-forget. You decide, at the source, which changes are worth broadcasting; you shape the payload so the consumer needs zero follow-up queries; and you give the receiver a way to prove the delivery is real. A platform that lets you do all three at the source turns webhooks from a liability into the cheapest, most reliable integration surface you have. The rest of this guide is how the better platforms actually implement that.

Filter at the source, not at the consumer

The single highest-leverage move in webhook design is deciding what fires before anything leaves the platform. If your only filter is document type, every consumer downstream inherits the cost of figuring out whether a given change is relevant to them. Move that decision upstream and the savings compound across every integration you ever wire up.

Sanity implements this with GROQ filters on the webhook itself. Instead of subscribing to all changes on a `product` document, you write a projection-aware filter such as `_type == "product" && defined(slug.current) && price != oldPrice`, and only the deliveries that satisfy it ever fire. Because the filter is GROQ, you get the same query language you already use against Content Lake: references resolved with `->`, array filters with `[...]`, and conditional logic evaluated against both the current and previous state of the document. That means you can fire a webhook only when a field that actually matters to the consumer changed, not on every autosave keystroke or every unrelated patch to the same document.

The practical effect is dramatic. A team rebuilding a storefront does not want a deploy when an editor fixes a typo in an internal note field; they want one when `slug`, `price`, or `availability` changes. Expressing that as a GROQ filter on the webhook means the build pipeline simply never hears about the irrelevant edits. Contrast that with platforms where the webhook fires on any change to the entry and the filtering logic has to live in a serverless function you write, deploy, and maintain. Source-side filtering is not a convenience feature; it is the thing that keeps your event volume proportional to your meaningful changes instead of your total edits.

Project the payload so consumers never call back

A webhook that ships an ID forces a round trip. The consumer receives the event, then queries the CMS to hydrate it, and now your reactive pipeline has a hidden synchronous dependency on an API that might be rate-limited, might be mid-deploy, or might return a draft when you wanted published. The better pattern is to send exactly the shape the consumer needs in the delivery itself.

Sanity webhooks carry a GROQ projection, so the payload is whatever you ask for. You can ship `{ _id, "slug": slug.current, title, "author": author->name, "heroUrl": heroImage.asset->url }` and the receiver gets a fully resolved, denormalized object with references already dereferenced, in one delivery, with no follow-up call. This is the same one round trip advantage GROQ gives you everywhere: ask for the exact shape you need, including projections, references, and filters, and get it back assembled. For a webhook, that means the consumer can act on the payload immediately rather than treating it as a pointer.

Projection also lets you decouple your internal content model from your integration contract. Your document might have forty fields and a deeply nested structure, but the search indexer downstream only needs five flattened ones. Project those five, and you have given that consumer a stable, minimal contract that does not break every time you refactor the schema. When the indexer's needs change, you edit the projection on the webhook, not the consumer's code. The payload becomes a versionable interface you control from inside the platform, which is exactly where the contract between your content and the outside world should live.

Sign, verify, and survive retries

Once a webhook does something consequential, like triggering a payment reconciliation or invalidating a cache, the receiver has to answer two questions on every request: is this delivery authentic, and have I already processed it. Getting either wrong is how you end up double-charging a customer or serving a stale page indefinitely.

Authenticity is solved with signing. Sanity signs each delivery with a secret and includes a signature header the receiver recomputes and compares, so a forged POST to your public endpoint is rejected before it does anything. This is table stakes for any endpoint exposed to the internet, and it is markedly better than the alternative of passing a static token in a query string that ends up in access logs. Rotate the secret on a schedule and you have a verifiable, auditable delivery channel.

Idempotency is the harder discipline because it lives in your code, not the platform's. Networks fail, receivers time out, and any honest delivery system retries, which means your endpoint will eventually see the same event twice. The fix is to make processing idempotent: key your work on the document `_id` plus a revision or timestamp from the payload, record what you have handled, and treat a repeat as a no-op. Design the consumer so that receiving the same delivery five times produces the same end state as receiving it once. Combine signed deliveries, sensible retry behavior on the platform, and idempotent receivers, and you get the property that actually matters in production: a webhook layer that is correct under failure, not just under the happy path. That correctness is what lets you delete the nightly re-sync cron and trust the events.

When to graduate from webhooks to Functions

Webhooks are an outbound notification: the platform tells an external system that something happened, and that system decides what to do. That boundary is exactly right when the work belongs to another service, like a CDN purge, a Slack message, or a downstream build. But a lot of content automation does not belong outside the platform at all, and forcing it through a webhook means standing up a server, exposing an endpoint, securing it, and round-tripping back into the CMS to write the result.

For that class of work, Sanity Functions run serverless logic in response to content events without you operating any infrastructure. When a document is created or updated, a Function can enrich it, translate a field, moderate an image, or set a derived value, and write the result straight back to Content Lake. There is no public endpoint to secure, no signature to verify, and no separate deploy target; the automation lives next to the content it acts on. Paired with the App SDK, the same model extends to in-Studio apps that react to and reshape content where editors actually work.

The decision rule is about ownership of the side effect. If the consequence is another system's responsibility, fire a webhook with a precise GROQ filter and projection and let that system own its retries and idempotency. If the consequence is a change to your own content, a Function is usually the cleaner tool because it removes an entire network hop, an auth surface, and a deploy pipeline from the path. The best implementations use both deliberately: webhooks at the platform boundary, Functions for content that needs to react to itself. Treating them as interchangeable is how you end up with a fragile mesh of endpoints doing work that should have stayed inside the platform.

Ready to try Sanity?

See how Sanity can transform your enterprise content operations.