Webhooks: Payloads firmados y reintentos
Acabas de integrar una pasarela de pago y ahora necesitas actualizar tu base de datos cuando un pago se completa. Hacer polling a la API cada pocos segundos es ineficiente y lento. Los webhooks solucionan esto enviando eventos a tu servidor en el momento en que ocurren. Pero sin la seguridad y fiabilidad adecuadas, los webhooks pueden convertirse en una fuente de bugs y vulnerabilidades. Este artículo explica cómo implementar webhooks correctamente, centrándose en payloads firmados y reintentos.
¿Qué son los webhooks?
Un webhook es un callback HTTP: ocurre un evento en un servicio y ese servicio envía una petición HTTP POST a una URL que tú proporcionas. El payload normalmente contiene datos JSON sobre el evento. Por ejemplo, cuando un cliente completa un pago, Stripe envía un evento charge.succeeded a tu endpoint.
Los webhooks se basan en push, por lo que reducen la latencia y la carga del servidor en comparación con el polling. Sin embargo, introducen desafíos: ¿cómo sabes que la petición es genuina? ¿Y si tu servidor está caído cuando se dispara el evento?
Por qué importan los payloads firmados
Cualquiera puede enviar un HTTP POST a tu endpoint de webhook. Sin verificación, un atacante podría falsificar eventos, provocando acciones no autorizadas como marcar un pedido como pagado. Los payloads firmados solucionan esto incluyendo una firma criptográfica que solo el emisor y tú podéis generar.
El método más común es HMAC (Hash-based Message Authentication Code) con un secreto compartido. El emisor calcula un hash del payload usando el secreto y lo incluye en una cabecera. Tú recalculas el hash y lo comparas. Si coinciden, la petición es auténtica.
Cómo verificar un payload firmado
Aquí tienes un enfoque paso a paso usando Node.js y Express:
- Obtén el cuerpo de la petición en bruto como string. No lo parsees como JSON antes de la verificación, ya que el parseo puede alterar la secuencia de bytes.
- Extrae la firma de la cabecera (por ejemplo,
X-Signature). - Calcula el HMAC usando tu secreto y el cuerpo en bruto.
- Compara la firma calculada con la recibida usando una comparación de tiempo constante para prevenir ataques de temporización.
const crypto = require('crypto');
function verifySignature(rawBody, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.headers['x-signature'];
if (!verifySignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
// Process event
res.status(200).send('OK');
});
Usa siempre un secreto fuerte y rótalo periódicamente. Nunca lo expongas en código del lado del cliente.
Implementar reintentos con backoff
Los webhooks se entregan a través de internet, así que los fallos ocurren. Tu servidor podría estar caído, o la red podría fallar. Un sistema de webhooks robusto reintenta las entregas fallidas con backoff exponencial.
Normalmente, el emisor reintenta si no recibe una respuesta 2xx dentro de un timeout (por ejemplo, 5 segundos). El calendario de reintentos podría ser: inmediato, luego tras 1 minuto, 5 minutos, 30 minutos, 2 horas, etc., hasta un número máximo de intentos (por ejemplo, 5).
Como receptor, debes responder rápidamente para evitar timeouts. Confirma la recepción con un 200 OK y procesa el evento de forma asíncrona.
Manejar la idempotencia
Como los reintentos pueden provocar entregas duplicadas, el procesamiento de tus eventos debe ser idempotente. Incluye un ID de evento único en el payload y almacena los IDs procesados. Antes de procesar, comprueba si el ID ya se ha gestionado.
async function processEvent(event) {
const { id, type, data } = event;
if (await db.processedEvents.findOne({ id })) {
return; // Already processed
}
await db.processedEvents.insertOne({ id });
// Handle event based on type
}
Usa una transacción de base de datos para asegurar que el evento se marca como procesado solo tras un manejo exitoso.
Buenas prácticas de seguridad para webhooks
- Verifica siempre las firmas antes de procesar.
- Usa HTTPS para evitar la interceptación.
- Valida el timestamp si está incluido, para prevenir ataques de replay.
- Limita el tamaño del payload para evitar denegación de servicio.
- Registra todos los intentos de webhook para depuración y auditoría.
Comparativa: polling vs webhooks
| Aspecto | Polling | Webhooks |
|---|---|---|
| Latencia | Alta (basada en intervalo) | Baja (casi en tiempo real) |
| Carga del servidor | Alta (peticiones constantes) | Baja (solo en eventos) |
| Complejidad | Simple de implementar | Requiere seguridad y reintentos |
| Fiabilidad | Depende de la frecuencia de polling | Depende de la lógica de reintentos |
FAQ
¿Cómo puedo probar webhooks en local?
Usa una herramienta como ngrok para exponer tu servidor local a internet. Configura el proveedor de webhooks para enviar eventos a tu URL de ngrok.
¿Y si mi servidor está caído cuando se envía un webhook?
El emisor debería reintentar con backoff. Asegúrate de que tu servidor tenga alta disponibilidad y responda rápido para evitar timeouts.
¿Puedo usar firmas asimétricas en lugar de HMAC?
Sí, algunos proveedores usan firmas RSA o ECDSA. Las verificas con su clave pública. HMAC es más simple pero requiere un secreto compartido.
Al depurar payloads de webhooks, es útil formatear el JSON para que sea legible. Prueba nuestro JSON Formatter para imprimir con formato y validar payloads de webhooks al instante.