Webhooks explicados: payloads firmados y reintentos
Has construido una API y ahora necesitas notificar a otros servicios cuando ocurren eventos. Configuras un endpoint de webhook, pero pronto descubres que cualquiera puede enviar payloads falsos y que cuando tu servidor está caído, los eventos se pierden para siempre. Esta es una trampa común: los webhooks son simples en concepto pero requieren un diseño cuidadoso para seguridad y confiabilidad.
En esta guía, repasaremos los aspectos esenciales de los webhooks: cómo firmar payloads para prevenir manipulaciones, cómo verificar firmas en el extremo receptor y cómo implementar lógica de reintentos con backoff exponencial para manejar fallos. También cubriremos la idempotencia para evitar el procesamiento duplicado. Al final, tendrás un sistema de webhooks robusto en el que puedes confiar.
¿Qué son los webhooks?
Los webhooks son callbacks HTTP definidos por el usuario. Cuando ocurre un evento en un sistema de origen (por ejemplo, se procesa un pago), se envía una solicitud HTTP POST a una URL que especificas, con un payload que contiene los detalles del evento. Esto permite comunicación en tiempo real entre servicios sin sondeo.
Sin embargo, los webhooks introducen dos desafíos:
- Seguridad: ¿Cómo sabe el receptor que la solicitud proviene genuinamente del emisor?
- Confiabilidad: ¿Qué pasa si el receptor no está disponible temporalmente? ¿Cómo aseguramos que el evento se entregue eventualmente?
Protegiendo webhooks con payloads firmados
Para prevenir solicitudes falsificadas, el emisor debe firmar el payload usando un secreto compartido. El receptor puede entonces verificar la firma para autenticar al emisor. El método más común es HMAC (Hash-based Message Authentication Code) con SHA-256.
Cómo funciona la firma HMAC
- El emisor y el receptor comparten una clave secreta (idealmente una cadena larga y aleatoria).
- Para cada webhook, el emisor calcula un HMAC del cuerpo de la solicitud usando el secreto y lo incluye en un encabezado (por ejemplo,
X-Signature). - El receptor calcula el mismo HMAC sobre el cuerpo recibido y lo compara con el valor del encabezado. Si coinciden, la solicitud es auténtica.
Es crucial firmar el cuerpo crudo de la solicitud, no una versión parseada, para evitar discrepancias debido a diferencias en la serialización JSON.
Ejemplo: Firmando un payload en Node.js
const crypto = require('crypto');
function signPayload(payload, secret) {
return crypto.createHmac('sha256', secret).update(payload).digest('hex');
}
// En el lado del emisor:
const payload = JSON.stringify({ event: 'payment.success', amount: 100 });
const signature = signPayload(payload, 'your-secret-key');
// Incluir firma en el encabezado: X-Signature: signature
Verificando la firma en el receptor
En el extremo receptor, debes verificar la firma antes de procesar el payload. Usa una comparación de tiempo constante para prevenir ataques de temporización.
const crypto = require('crypto');
function verifySignature(payload, signature, secret) {
const expected = crypto.createHmac('sha256', secret).update(payload).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
// En tu endpoint de webhook:
app.post('/webhook', (req, res) => {
const signature = req.headers['x-signature'];
const payload = req.rawBody; // cuerpo crudo como string
if (!verifySignature(payload, signature, 'your-secret-key')) {
return res.status(401).send('Invalid signature');
}
// Procesar el evento
res.status(200).send('OK');
});
Nota: Siempre usa HTTPS para prevenir ataques de man-in-the-middle. Además, considera incluir una marca de tiempo en el payload firmado para prevenir ataques de repetición.
Implementando reintentos con backoff exponencial
Incluso con un receptor confiable, problemas de red o cortes temporales pueden causar que la entrega del webhook falle. Un emisor robusto debe reintentar las entregas fallidas. Sin embargo, los reintentos ingenuos pueden abrumar al receptor o causar procesamiento duplicado. La solución: backoff exponencial con jitter y un límite máximo de reintentos.
Estrategia de reintentos
- Reintentar en fallo: Reintentar cuando el receptor devuelve un código de estado no 2xx o se agota el tiempo de espera.
- Backoff exponencial: Aumentar el retraso entre reintentos exponencialmente (por ejemplo, 1s, 2s, 4s, 8s...).
- Jitter: Agregar variación aleatoria al retraso para evitar problemas de estampida.
- Intentos máximos: Establecer un límite (por ejemplo, 5 reintentos) y luego marcar el webhook como fallido (quizás alertando a un administrador).
Ejemplo: Lógica de reintentos en Node.js
async function deliverWebhook(url, payload, secret, maxRetries = 5) {
const signature = signPayload(payload, secret);
let attempt = 0;
while (attempt < maxRetries) {
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Signature': signature },
body: payload,
});
if (response.ok) return;
throw new Error(`HTTP ${response.status}`);
} catch (error) {
attempt++;
if (attempt >= maxRetries) throw error;
const delay = Math.min(1000 * 2 ** attempt + Math.random() * 1000, 30000);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
Esta función reintenta hasta 5 veces con backoff exponencial y jitter, limitando el retraso a 30 segundos. Ajusta los parámetros según tus necesidades.
Asegurando la idempotencia
Los reintentos pueden causar entregas duplicadas, por lo que tu manejador de webhooks debe ser idempotente. Incluye un ID de evento único en el payload y haz que el receptor rastree los IDs procesados para evitar el doble procesamiento.
// En el receptor:
const processedEvents = new Set();
app.post('/webhook', (req, res) => {
const { id } = req.body;
if (processedEvents.has(id)) {
return res.status(200).send('Already processed');
}
// Procesar evento
processedEvents.add(id);
res.status(200).send('OK');
});
En producción, usa un almacén persistente (por ejemplo, Redis o una base de datos) para rastrear los IDs procesados entre reinicios.
Comparación: Webhooks firmados vs. no firmados
| Aspecto | No firmado | Firmado (HMAC) |
|---|---|---|
| Seguridad | Cualquiera puede falsificar solicitudes | Solo el poseedor del secreto puede crear una firma válida |
| Integridad | El payload puede ser manipulado | La manipulación invalida la firma |
| Complejidad | Baja | Moderada (gestión de claves, verificación) |
| Caso de uso | Redes internas y confiables | Cualquier webhook público |
Preguntas frecuentes
¿Qué es una firma de webhook?
Una firma de webhook es un hash criptográfico (generalmente HMAC) del payload, creado con un secreto compartido. Permite al receptor verificar que la solicitud provino del emisor esperado y que el payload no fue alterado.
¿Cuántas veces debo reintentar un webhook fallido?
No hay un número fijo, pero la práctica común es de 3 a 5 reintentos con backoff exponencial. Después de eso, marca la entrega como fallida y alerta a tu equipo. Considera la criticidad del evento: las notificaciones de pago podrían requerir más reintentos que las actualizaciones no críticas.
¿Puedo usar webhooks sin HTTPS?
Nunca debes usar webhooks sobre HTTP simple en producción. HTTPS cifra el payload en tránsito, previniendo escuchas y ataques de man-in-the-middle. Incluso con firmas, HTTP expone datos sensibles.
¿Listo para probar tus payloads de webhook? Usa nuestro Formateador JSON para imprimir y validar payloads JSON antes de enviarlos o depurarlos.