Agent skill

Sanity Webhooks Skill

Receive and verify Sanity GROQ-powered webhooks. Use when setting up Sanity webhook handlers, debugging signature verification with the sanity-webhook-signature header, or handling Content Lake document create/update/delete events for cache revalidation and search reindexing.

Install this skill

npx skills add hookdeck/webhook-skills --skill sanity-webhooks


When to Use This Skill

  • How do I receive Sanity webhooks?
  • How do I verify Sanity webhook signatures?
  • Why is my sanity-webhook-signature verification failing?
  • How do I trigger cache revalidation or search reindexing when a document changes?
  • How do I handle document create, update, and delete events from the Content Lake?

How Sanity Webhooks Work

Sanity uses GROQ-powered webhooks. There are no fixed event-type strings. Instead, each webhook is configured at sanity.io/manage with:

  • A GROQ filter that decides which document changes fire the webhook (e.g. _type == "post", or delta helpers like delta::changedAny(...)).
  • A GROQ projection that shapes the request body (JSON). If left empty, the payload is the whole document after the change, which always includes _id, _type, and _rev.

Handlers therefore dispatch on the document's _type (and any fields you project), not on a provider-defined event name. Webhooks fire on create / update / delete in the Content Lake and ignore draft and version documents by default.

Verification (core)

Sanity signs with the official @sanity/webhook package (v4 requires Node 18+). The sanity-webhook-signature header is Stripe-style — t=<ms-timestamp>,v1=<sig> — an HMAC-SHA256 over `${timestamp}.${rawBody}` (timestamp in milliseconds), base64url encoded with no padding. Pass the raw request body — do not JSON.parse first.

const { isValidSignature, SIGNATURE_HEADER_NAME } = require('@sanity/webhook');
// SIGNATURE_HEADER_NAME === 'sanity-webhook-signature'

const signature = req.headers[SIGNATURE_HEADER_NAME];

// isValidSignature is async in v4+ and returns a boolean (never throws on a
// bad signature). It recomputes the HMAC from the timestamp in the header.
const valid = await isValidSignature(
  rawBody,                              // raw HTTP body string — NOT parsed JSON
  signature,
  process.env.SANITY_WEBHOOK_SECRET,   // secret from sanity.io/manage
);
if (!valid) return res.status(400).send('Invalid signature');

No official Python package exists — for FastAPI, verify manually (parse t/v1, recompute the base64url HMAC, timing-safe compare). See the FastAPI example.

For complete handlers with route wiring, event dispatch, and tests, see:

Document Types (dispatch targets)

There are no fixed events. Dispatch on the projected _type. Common studio types:

_typeTriggered whenCommon use case
postA blog post is created/updated/deletedRevalidate /blog/[slug]
authorAn author document changesRevalidate author pages
productA product changesRevalidate storefront, reindex search
categoryA category changesRebuild navigation
pageA page document changesRevalidate the page route

Docs: Sanity Webhooks · GROQ filters & projections

Environment Variables

SANITY_WEBHOOK_SECRET=your_webhook_secret   # Set when creating the webhook at sanity.io/manage

Delivery & Idempotency

  • At-least-once delivery: 1 concurrent request, 2 retries at 30s intervals, 30s timeout. Don't rely on webhooks as your only source of truth.
  • Deduplicate using the idempotency-key request header.
  • See webhook-handler-patterns for idempotency and retry handling.

Local Development

# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 sanity --path /webhooks/sanity

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Aug 3, 2026

View on GitHub →