tutoriales·7 min de lectura

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

typescript
// 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.

typescript
// 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 de request.json() -- la verificación de firma necesita el body crudo
  • constructEvent verifica 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:

typescript
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

typescript
// 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:

typescript
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

bash
# Instalar
brew install stripe/stripe-cli/stripe
 
# Login
stripe login
 
# Redirigir eventos a tu localhost
stripe listen --forward-to localhost:3000/api/webhooks/stripe

La 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.

#nextjs#webhooks#api#stripe#typescript

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.