Verificación de Webhooks
Los webhooks envían eventos a tu API de Node. Verifica las firmas HMAC, bloquea las repeticiones y procesa idempotentemente porque los proveedores reintentan en caso de tiempo de espera.
Busca en todas las páginas de la documentación
Los webhooks envían eventos a tu API de Node. Verifica las firmas HMAC, bloquea las repeticiones y procesa idempotentemente porque los proveedores reintentan en caso de tiempo de espera.
Tarjeta de receta de referencia rápida - lista para copiar y pegar.
// Express 5 - cuerpo sin procesar para Stripe
import express from "express";
import Stripe from "stripe";
const app = express();
app.post(
"/webhooks/stripe",
express.raw({ type: "application/json" }),
(req, res) => {
const sig = req.headers["stripe-signature"] as string;
try {
const event = Stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
);
// despachar event.type
res.json({ received: true });
} catch {
res.status(400).send("invalid signature");
}
}
);Cuándo usarlo:
// src/webhooks/stripe.ts
import type { Request, Response } from "express";
import Stripe from "stripe";
import { getStripe } from "../integrations/stripe-client";
import { paymentsQueue } from "../queues/payments";
const stripe = getStripe();
export async function stripeWebhookHandler(req: Request, res: Response) {
const sig = req.headers["stripe-signature"];
if (!sig || !Buffer.isBuffer(req.body)) {
return res.status(400).send("bad request");
}
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
console.warn({ msg: "stripe_sig_fail", err: (err as Error).message });
return res.status(400).send("invalid signature");
}
if (await isEventProcessed(event.id)) {
return res.json({ received: true, duplicate: true });
}
await markEventReceived(event.id);
if (event.type === "payment_intent.succeeded") {
await paymentsQueue.add("fulfill", { paymentIntentId: (event.data.object as Stripe.PaymentIntent).id });
}
res.json({ received: true });
}
// Verificación HMAC genérica (estilo Twilio)
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyHmacSha256(rawBody: Buffer, signature: string, secret: string): boolean {
const expected = createHmac("sha256", secret).update(rawBody).digest("base64");
const a = Buffer.from(expected);
const b = Buffer.from(signature);
return a.length === b.length && timingSafeEqual(a, b);
}Lo que esto demuestra:
event.id antes de los efectos secundarios// Monta las rutas de webhook ANTES de express.json() globalmente
const webhookRouter = express.Router();
webhookRouter.post("/stripe", express.raw({ type: "application/json" }), stripeWebhookHandler);
app.use("/webhooks", webhookRouter);
app.use(express.json());addContentTypeParser para el búfer sin procesar en la ruta del webhook// Rechaza eventos con más de 5 minutos de antigüedad (analizadores personalizados)
const ts = Number(req.header("X-Webhook-Timestamp"));
if (Math.abs(Date.now() / 1000 - ts) > 300) {
return res.status(400).send("stale");
}constructEventimport twilio from "twilio";
const valid = twilio.validateRequest(
process.env.TWILIO_AUTH_TOKEN!,
req.headers["x-twilio-signature"] as string,
fullUrl,
req.body
);| Código | Comportamiento del proveedor |
|---|---|
| 2xx | Detener reintento |
| 4xx (firma incorrecta) | Detener reintento (corregir configuración) |
| 5xx / tiempo de espera | Reintentar con retroceso exponencial |
timingSafeEqual.| Alternativa | Usar Cuándo | No Usar Cuándo |
|---|---|---|
| Sondeo de la API del proveedor | Webhook no disponible | Se necesita tiempo real, los límites de tasa son costosos |
| Fan-in de SNS/SQS | Ingestión de eventos de AWS | Stripe simple de un solo endpoint |
| Relevo Svix | UX de verificación de múltiples proveedores | Integración única solo con Stripe |
| Webhooks de socio mTLS | El contrato B2B lo requiere | El proveedor SaaS usa HMAC |
HMAC se calcula sobre bytes exactos. JSON.stringify después del análisis cambia los espacios en blanco.
Secreto de firma separado por URL de endpoint en el panel del proveedor.
Stripe CLI stripe listen --forward-to localhost:3000/webhooks/stripe.
Afirma 400 y cero efectos secundarios en la base de datos cuando se manipula el encabezado.
Generalmente ninguna. Diseña manejadores idempotentes; usa campos de versión si el orden importa.
X-Hub-Signature-256 HMAC SHA256 - el mismo patrón de cuerpo sin procesar.
app.useBodyParser("raw", { verify: ...}) en la ruta del webhook o middleware dedicado.
Montaje de ruta separado sin JWT - la firma es la autenticación.
Persiste la carga útil fallida para una repetición manual después de la corrección - no confíes solo en la ventana de reintento del proveedor.
Defensa en profundidad opcional. La verificación HMAC es la principal; las IP del proveedor cambian.
Versiones de la pila: Esta página fue escrita para Node.js 24.18.0 (LTS Activo), npm 10+, TypeScript 5.6+, Express 5, Fastify 5 y NestJS 11.
Revisado por Chris St. John·Última actualización: 16 jul 2026