Webhooks explicados: cargas firmadas y reintentos

Backend2026-10-26TryQuickToolBox

Por qué los webhooks necesitan cargas firmadas y reintentos

Acabas de integrar un servicio de terceros que envía webhooks a tu endpoint. Todo funciona en las pruebas, pero en producción notas eventos faltantes, entregas duplicadas y, peor aún, alguien falsificando solicitudes a tu URL de webhook. Sin las medidas adecuadas de seguridad y confiabilidad, los webhooks pueden convertirse en una fuente de errores y vulnerabilidades.

Los webhooks son callbacks HTTP que notifican a tu aplicación cuando ocurre un evento. Son simples en concepto pero complicados en la práctica porque cruzan límites de confianza y problemas de confiabilidad de red. En esta guía, cubriremos dos aspectos críticos: cargas firmadas para verificar la autenticidad y reintentos para asegurar la entrega. Recorreremos implementaciones prácticas que puedes aplicar de inmediato.

Entendiendo las cargas firmadas

Una carga firmada es una forma en que el emisor demuestra que el webhook provino de él y no fue alterado. El emisor calcula una firma criptográfica sobre el cuerpo de la solicitud (y a veces los encabezados) usando un secreto compartido, luego incluye esa firma en un encabezado. Tu endpoint recalcula la firma y la compara.

El algoritmo más común es HMAC-SHA256. Es rápido, seguro y ampliamente soportado. Así es como funciona:

  1. El emisor y el receptor comparten una clave secreta (configurada en los ajustes del webhook).
  2. Para cada webhook, el emisor crea una firma hasheando la carga con el secreto usando HMAC-SHA256.
  3. La firma se envía en un encabezado como X-Signature.
  4. El receptor calcula el mismo HMAC y lo compara usando una comparación de tiempo constante para prevenir ataques de temporización.

Implementando verificación HMAC en Node.js

Veamos un ejemplo concreto. Supongamos que el emisor incluye la firma en X-Signature como una cadena hexadecimal. Así es como la verificas en una ruta de Express:

const crypto = require('crypto');
const express = require('express');
const app = express();

// Usar el cuerpo sin procesar para la verificación de firma
app.use('/webhook', express.raw({ type: 'application/json' }));

app.post('/webhook', (req, res) => {
  const signature = req.headers['x-signature'];
  const secret = process.env.WEBHOOK_SECRET;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(req.body)
    .digest('hex');

  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
    return res.status(401).send('Firma inválida');
  }

  // Procesar el evento
  const event = JSON.parse(req.body);
  console.log('Evento recibido:', event.type);

  res.status(200).send('OK');
});

app.listen(3000);

Importante: Siempre usa el cuerpo de la solicitud sin procesar para el cálculo de la firma, no el JSON parseado. Parsear y volver a stringificar puede cambiar los espacios en blanco y el orden, rompiendo la firma.

Protección contra marcas de tiempo y replay

Las firmas por sí solas no previenen ataques de replay. Un atacante podría capturar una solicitud firmada válida y reenviarla más tarde. Para mitigar esto, incluye una marca de tiempo en la carga firmada o en un encabezado. Por ejemplo, agrega un encabezado X-Timestamp con la hora Unix. El receptor verifica que la marca de tiempo esté dentro de una ventana de tolerancia (por ejemplo, 5 minutos) y rechaza solicitudes más antiguas.

Además, incluye la marca de tiempo en el cálculo de la firma para prevenir alteraciones. El emisor calcula HMAC sobre timestamp + '.' + body. El receptor hace lo mismo y compara.

Implementando reintentos para una entrega confiable

Los webhooks viajan por la internet pública, así que los fallos son inevitables: tiempos de espera de red, errores del servidor (5xx) o tu endpoint temporalmente caído. Un sistema de webhooks robusto debe reintentar entregas fallidas.

Estrategias de reintento

Las estrategias de reintento comunes incluyen:

Una política típica podría ser: reintentar hasta 5 veces con backoff exponencial y jitter, comenzando a los 10 segundos. Después de eso, marcar la entrega como fallida y posiblemente alertar.

Idempotencia: manejando duplicados

Los reintentos pueden causar entregas duplicadas. Tu endpoint debe ser idempotente: procesar el mismo evento dos veces no debería causar efectos secundarios como cobrar dos veces a un cliente. Incluye un ID de evento único en la carga (por ejemplo, event.id) y almacena los IDs procesados durante un período razonable. Antes de procesar, verifica si el ID ya ha sido visto.

Alternativamente, diseña tus operaciones para que sean naturalmente idempotentes (por ejemplo, usando upserts en lugar de inserts).

Respondiendo rápidamente

Tu endpoint de webhook debe acusar recibo rápidamente (en unos pocos segundos) y procesar el evento de forma asíncrona. Si el procesamiento tarda demasiado, el emisor puede agotar el tiempo de espera y reintentar, causando duplicados. Usa una cola: responde con 200 OK inmediatamente después de verificar la firma, luego encola el evento para procesamiento en segundo plano.

Comparación de medidas de seguridad para webhooks

Medida Propósito Complejidad de implementación
Firma HMAC Verificar autenticidad e integridad Baja
Validación de marca de tiempo Prevenir ataques de replay Baja
Claves de idempotencia Evitar procesamiento duplicado Media
Lista blanca de IPs Restringir a IPs de emisores conocidos Baja (pero frágil)
HTTPS Cifrar datos en tránsito Baja

Si bien la lista blanca de IPs puede agregar una capa, no es confiable porque las IPs del emisor pueden cambiar. Confía principalmente en las firmas.

Probando tu endpoint de webhook

Usa herramientas como ngrok para exponer tu servidor local y disparar webhooks de prueba desde el panel del proveedor. Verifica que las firmas se validen correctamente y que los reintentos funcionen simulando fallos (por ejemplo, devolver 500 en los primeros intentos).

Al depurar, es posible que necesites inspeccionar cargas JSON. El Formateador JSON puede ayudarte a formatear y validar cargas de webhook rápidamente.

Preguntas frecuentes

¿Qué es una carga de webhook firmada?

Una carga firmada incluye una firma criptográfica (generalmente HMAC) que permite al receptor verificar que la solicitud provino del emisor esperado y no fue alterada. Se calcula usando un secreto compartido.

¿Cuántas veces debo reintentar un webhook fallido?

No hay un número universal, pero 3–5 reintentos con backoff exponencial son comunes. Después de eso, registra el fallo y considera alertar. El objetivo es manejar problemas transitorios sin reintentos infinitos.

¿Puedo usar JWT para firmas de webhooks?

Sí, algunos proveedores usan JWT (JSON Web Tokens) para firmar webhooks. El token contiene claims y está firmado con un secreto o clave privada. La verificación es similar: valida la firma y verifica claims como la expiración.

¿Listo para implementar webhooks seguros? Comienza generando un secreto fuerte, agregando verificación HMAC y configurando reintentos con idempotencia. Para una inspección rápida de cargas durante el desarrollo, prueba nuestro Formateador JSON para formatear y validar el JSON de webhooks.