Webhooks explicados: payloads firmados y reintentos
Acabas de integrar una pasarela de pago. La documentación dice: "Enviaremos una petición POST a tu endpoint cuando un pago se complete con éxito". Fácil, ¿verdad? Creas una ruta sencilla que acepta JSON, actualiza tu base de datos y devuelve 200. Entonces ocurre: el pago de un cliente se completa, pero tu servidor está caído brevemente. El webhook nunca llega. O peor aún, llega dos veces y abonas el importe duplicado en la cuenta. Los webhooks parecen simples hasta que fallan.
Este artículo explica cómo funcionan realmente los webhooks, por qué los payloads firmados son esenciales para la seguridad y cómo construir una lógica de reintentos que mantenga tu integración confiable, incluso cuando las cosas van mal.
¿Qué es un webhook?
Un webhook es un callback HTTP: en lugar de que tu aplicación consulte una API en busca de cambios, el proveedor envía un POST HTTP a una URL que especificas cuando ocurre un evento. Es push en lugar de pull.
Ejemplos comunes incluyen confirmaciones de pago (Stripe, PayPal), notificaciones de compilación CI/CD (GitHub, GitLab) y plataformas de mensajería (Slack, Discord). El patrón es simple: registras una URL, el proveedor envía payloads JSON a esa URL y tu servidor los procesa.
Pero esta simplicidad esconde dos desafíos críticos: autenticidad (¿cómo sabes que la petición realmente proviene del proveedor?) y confiabilidad (¿qué ocurre si tu endpoint está caído o lento?).
Por qué importan los payloads firmados
Tu endpoint de webhook es una URL pública. Cualquiera puede enviarle una petición POST. Sin verificación, un atacante podría falsificar un evento de pago exitoso, desencadenar un envío o corromper tus datos.
Los payloads firmados resuelven esto incluyendo una firma criptográfica en las cabeceras de la petición. El proveedor calcula un HMAC (código de autenticación de mensajes basado en hash) usando un secreto compartido y el cuerpo de la petición, y luego lo envía junto con el payload. Tu servidor recalcula el HMAC y lo compara. Si coinciden, la petición es auténtica.
Cómo funciona la firma HMAC
- El proveedor y tú compartís una clave secreta (proporcionada durante la configuración).
- Para cada webhook, el proveedor concatena el timestamp y el cuerpo crudo de la petición, y luego calcula un hash HMAC-SHA256 usando el secreto.
- La firma se envía en una cabecera, a menudo algo como
X-SignatureoStripe-Signature. - Tu servidor recalcula el HMAC usando el mismo secreto y lo compara con la firma recibida.
- Si coinciden, procesas el evento; de lo contrario, lo rechazas con un 401.
Usa siempre el cuerpo crudo de la petición para el cálculo del HMAC. Si tu framework parsea el JSON antes de que verifiques, el cuerpo puede alterarse (espacios en blanco, orden de claves), provocando que la verificación falle.
Ejemplo: verificar una firma de webhook en Node.js
const crypto = require('crypto');
const express = require('express');
const app = express();
// Use raw body for signature verification
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; // Buffer
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).send('Invalid signature');
}
// Process the event
const event = JSON.parse(payload.toString());
// ... handle event
res.status(200).send('OK');
});
Observa el uso de timingSafeEqual para prevenir ataques de temporización. Nunca uses una comparación simple con === para las firmas.
Diseñar la lógica de reintentos
Incluso con firmas, los webhooks pueden fallar. Tu servidor podría estar caído, la red podría perder la petición o tu procesamiento podría agotar el tiempo de espera. Los proveedores normalmente reintentan las entregas fallidas, pero tú también deberías diseñar tu sistema para manejar los reintentos con elegancia.
¿Qué cuenta como fallo?
La mayoría de los proveedores considera cualquier respuesta que no sea 2xx como un fallo. Esto incluye redirecciones 3xx, errores de cliente 4xx y errores de servidor 5xx. Algunos también aplican un timeout (por ejemplo, 5 segundos). Si no respondes rápido, la entrega se marca como fallida y se reintenta.
Backoff exponencial con jitter
Los proveedores suelen reintentar con backoff exponencial: primer reintento tras 1 minuto, luego 5 minutos, 30 minutos, etc. Añadir jitter (variación aleatoria) previene estampidas si muchos webhooks fallan simultáneamente.
Si estás construyendo un emisor de webhooks, implementa el backoff así:
- Reintento 1: tras 10 segundos
- Reintento 2: tras 1 minuto
- Reintento 3: tras 5 minutos
- Reintento 4: tras 30 minutos
- Reintento 5: tras 2 horas
Tras un número máximo de intentos, mueve el evento a una cola de mensajes muertos para inspección manual.
Idempotencia: manejar entregas duplicadas
Los reintentos implican que tu endpoint puede recibir el mismo evento varias veces. Tu procesamiento debe ser idempotente: procesar el mismo evento dos veces debe tener el mismo efecto que procesarlo una vez.
Estrategias comunes:
- Deduplicación por ID de evento: Almacena los IDs de eventos procesados en una base de datos o caché. Antes de procesar, comprueba si el ID ya se ha visto.
- Upsert en lugar de insert: Usa operaciones de base de datos que manejen duplicados de forma natural (por ejemplo,
INSERT ... ON CONFLICT DO NOTHING). - Claves de idempotencia: Para operaciones como cobrar una tarjeta, pasa una clave única para que el proveedor de pagos ignore peticiones duplicadas.
Buenas prácticas para consumidores de webhooks
| Práctica | Por qué importa |
|---|---|
| Verificar firmas | Previene peticiones falsificadas |
| Responder rápido (en menos de 2 s) | Evita timeouts y reintentos innecesarios |
| Procesar de forma asíncrona | Desacopla la recepción del procesamiento pesado |
| Usar manejadores idempotentes | Seguro frente a entregas duplicadas |
| Registrar todos los eventos | Facilita la depuración y la auditoría |
| Monitorizar fallos | Detecta problemas antes de que escalen |
Considera también validar el timestamp en la firma para prevenir ataques de replay. Muchos proveedores incluyen un timestamp en el payload firmado; rechaza eventos con más de unos minutos de antigüedad.
Buenas prácticas para productores de webhooks
Si estás construyendo un servicio que envía webhooks, sigue estas pautas:
- Firma cada payload con HMAC-SHA256.
- Incluye un ID de evento único y un timestamp.
- Reintenta con backoff exponencial y jitter.
- Proporciona una forma para que los consumidores vean los registros de entrega y reenvíen eventos manualmente.
- Documenta claramente tu proceso de verificación de firmas.
FAQ
¿Qué es un payload de webhook firmado?
Un payload firmado incluye una firma criptográfica (normalmente HMAC) calculada a partir del cuerpo de la petición y un secreto compartido. El receptor verifica la firma para asegurar que la petición es auténtica y no ha sido modificada.
¿Cuántas veces se debe reintentar un webhook?
No hay un número universal, pero la mayoría de los proveedores reintentan de 3 a 5 veces durante varias horas. Después, el evento normalmente se marca como fallido y puede quedar disponible para reenvío manual. Tu sistema debe manejar duplicados de forma idempotente independientemente del número de reintentos.
¿Puedo usar webhooks sin HTTPS?
Técnicamente sí, pero siempre deberías usar HTTPS. Los webhooks a menudo contienen datos sensibles y, sin TLS, un atacante podría interceptar o manipular el payload. HTTPS también garantiza que estás enviando al servidor previsto.
Depurar webhooks con logs de Nginx
Cuando los webhooks fallan, el primer lugar donde mirar son los logs de tu servidor web. Si usas Nginx, los logs de acceso y errores registran cada petición, incluidos los códigos de estado y los tiempos de respuesta. Puedes detectar patrones rápidamente: 401 repetidos (fallo en la verificación de firma), 500 (errores de aplicación) o timeouts.
Para un análisis más rápido, prueba el Nginx Log Analyzer para parsear y resumir el tráfico de webhooks, filtrar por código de estado e identificar endpoints problemáticos.