Webhooks explicados: payloads firmados y reintentos
Acabas de integrar una pasarela de pago. La documentación dice: "Enviaremos un webhook cuando un pago se complete". Genial. Pero luego te preguntas: ¿cómo sé que la petición realmente vino de ellos? ¿Y si mi servidor está caído cuando la envían? ¿Y si envían el mismo evento dos veces?
Los webhooks son potentes, pero tienen trampas. Esta guía repasa la mecánica, la seguridad y los patrones de fiabilidad que necesitas para gestionarlos correctamente.
¿Qué es un webhook?
Un webhook es una petición HTTP POST que un sistema envía a otro cuando ocurre un evento específico. En lugar de que tu app haga polling a una API cada pocos segundos ("¿Hay pedidos nuevos? ¿Hay pedidos nuevos?"), el proveedor te empuja los datos en cuanto ocurre algo.
Flujo típico:
- Registras una URL (p. ej.,
https://yourapp.com/webhooks/payments) con el proveedor. - Ocurre un evento (pago completado, usuario registrado, archivo subido).
- El proveedor envía una petición POST a tu URL con un cuerpo JSON que describe el evento.
- Tu servidor procesa el evento y responde con
2xxpara confirmar la recepción.
Si no respondes rápido (normalmente en 5–30 segundos), el proveedor considera que la entrega falló y puede reintentar.
Por qué debes verificar los payloads firmados
Tu endpoint de webhook es de acceso público. Cualquiera que descubra la URL puede enviar eventos falsos. Sin verificación, un atacante podría falsificar un evento de "pago completado" y obtener productos gratis.
La solución: payloads firmados. El proveedor calcula una firma criptográfica sobre el cuerpo de la petición usando un secreto compartido (conocido solo por ti y por él) y la incluye en una cabecera. Tú recalculas la firma en tu lado y comparas.
Cómo funciona la firma HMAC
La mayoría de los proveedores usan HMAC-SHA256. Los pasos:
- El proveedor concatena el cuerpo crudo de la petición con un timestamp (para prevenir ataques de replay).
- El proveedor calcula
HMAC-SHA256(secret, timestamp + '.' + body). - El proveedor envía la firma en una cabecera, p. ej.,
X-Signature: t=1234567890,v1=abc123.... - Tú extraes el timestamp y la firma, recalculas el HMAC usando tu copia del secreto y comparas usando una función de comparación en tiempo constante.
Aquí tienes un ejemplo mínimo en Node.js:
const crypto = require('crypto');
function verifySignature(req, secret) {
const signatureHeader = req.headers['x-signature'];
const [tPart, vPart] = signatureHeader.split(',');
const timestamp = tPart.split('=')[1];
const receivedSig = vPart.split('=')[1];
const payload = timestamp + '.' + JSON.stringify(req.body);
const expectedSig = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
// Comparación en tiempo constante para prevenir ataques de timing
return crypto.timingSafeEqual(
Buffer.from(receivedSig, 'hex'),
Buffer.from(expectedSig, 'hex')
);
}
Importante: usa siempre el cuerpo crudo de la petición para verificar la firma. Si tu framework parsea el JSON y lo vuelve a serializar, la secuencia de bytes puede diferir y la verificación fallará. Captura el cuerpo crudo antes de parsearlo.
Validación del timestamp
Comprueba que el timestamp sea reciente (p. ej., dentro de 5 minutos). Esto previene ataques de replay en los que un atacante captura una petición válida y la reenvía más tarde.
Lógica de reintentos: gestionar fallos con elegancia
Tu servidor podría estar caído, sobrecargado o sufrir un corte de red. Los proveedores gestionan esto reintentando las entregas fallidas. Pero debes diseñar tu endpoint para manejar los reintentos de forma segura.
Backoff exponencial con jitter
Los proveedores suelen reintentar con retardos crecientes: 1 minuto, 5 minutos, 30 minutos, 2 horas, etc. Esto se llama backoff exponencial. Algunos añaden jitter aleatorio para evitar el problema del thundering herd.
Desde tu lado, no controlas el calendario de reintentos del proveedor, pero deberías:
- Responder con
2xxen cuanto hayas almacenado el evento de forma segura (aunque el procesamiento sea asíncrono). - Devolver
4xxpara fallos permanentes (p. ej., payload malformado) para que el proveedor deje de reintentar. - Devolver
5xxpara fallos transitorios (p. ej., base de datos caída) para activar los reintentos.
Idempotencia: la clave para reintentos seguros
Los reintentos implican que podrías 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.
Enfoque común: incluir un ID de evento único en el payload. Antes de procesar, comprueba si ya has gestionado ese ID. Si es así, devuelve 200 y omite el procesamiento.
async function handleWebhook(event) {
const existing = await db.events.findOne({ id: event.id });
if (existing) {
return { status: 200, body: 'Already processed' };
}
await db.events.insert({ id: event.id, processedAt: new Date() });
await processEvent(event);
return { status: 200, body: 'OK' };
}
Este patrón asegura que, aunque el proveedor envíe el evento tres veces, tú solo actúas una vez.
Comparación: polling vs webhooks
| Aspecto | Polling | Webhooks |
|---|---|---|
| Latencia | Depende del intervalo (p. ej., 30 s) | Casi en tiempo real |
| Carga del servidor | Constante, incluso sin eventos | Solo cuando ocurren eventos |
| Complejidad | Fácil de implementar | Requiere endpoint, verificación y reintentos |
| Fiabilidad | Tú controlas los reintentos | Depende de la política de reintentos del proveedor |
Los webhooks ganan para actualizaciones en tiempo real, pero el polling puede ser un respaldo para eventos críticos si te preocupa perder entregas.
Checklist de buenas prácticas
- Verifica siempre las firmas. Nunca confíes en un webhook sin verificar.
- Usa HTTPS. Los payloads de webhook suelen contener datos sensibles.
- Responde rápido. Confirma la recepción cuanto antes y luego procesa de forma asíncrona si es necesario.
- Implementa idempotencia. Almacena los IDs de evento para evitar procesamiento duplicado.
- Registra todo. Mantén un historial de eventos recibidos para depuración y auditoría.
- Monitoriza los fallos. Configura alertas para respuestas 5xx repetidas o fallos de verificación de firma.
- Prueba con payloads reales. Usa el modo de prueba del proveedor o envía eventos de ejemplo a tu endpoint.
Depurar problemas con webhooks
Cuando los webhooks fallan, el primer paso es inspeccionar la petición cruda. Problemas comunes:
- Firma incorrecta: estás usando el cuerpo parseado en lugar del crudo. Soluciónalo capturando el cuerpo crudo.
- Timeout: tu endpoint tarda demasiado en responder. Mueve el procesamiento pesado a un trabajo en segundo plano.
- Eventos perdidos: revisa el panel del proveedor para ver los logs de entrega y el historial de reintentos.
Si estás depurando tráfico HTTP, herramientas como Nginx Log Analyzer pueden ayudarte a detectar patrones en fallos de peticiones y códigos de respuesta.
FAQ
¿Qué es un secreto de webhook y cómo lo almaceno?
El secreto es una clave compartida usada para firmar payloads. Guárdalo en variables de entorno o en un gestor de secretos; nunca en el código ni en el control de versiones. Rótalo periódicamente si el proveedor lo permite.
¿Cuántas veces reintentará un proveedor un webhook fallido?
Varía según el proveedor. Algunos reintentan durante 24 horas con backoff exponencial, otros durante 3 días. Consulta la documentación de tu proveedor para conocer el calendario exacto y el número máximo de intentos.
¿Puedo usar webhooks con funciones serverless?
Sí, pero ten en cuenta los límites de tiempo de ejecución. Confirma el webhook rápidamente (devuelve 200) y descarga el procesamiento pesado a una cola o función en segundo plano para evitar timeouts.
¿Listo para construir integraciones de webhook robustas? Empieza verificando las firmas e implementando idempotencia: estos dos pasos te ahorrarán horas de depuración y evitarán incidentes de seguridad costosos.