Webhooks en Sanity.io: El Framework de 3 Capas que el 90% Implementa Mal
Aprende a configurar webhooks de Sanity.io para automatizar builds, sincronizar datos y gestionar flujos de contenido con un framework de 3 capas.
El 90% de Implementaciones de Sanity Pierden Automatización por Configurar Webhooks Incorrectamente
Vuestra estrategia de contenido depende de webhooks. Pero la mayoría configuráis los vuestros como si fueran simples botones de notificación.
Un webhook no es un "aviso de que pasó algo". Es un evento estructurado que vuestra infraestructura puede consumir para orquestar reacciones complejas: rebuilds, sincronizaciones, notificaciones, invalidaciones de caché.
La diferencia entre una implementación que funciona y una que escala no está en el CMS. Está en cómo diseñáis el flujo de webhook.
*El problema real no es recibir el evento. Es estructurar la reacción para que sea idempotente, escalable, y tolerante a fallos.*
El Problema: webhooks que disparan builds sin estrategia detrás
La mayoría configuráis un webhook en Sanity con la URL de Vercel o Netlify. Cuando cambia un documento, el CMS envía una petición POST. El host recibe el payload y dispara un deploy.
Funciona. Al principio.
El problema aparece cuando escaláis:
50 artículos modificados en batch = 50 builds disparadas
El webhook se ejecuta antes de que GROQ termine de reindexar
No hay retry logic si el deploy falla
No diferenciáis entre contenido y configuración
No validáis que el payload viene realmente de Sanity
Cada uno de estos puntos genera fallos en cadena. Y la mayoría no los descubrís hasta que un editor modifica 200 productos y vuestra pipeline de deploy colapsa durante 3 horas.
La implementación por defecto no es robusta. Es naïve.
El Payload de Sanity: Lo Que Realmente Estáis Recibiendo
Cuando configuráis un webhook en Sanity, el cuerpo del evento contiene datos estructurados que la mayoría ignoráis:
```json
{
"type": "create",
"id": "abc123-def456",
"_rev": "v123",
"_type": "product",
"slug": {
"current": "zapatillas-runner-pro"
},
"timestamp": "2026-05-02T08:30:00Z"
}
```
Este payload os dice exactamente:
Qué tipo de operación ocurrió (create, update, delete)
Qué documento se afectó (id + _type)
Qué revisión del schema cambió (_rev)
El identificador legible (slug.current)
Con esta información, vuestro handler puede decidir qué hacer. No todo necesita un deploy completo.
Por Qué Esto Cambia Todo
Vuestra pipeline puede filtrar eventos:
Cambios en `settings` o `navigation` → rebuild completo (afectan estructura)
Cambios en `product` → invalidar caché + reindexar búsqueda
Cambios en `author` → solo propagar a contenido relacionado
Sin esta distinción, estáis tratando una actualización de copyright como si fuera un nuevo landing page.
El Framework de 3 Capas para Webhooks Robustos en Sanity
Este framework os da la arquitectura para construir webhooks que escalan, se recuperan de errores, y diferencian entre tipos de contenido.
Capa 1: Validación y Autenticación
Antes de procesar nada, verificad la firma del webhook:
```javascript
// sanity-webhook-handler.js
import crypto from 'crypto';
export async function verifySanityWebhook(req) {
const signature = req.headers['sanity-webhook-signature'];
const secret = process.env.SANITY_WEBHOOK_SECRET;
if (!signature || !secret) {
throw new Error('Missing webhook authentication');
}
const rawBody = req.body; // Ya parseado
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(rawBody))
.digest('hex');
const isValid = crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
if (!isValid) {
throw new Error('Invalid webhook signature');
}
return true;
}
```
Esta capa previene ataques de replay y asegura que el evento viene de Sanity, no de un agente externo.
Capa 2: Clasificación por Tipo de Contenido
Distinguid entre operaciones que requieren rebuild y las que no:
```javascript
// classify-operation.js
export function classifyWebhookEvent(payload) {
const { _type, type } = payload;
// Operaciones destructivas siempre requieren atención
if (type === 'delete') {
return {
action: 'cleanup',
rebuild: false,
priority: 'high'
};
}
// Contenido estructural requiere rebuild completo
const structuralTypes = ['settings', 'navigation', 'siteConfig', 'menu'];
if (structuralTypes.includes(_type)) {
return {
action: 'fullRebuild',
rebuild: true,
priority: 'high'
};
}
// Productos: invalidar caché + reindexar
if (_type === 'product') {
return {
action: 'invalidateAndReindex',
rebuild: false,
priority: 'medium'
};
}
// Blog posts: propagar a related content
if (_type === 'post') {
return {
action: 'propagateRelated',
rebuild: false,
priority: 'low'
};
}
// Default: solo logging
return {
action: 'log',
rebuild: false,
priority: 'low'
};
}
```
Con esta clasificación, vuestra Edge Function o serverless handler ejecuta la lógica correcta según el evento.
Capa 3: Cola de Procesamiento con Retry
Nunca ejecutéis lógica compleja en el handler del webhook directamente. Usad una cola:
```javascript
// enqueue-operation.js
import { createClient } from '@sanity/client';
import { v4 as uuidv4 } from 'uuid';
export async function enqueueWebhookOperation(payload, classification) {
const client = createClient({
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET,
token: process.env.SANITY_API_TOKEN,
apiVersion: '2024-01-01'
});
const operation = {
id: uuidv4(),
payload: payload,
classification: classification,
status: 'pending',
attempts: 0,
createdAt: new Date().toISOString()
};
// Almacenar en Sanity (o Redis, Vercel KV, etc.)
await client.create({
_type: 'webhookOperation',
...operation
});
return operation.id;
}
```
Esta aproximación os da:
Idempotencia: si el webhook se dispara twice, la operación ya existe
Retry logic: si falla, el handler puede reintentarlo con backoff exponencial
Observabilidad: tenéis un registro de todas las operaciones y su estado
El Error Más Común: Webhooks sin Signature Verification
La mayoría ignoran la capa de autenticación. Configuran el webhook, apuntan a su endpoint, y funcionan.
Hasta que alguien descubre la URL y empieza a enviar eventos falsos.
Sin signature verification, un agente externo puede:
Disparar builds falsos que agotan vuestros minutos de deployment
Invalidar caché continuamente
Sobrecargar vuestras APIs downstream
Añadir la verificación de Sanity toma 20 líneas de código. No lo ignoréis.
❌ Sin verificación:
```javascript
// MAL: procesáis cualquier request
export default async function handler(req, res) {
const event = req.body;
await triggerDeploy(); // ¿Quién ha llamado esto?
res.status(200).end();
}
```
✅ Con verificación:
```javascript
// BIEN: solo procesáis eventos legítimos de Sanity
export default async function handler(req, res) {
await verifySanityWebhook(req);
const event = req.body;
const classification = classifyWebhookEvent(event);
await enqueueWebhookOperation(event, classification);
res.status(200).end();
}
```
Desplegando el Handler: Vercel Edge Function o Serverless?
Para webhooks de Sanity, vuestra mejor opción es una Edge Function en Vercel por tres razones:
1. Latencia baja: respuesta inmediata al webhook de Sanity
2. Distribución global: el handler se ejecuta cerca del servidor de Sanity
3. Escalado automático: cada evento ejecuta una instancia fría pero rápida
El código del framework que os he mostrado funciona tanto en Edge Functions como en Serverless Functions estándar. La única diferencia es el runtime:
```javascript
// edge-handler.ts (para Vercel Edge Functions)
import { NextResponse } from 'next/server';
import { verifySanityWebhook } from '../lib/webhook-auth';
import { classifyWebhookEvent } from '../lib/classify';
import { enqueueWebhookOperation } from '../lib/enqueue';
export const config = {
runtime: 'edge'
};
export async function POST(req: Request) {
try {
const body = await req.json();
await verifySanityWebhook({ body, headers: Object.fromEntries(req.headers) });
const classification = classifyWebhookEvent(body);
await enqueueWebhookOperation(body, classification);
return new NextResponse('OK', { status: 200 });
} catch (error) {
console.error('Webhook processing failed:', error);
return new NextResponse('Error', { status: 500 });
}
}
```
Configurar el Webhook en Sanity Dashboard
Ahora que vuestro handler está listo, configurad el webhook en Sanity:
1. Id a Sanity Dashboard → API → Webhooks
2. Click en Add Webhook
3. Configurad:
URL: vuesta Edge Function URL (ej: `https://vuesto-proyecto.vercel.app/api/webhook`)
Trigger on: seleccionad los tipos de documento que os interesan
Filter: opción avanzada para filtrar por condiciones (ej: solo contenido publicado)
Projection: qué campos incluir en el payload
Signature: generad un secreto y guardadlo en `.env` como `SANITY_WEBHOOK_SECRET`
La opción de filter es potente. Podéis filtrar por estado de publicación:
```groq
_type == "post" && published == true
```
Así solo recibís eventos de contenido que realmente necesita procesamiento.
Monitoring: Saber Cuándo Algo Falla
Un webhook sin monitoring es un servicio ciego. Añadid logging estructurado:
```javascript
export async function logWebhookEvent(operation, status, error = null) {
const client = createClient({
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET,
token: process.env.SANITY_API_TOKEN,
apiVersion: '2024-01-01',
useCdn: false
});
await client.create({
_type: 'webhookLog',
operationId: operation.id,
classification: operation.classification,
status: status,
error: error?.message || null,
timestamp: new Date().toISOString()
});
}
```
Con estos logs en Sanity, podeis consultar qué webhooks fallaron y por qué.
Qué Hacer con los Eventos Adelantados
El problema más sutil: Sanity envía el webhook cuando el documento se guarda, pero antes de que GROQ indexes los cambios.
Si vuestra web consume el API de Sanity desde la frontend, el contenido nuevo puede no aparecer durante 30-60 segundos.
*La solución no es esperar más. Es invalidar la caché de vuesta CDN después del rebuild.*
En Vercel, disparad una invalidación de caché programática:
```javascript
import { deploy } from '@vercel/sdk';
export async function invalidateCache(projectId, paths) {
const client = new deploy.Client({
token: process.env.VERCEL_API_TOKEN
});
await client.deployment.invalidateCache({
projectId: projectId,
paths: paths
});
}
```
Añadid esto a la Capa 3 después de que el deploy complete.
Resumen: El Framework Completo
El Patrón de las 3 Capas para Webhooks Robustos:
1. Validación: verificad la firma con HMAC-SHA256 antes de procesar
2. Clasificación: diferenciad entre tipos de contenido y decidid la acción correspondiente
3. Cola + Retry: encolad operaciones en Sanity o Redis para procesamiento asíncrono con reintentos
Con este framework, vuestros webhooks de Sanity dejan de ser notificaciones y se convierten en la columna vertebral de vuestra automatización de contenido.
No esperéis a que 200 documentos cambien y vuestra pipeline colapse. Implementad la clasificación por tipo desde el día uno.
Lee el artículo completo en brianmenagomez.com
Más sobre mis servicios en brianmenagomez.com
Herramientas: Conversor IAE CNAE · Gestorias cerca de ti · Calculadora IRPF

