Webhooks en Next.js: Recibe y Procesa Eventos
Implementa webhooks en Next.js para recibir eventos de Stripe, GitHub, Clerk y otros servicios. Verificación de firmas, tipado y manejo de errores.
Webhooks en Next.js: Recibe y Procesa Eventos
Webhooks son la forma en que servicios externos te avisan cuando algo pasa: un pago se completo en Stripe, alguien hizo push en GitHub, un usuario se registro en Clerk. En vez de hacer polling cada 5 segundos, el servicio te manda un POST con los datos del evento.
En Next.js, recibir webhooks es crear una API route que procesa ese POST. Lo crítico es verificar que la request sea autentica y manejar los eventos correctamente.
La estructura básica
// app/api/webhooks/stripe/route.ts
import { NextResponse } from "next/server";
export async function POST(request: Request) {
const body = await request.text();
const signature = request.headers.get("stripe-signature");
// 1. Verificar firma
// 2. Parsear el evento
// 3. Procesar según el tipo
// 4. Responder 200
return NextResponse.json({ received: true });
}Siempre responde 200 rápido. Si tardas más de 5-10 segundos, el servicio asume que fallo y reintenta.
Webhook de Stripe (ejemplo completo)
Este es el patron más común. Si usas Stripe para pagos, revisa la guía completa de Stripe con Next.js.
// app/api/webhooks/stripe/route.ts
import { NextResponse } from "next/server";
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
export async function POST(request: Request) {
const body = await request.text();
const signature = request.headers.get("stripe-signature")!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
} catch (err) {
console.error("Firma invalida:", err);
return NextResponse.json({ error: "Firma invalida" }, { status: 400 });
}
switch (event.type) {
case "checkout.session.completed": {
const session = event.data.object as Stripe.Checkout.Session;
await activarSuscripción(session.customer as string);
break;
}
case "invoice.payment_failed": {
const invoice = event.data.object as Stripe.Invoice;
await notificarPagoFallido(invoice.customer as string);
break;
}
default:
console.log(`Evento no manejado: ${event.type}`);
}
return NextResponse.json({ received: true });
}Puntos clave:
request.text()en vez derequest.json()-- la verificación de firma necesita el body crudoconstructEventverifica el HMAC. Si alguien manda un POST falso, falla aquí- Switch por tipo -- cada servicio manda distintos tipos de eventos
Verificación de firmas (genérico)
Si el servicio no tiene SDK (como GitHub o un servicio custom), verificas la firma manualmente:
import { createHmac, timingSafeEqual } from "crypto";
function verificarFirma(
payload: string,
signature: string,
secret: string
): boolean {
const expected = createHmac("sha256", secret)
.update(payload)
.digest("hex");
const sig = signature.replace("sha256=", "");
return timingSafeEqual(
Buffer.from(sig, "hex"),
Buffer.from(expected, "hex")
);
}timingSafeEqual previene timing attacks. Nunca compares firmas con ===.
Webhook de GitHub
// app/api/webhooks/github/route.ts
import { NextResponse } from "next/server";
const secret = process.env.GITHUB_WEBHOOK_SECRET!;
export async function POST(request: Request) {
const body = await request.text();
const signature = request.headers.get("x-hub-signature-256")!;
if (!verificarFirma(body, signature, secret)) {
return NextResponse.json({ error: "No autorizado" }, { status: 401 });
}
const event = request.headers.get("x-github-event");
const payload = JSON.parse(body);
switch (event) {
case "push":
console.log(`Push a ${payload.ref} por ${payload.pusher.name}`);
break;
case "pull_request":
console.log(`PR ${payload.action}: ${payload.pull_request.title}`);
break;
}
return NextResponse.json({ ok: true });
}Hacer tu endpoint idempotente
Los servicios reintentan webhooks si no reciben 200. Tu código debe manejar duplicados:
case "checkout.session.completed": {
const session = event.data.object as Stripe.Checkout.Session;
// Verificar si ya procesamos este evento
const existing = await db.payment.findUnique({
where: { stripeSessionId: session.id },
});
if (existing) break; // Ya lo procesamos, ignorar
await db.payment.create({
data: {
stripeSessionId: session.id,
userId: session.metadata?.userId,
amount: session.amount_total,
},
});
break;
}Testing local con Stripe CLI
# Instalar
brew install stripe/stripe-cli/stripe
# Login
stripe login
# Redirigir eventos a tu localhost
stripe listen --forward-to localhost:3000/api/webhooks/stripeLa CLI te da un webhook secret temporal para desarrollo. Usalo en tu .env.local.
Cuando manejas webhook secrets y API keys, Asegúrate de que no esten expuestos en tu repo. Herramientas como datahogo escanean tu repositorio y detectan credenciales filtradas automáticamente.
Siguiente paso
Si tus webhooks necesitan protección adicional contra abuso, la guía de rate limiting en Next.js cubre como limitar requests por IP. Y para el setup completo de pagos con Stripe, revisa la guía de Stripe con Next.js.
Preguntas frecuentes
¿Qué es un webhook?
Un webhook es una notificación HTTP que un servicio externo envia a tu app cuando algo pasa. En vez de que tu app pregunte cada 5 segundos si hay algo nuevo (polling), el servicio te avisa automáticamente con un POST a una URL que tu defines.
¿Cómo verifico que un webhook es autentico?
Con la firma (signature). Los servicios serios como Stripe envian un header con un hash HMAC del payload. Tu verificas ese hash con tu webhook secret. Si no coincide, rechazas la request.
¿Puedo recibir webhooks en desarrollo local?
Si, con herramientas como ngrok o la CLI de Stripe. Crean un tunel público a tu localhost para que los servicios puedan enviar webhooks a tu maquina de desarrollo.
¿Qué pasa si mi webhook endpoint falla?
La mayoria de servicios reintentan automáticamente. Stripe reintenta hasta 3 veces en 24 horas. Por eso es importante que tu endpoint sea idempotente: procesar el mismo evento dos veces no debería causar problemas.
Articulos relacionados
Zod Avanzado: Discriminated Unions, Transforms y Pipes
Patrones avanzados de Zod: discriminated unions, transforms, pipes, preprocess, y como validar datos complejos en TypeScript con schemas reutilizables.
tRPC + Next.js: APIs Type-Safe sin REST
Implementa tRPC en Next.js para APIs 100% type-safe. Sin schemas de API, sin fetch manual, sin types duplicados. End-to-end type safety con TypeScript.
Formularios dinámicos con React Hook Form y Zod
Crea formularios dinámicos con campos condicionales, arrays de campos y validación type-safe usando React Hook Form y Zod en Next.js.