Webhooks explicados: payloads firmados y reintentos
Acabas de integrar una pasarela de pago. La documentación dice: "Enviaremos un webhook a tu endpoint cuando un pago se complete con éxito". Configuras una ruta simple, la pruebas localmente y la despliegas. Entonces, en producción, notas eventos duplicados, notificaciones perdidas y misteriosos errores 500. Los webhooks parecen simples, hasta que dejan de serlo.
Esta guía explica cómo funcionan realmente los webhooks, por qué importan los payloads firmados y cómo construir una lógica de reintentos que mantenga tu sistema sincronizado incluso cuando las cosas salen mal.
¿Qué es un webhook?
Un webhook es un callback HTTP: un sistema envía una solicitud HTTP POST a una URL que tú proporcionas cuando ocurre un evento específico. A diferencia del polling, donde preguntas repetidamente "¿Ocurrió algo?", los webhooks te envían eventos casi en tiempo real.
Los usos comunes incluyen:
- Procesadores de pago que te notifican de cargos exitosos.
- Servicios de hosting de Git que activan pipelines de CI/CD al hacer push.
- Plataformas de comunicación que te alertan de nuevos mensajes.
El emisor (por ejemplo, Stripe, GitHub) es el proveedor de webhooks; tu servidor es el consumidor de webhooks.
Por qué son esenciales los payloads firmados
Tu endpoint de webhook es de acceso público. Cualquiera que conozca la URL puede enviar eventos falsos. Sin verificación, un atacante podría falsificar un evento de "pago exitoso" y podrías enviar productos gratis.
Los payloads firmados resuelven esto incluyendo una firma criptográfica en las cabeceras de la solicitud, generada con un secreto compartido conocido solo por ti y el proveedor. Cuando recibes un webhook, recalculas la firma y la comparas. Si coinciden, el payload es auténtico.
Cómo funcionan las firmas HMAC
La mayoría de los proveedores usan HMAC-SHA256. El proveedor calcula:
signature = HMAC-SHA256(secret, payload)y la envía en una cabecera como X-Signature. Tu servidor hace el mismo cálculo y verifica la igualdad. Como el secreto es secreto, solo el proveedor puede generar una firma válida.
Verificar una firma en la práctica
Aquí tienes un ejemplo mínimo en Node.js usando Express:
const crypto = require('crypto');
const express = require('express');
const app = express();
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.headers['x-signature'];
const secret = process.env.WEBHOOK_SECRET;
const payload = req.body; // raw Buffer
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
if (signature !== expected) {
return res.status(401).send('Invalid signature');
}
// Process the event
const event = JSON.parse(payload);
console.log('Received event:', event.type);
res.status(200).send('OK');
});
app.listen(3000);Importante: usa siempre el cuerpo de la solicitud en bruto para la verificación de la firma. Si primero parseas el JSON, la cadena serializada puede diferir (espacios en blanco, orden de claves) y la verificación de la firma fallará.
Errores comunes
- Usar el secreto incorrecto: algunos proveedores tienen secretos separados para modos de prueba y en vivo.
- Ataques de temporización: usa una función de comparación de tiempo constante (por ejemplo,
crypto.timingSafeEqual) para evitar filtrar información. - Ignorar las cabeceras de timestamp: muchos proveedores incluyen un timestamp para prevenir ataques de repetición. Rechaza solicitudes con más de unos minutos de antigüedad.
Reintentos: manejar fallos con elegancia
Las redes fallan, los servidores se reinician y los despliegues ocurren. Los proveedores de webhooks esperan que tu endpoint pueda estar temporalmente no disponible, así que reintentan las entregas fallidas. Pero los reintentos introducen nuevos desafíos: eventos duplicados, entrega desordenada y bucles infinitos si no tienes cuidado.
Cómo reintentan los proveedores
La mayoría de los proveedores usan backoff exponencial: esperan un tiempo corto tras el primer fallo y luego intervalos progresivamente más largos. Por ejemplo, Stripe reintenta hasta 3 días con retrasos crecientes. GitHub reintenta hasta 8 veces. El calendario exacto varía, pero el patrón es consistente.
Tu endpoint debe devolver un código de estado 2xx rápidamente para confirmar la recepción. Cualquier otro estado (4xx, 5xx) señala un fallo y activa un reintento.
Construir manejadores idempotentes
Como los reintentos pueden entregar el mismo evento varias veces, tu manejador debe ser idempotente: procesar el mismo evento dos veces debe tener el mismo efecto que procesarlo una vez.
Estrategias comunes:
- Seguimiento de ID de evento: almacena el ID único del evento (proporcionado en el payload o las cabeceras) en una base de datos. Antes de procesar, comprueba si ya se ha gestionado.
- Upserts: usa operaciones de base de datos que son naturalmente idempotentes (por ejemplo,
INSERT ... ON CONFLICT DO NOTHING). - Máquinas de estado: diseña tu lógica de negocio para tolerar transiciones repetidas (por ejemplo, "marcar pedido como pagado" solo si no está ya pagado).
Ejemplo usando un conjunto simple en memoria (a modo de ilustración; usa un almacén persistente en producción):
const processedEvents = new Set();
app.post('/webhook', (req, res) => {
const event = req.body;
if (processedEvents.has(event.id)) {
return res.status(200).send('Already processed');
}
// Process event...
processedEvents.add(event.id);
res.status(200).send('OK');
});Responder rápidamente
Los proveedores suelen tener un timeout (por ejemplo, 5 segundos). Si tu manejador hace un procesamiento pesado, confirma inmediatamente y procesa de forma asíncrona. Por ejemplo, envía el evento a una cola y devuelve 200.
Mejores prácticas para webhooks confiables
- Verifica siempre las firmas usando el cuerpo en bruto y una comparación de tiempo constante.
- Usa HTTPS para tu endpoint para prevenir escuchas y manipulaciones.
- Implementa idempotencia para manejar entregas duplicadas.
- Registra todo — payloads entrantes, firmas y tus respuestas — para depuración.
- Monitorea los fallos y configura alertas para errores repetidos.
- Prueba con eventos reales usando los paneles del proveedor o herramientas CLI.
- Maneja eventos desordenados incluyendo timestamps o números de secuencia en tu lógica.
Comparación: polling vs. webhooks
| Aspecto | Polling | Webhooks |
|---|---|---|
| Latencia | Depende del intervalo | Casi en tiempo real |
| Carga del servidor | Alta (solicitudes constantes) | Baja (solo en eventos) |
| Complejidad | Simple de implementar | Requiere endpoint, seguridad, reintentos |
| Confiabilidad | Tú controlas los reintentos | El proveedor controla los reintentos |
Preguntas frecuentes
¿Qué es una firma de webhook?
Una firma de webhook es un hash criptográfico (normalmente HMAC-SHA256) del payload, generado con un secreto compartido. Permite al receptor verificar que la solicitud provino del emisor esperado y que no fue manipulada.
¿Cómo manejo eventos de webhook duplicados?
Haz tu manejador idempotente rastreando los IDs de eventos o usando operaciones de base de datos que sean naturalmente idempotentes. Comprueba si un evento ya se ha procesado antes de aplicar cambios.
¿Qué debería devolver mi endpoint de webhook?
Devuelve un código de estado 2xx (por ejemplo, 200 OK) tan pronto como hayas recibido y validado el evento. Si necesitas hacer un procesamiento pesado, confirma inmediatamente y maneja el trabajo de forma asíncrona.
¿Listo para probar tus payloads de webhook? Usa nuestro JSON Formatter para inspeccionar y validar la estructura JSON de los eventos entrantes.