Webhooks en Next.js: Recibe y Procesa Eventos
Implementa webhooks en Next.js para recibir eventos de Stripe, GitHub, Clerk y otros servicios. Verificacion 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 critico es verificar que la request sea autentica y manejar los eventos correctamente.
La estructura basica
// 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 segun el tipo
// 4. Responder 200
return NextResponse.json({ received: true });
}Siempre responde 200 rapido. Si tardas mas de 5-10 segundos, el servicio asume que fallo y reintenta.
Webhook de Stripe (ejemplo completo)
Este es el patron mas comun. Si usas Stripe para pagos, revisa la guia 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 activarSuscripcion(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 verificacion de firma necesita el body crudoconstructEventverifica el HMAC. Si alguien manda un POST falso, falla aqui- Switch por tipo -- cada servicio manda distintos tipos de eventos
Verificacion de firmas (generico)
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 codigo 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, asegurate de que no esten expuestos en tu repo. Herramientas como datahogo escanean tu repositorio y detectan credenciales filtradas automaticamente.
Siguiente paso
Si tus webhooks necesitan proteccion adicional contra abuso, la guia de rate limiting en Next.js cubre como limitar requests por IP. Y para el setup completo de pagos con Stripe, revisa la guia de Stripe con Next.js.
Preguntas frecuentes
Que es un webhook?
Un webhook es una notificacion 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 automaticamente con un POST a una URL que tu defines.
Como 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 publico a tu localhost para que los servicios puedan enviar webhooks a tu maquina de desarrollo.
Que pasa si mi webhook endpoint falla?
La mayoria de servicios reintentan automaticamente. Stripe reintenta hasta 3 veces en 24 horas. Por eso es importante que tu endpoint sea idempotente: procesar el mismo evento dos veces no deberia 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 Dinamicos con React Hook Form y Zod
Crea formularios dinamicos con campos condicionales, arrays de campos y validacion type-safe usando React Hook Form y Zod en Next.js.