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

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

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

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 codigo 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, 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.

#nextjs#webhooks#api#stripe#typescript

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.