Merchandising publishes a liveshow in BamHub and expects it on the storefront overview within minutes. A Next.js route handler receives Bambuser webhooks, maps fields onto Sanity documents, and the public /liveshow page reads that dataset. Ops noticed shows that looked live in BamHub never appeared in the Sanity-powered list. No error in Slack. The webhook returned 200. The gap was permanent until someone opened Sanity Studio and toggled publish by hand.
BamHub event payloads are sparse. A show-updated webhook might omit published, ship a stale flag, or skip fields the storefront query filters on. Writing the JSON body straight into a Sanity patch looked correct in logs: 200, document touched, webhook acknowledged. Sanity simply stayed in draft or missing state relative to what the public GROQ query required. One bad write was enough. Retries from the vendor would not fix data we had already persisted wrong.
Hydrate before you patch Sanity
When a server-side Bambuser API key is configured, the handler now calls GET /shows/{id} before any Sanity write. That response carries the authoritative publish state, title, schedule, and identifiers the sparse webhook skipped. The patch applies the hydrated document shape, not the raw webhook alone.
If hydration fails with a server error, rate limit, or network fault, the handler does not write Sanity at all. It responds with 503 Service Unavailable so Bambuser's delivery pipeline retries later. Transient outages therefore never freeze a half-truth document in the CMS. Client errors (4xx from the API after a bad id) still map to 500 so we do not retry forever on garbage input.
When no API key is present (local dev or a stripped environment), the code falls back to payload-only sync. That path is explicit and logged so nobody confuses it with production behavior.
const hydrated = await fetchShowFromBambuser(showId);
if (!hydrated.ok && hydrated.retryable) {
return new Response('Upstream unavailable', { status: 503 });
}
await sanity.patch(showId).set(buildSyncFields(hydrated.data ?? body)).commit();Why 503 beats a optimistic 200
Acknowledging a webhook with 200 while writing incomplete fields trains the vendor to stop trying. The show stays wrong in Sanity forever. Returning 503 on hydrate failure keeps the event in the retry queue. Idempotent patches mean a later attempt can still land the full document without manual cleanup.
I log hydrate failures with the show id and upstream status. Ops can correlate a burst of 503s with BamHub incidents instead of debugging missing rows days later.
Sanity patches stay idempotent: the same show id upserts the same document shape. A retry after 503 does not duplicate rows or fork history. That mattered when choosing 503 over writing partial fields just to silence the webhook.
For local handlers I still validate signature and map BamHub ids onto Sanity _id values before any network call. Hydration adds latency on every event. The trade is fewer silent CMS drifts and fewer all-hands "why is this show missing" threads.
Sparse vendor webhooks are notifications, not snapshots. Treat them as a trigger to re-read the source of truth, and refuse to commit CMS state when that read fails.
Happy coding! Sander