Validating Sanity webhooks in Next.js
Set up webhook-based cache revalidation in Next.js using parseBody from next-sanity/webhook. Covers path-based and tag-based revalidation for App Router and Pages Router.
When content changes in Sanity, you can use webhooks to revalidate cached pages in your Next.js application on demand. The next-sanity toolkit includes parseBody, a utility that validates webhook signatures and parses payloads, so you can build secure revalidation handlers with minimal code.
This guide covers two revalidation patterns:
- Path-based revalidation: revalidate specific routes when a document changes
- Tag-based revalidation: revalidate all pages that depend on a document type
Both patterns work with App Router and Pages Router API routes.
Before you start: this guide covers the mechanism for webhook-based revalidation. If you're deciding which caching strategy to use (time-based, tag-based, or path-based), see Caching and revalidation in Next.js.
Prerequisites
- A Next.js application with
next-sanityconfigured. - A Sanity project with permission to create webhooks.
- A shared secret for webhook signature validation.
Set up the shared secret
Create a SANITY_REVALIDATE_SECRET environment variable with a random string. This secret must match in both your Sanity webhook configuration and your Next.js application.
# .env.local SANITY_REVALIDATE_SECRET=<your-random-secret-string>
Add the same value to your hosting provider's environment variables (Vercel, Netlify, etc.).
The parseBody utility
parseBody from next-sanity/webhook handles signature validation and payload parsing in one call:
import { parseBody } from 'next-sanity/webhook'
const { isValidSignature, body } = await parseBody<MyPayloadType>(
request,
secret,
waitForContentLakeEvent
)The function accepts three arguments:
| Argument | Type | Description |
|---|---|---|
| request | Request | The incoming webhook request |
| secret | string | Your SANITY_REVALIDATE_SECRET value |
| waitForContentLakeEvent | boolean (optional) | When true, adds a short delay before returning. This gives the Content Lake time to propagate changes, preventing your revalidation from fetching stale data. Defaults to false. |
parseBody returns an object with:
isValidSignature:trueif the webhook request was signed with the correct secretbody: the parsed webhook payload, typed as the generic you provide
Set waitForContentLakeEvent to true if your client uses useCdn: true. The CDN may serve stale data for a few seconds after a mutation. The delay ensures your revalidated pages fetch fresh content. See Caching and revalidation in Next.js for more on how the Sanity CDN and Next.js data cache interact.
Path-based revalidation
Use path-based revalidation when you know which routes correspond to which documents. For example, a blog post at /posts/my-post maps directly to a Sanity document with slug.current === 'my-post'.
Create the API route
// src/app/api/revalidate-path/route.ts
import { revalidatePath } from 'next/cache'
import { type NextRequest, NextResponse } from 'next/server'
import { parseBody } from 'next-sanity/webhook'
type WebhookPayload = {
path?: string
}
export async function POST(req: NextRequest) {
try {
if (!process.env.SANITY_REVALIDATE_SECRET) {
return new Response(
'Missing environment variable SANITY_REVALIDATE_SECRET',
{ status: 500 }
)
}
const { isValidSignature, body } = await parseBody<WebhookPayload>(
req,
process.env.SANITY_REVALIDATE_SECRET,
true // wait for Content Lake propagation
)
if (!isValidSignature) {
return new Response(
JSON.stringify({ message: 'Invalid signature', isValidSignature, body }),
{ status: 401 }
)
}
if (!body?.path) {
return new Response(
JSON.stringify({ message: 'Bad Request', body }),
{ status: 400 }
)
}
revalidatePath(body.path)
return NextResponse.json({
message: `Revalidated path: ${body.path}`,
body,
})
} catch (err: unknown) {
console.error(err)
const message = err instanceof Error ? err.message : 'Unknown error'
return new Response(message, { status: 500 })
}
}Configure the webhook projection
The webhook needs to send a path value that matches your Next.js routes. Use a GROQ projection with the select() function to generate paths dynamically:
{
"path": select(
_type == "post" => "/posts/" + slug.current,
"/" + slug.current
)
}Extend this pattern for your own document types and route structure:
{
"path": select(
_type == "post" => "/posts/" + slug.current,
_type == "author" => "/authors/" + slug.current,
_type == "category" => "/categories/" + slug.current,
"/" + slug.current
)
}To revalidate all routes on demand, create an API route that calls revalidatePath('/', 'layout'). This is a blunt instrument, but useful as a fallback.
Tag-based revalidation
Use tag-based revalidation when a single document change affects many pages. For example, updating an author's name should revalidate every post that displays it.
Tag-based revalidation has two parts:
- Tagging queries: when fetching data, you associate queries with tags (covered in Caching and revalidation in Next.js)
- Busting tags: when content changes, a webhook calls
revalidateTag()to invalidate all queries with that tag
This section covers the webhook side.
Create the API route
// src/app/api/revalidate-tag/route.ts
import { revalidateTag } from 'next/cache'
import { type NextRequest, NextResponse } from 'next/server'
import { parseBody } from 'next-sanity/webhook'
type WebhookPayload = {
_type: string
}
export async function POST(req: NextRequest) {
try {
if (!process.env.SANITY_REVALIDATE_SECRET) {
return new Response(
'Missing environment variable SANITY_REVALIDATE_SECRET',
{ status: 500 }
)
}
const { isValidSignature, body } = await parseBody<WebhookPayload>(
req,
process.env.SANITY_REVALIDATE_SECRET,
true // wait for Content Lake propagation
)
if (!isValidSignature) {
return new Response(
JSON.stringify({ message: 'Invalid signature', isValidSignature, body }),
{ status: 401 }
)
}
if (!body?._type) {
return new Response(
JSON.stringify({ message: 'Bad Request', body }),
{ status: 400 }
)
}
revalidateTag(body._type)
return NextResponse.json({ body })
} catch (err: unknown) {
console.error(err)
const message = err instanceof Error ? err.message : 'Unknown error'
return new Response(message, { status: 500 })
}
}How tags connect to queries
When you fetch data using the manual sanityFetch helper with tags:
const posts = await sanityFetch({
query: POSTS_QUERY,
tags: ['post', 'author'],
})Next.js associates the cached response with both the post and author tags. When the webhook fires for a document with _type === 'post', calling revalidateTag('post') invalidates all cached queries tagged with 'post'.
See Caching and revalidation in Next.js for the full sanityFetch helper implementation and tagging patterns.
Configure the webhook projection
For tag-based revalidation, the projection is simple. You only need the document type:
{_type}Setting up the webhook in Sanity
Configure the webhook in your Sanity project dashboard:
| Field | Path-based | Tag-based |
|---|---|---|
| URL | https://your-domain.com/api/revalidate-path | https://your-domain.com/api/revalidate-tag |
| Events | Create, Update, Delete | Create, Update, Delete |
| Filter | _type in ["post", "author"] | _type in ["post", "author"] |
| Projection | {"path": select(...)} | {_type} |
| Secret | Your SANITY_REVALIDATE_SECRET | Your SANITY_REVALIDATE_SECRET |
| HTTP method | POST | POST |
Adjust the filter to include the document types your application uses.
Quick setup with webhook templates
Use these shareable templates to create pre-configured webhooks in your Sanity project:
After importing a template, update the URL to point to your deployed application and set the secret.
Security
Webhook endpoints are publicly accessible URLs. Follow these practices to keep them secure:
- Always validate signatures. Never skip the
parseBodyvalidation step, even in development. An unvalidated endpoint lets anyone trigger revalidation (or worse, if you add write operations later). - Use a strong secret. Generate a random string of at least 32 characters. Tools like
openssl rand -base64 32work well. - Keep the secret out of version control. Use
.env.localfor local development and your hosting provider's secrets management for production. - Consider rate limiting. In production, add rate limiting to your revalidation endpoints to prevent abuse. Most hosting providers offer this at the infrastructure level.
Pages Router
Webhook revalidation works with Pages Router API routes. The Live Content API is App Router only, but webhook-based revalidation is available on both routers.
The key differences from the App Router examples above:
- API routes live in
pages/api/instead ofsrc/app/api/ - The handler receives
reqandresobjects instead of aRequest - Use
res.revalidate()instead of importingrevalidatePathfromnext/cache
// pages/api/revalidate-path.ts
import type { NextApiRequest, NextApiResponse } from 'next'
import { parseBody } from 'next-sanity/webhook'
type WebhookPayload = {
path?: string
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ message: 'Method not allowed' })
}
try {
if (!process.env.SANITY_REVALIDATE_SECRET) {
return res.status(500).json({
message: 'Missing environment variable SANITY_REVALIDATE_SECRET',
})
}
const { isValidSignature, body } = await parseBody<WebhookPayload>(
req,
process.env.SANITY_REVALIDATE_SECRET,
true
)
if (!isValidSignature) {
return res.status(401).json({
message: 'Invalid signature',
isValidSignature,
body,
})
}
if (!body?.path) {
return res.status(400).json({ message: 'Bad Request', body })
}
await res.revalidate(body.path)
return res.status(200).json({
message: `Revalidated path: ${body.path}`,
body,
})
} catch (err: unknown) {
console.error(err)
const message = err instanceof Error ? err.message : 'Unknown error'
return res.status(500).json({ message })
}
}Pages Router doesn't support revalidateTag. For tag-based patterns, you'll need to map document types to paths and use res.revalidate(path) instead. This means tag-based revalidation in Pages Router effectively becomes path-based.
Related resources
- Caching and revalidation in Next.js: choosing a caching strategy and implementing the sanityFetch helper.
- GROQ-powered webhooks: configuring webhooks in the Sanity dashboard.
- GROQ Functions Reference: reference for select() and other functions used in webhook projections.
- next-sanity overview: what the toolkit includes and how to get started.