Webhooks explicados: payloads firmados y lógica de reintentos

Backend2026-10-25TryQuickToolBox

Has creado una API y ahora necesitas notificar a otros servicios cuando ocurren eventos. Decides implementar webhooks: callbacks HTTP que envían datos a URLs de suscriptores. Pero pronto te enfrentas a dos preguntas críticas: ¿Cómo aseguras que el payload no ha sido manipulado? ¿Y qué sucede cuando el servidor receptor está caído o devuelve un error? Sin las salvaguardas adecuadas, los webhooks pueden convertirse en un agujero de seguridad y una pesadilla de confiabilidad.

En esta guía, repasaremos los aspectos esenciales para asegurar webhooks con payloads firmados e implementar una lógica de reintentos robusta. Aprenderás a generar y verificar firmas HMAC, diseñar manejadores idempotentes y construir un mecanismo de reintentos que maneje los fallos con elegancia. Al final, tendrás un enfoque listo para producción para webhooks.

Por qué los webhooks necesitan seguridad y confiabilidad

Los webhooks son esencialmente solicitudes HTTP de servidor a servidor desencadenadas por eventos. A diferencia de las llamadas API iniciadas por un cliente, los webhooks se envían desde tu servidor al endpoint de un suscriptor. Este modelo de envío introduce dos desafíos principales:

Ignorar estos desafíos puede llevar a brechas de datos, eventos perdidos o estados inconsistentes entre sistemas. Abordémoslos uno por uno.

Asegurando webhooks con payloads firmados

Un payload firmado utiliza un hash criptográfico para probar la integridad y autenticidad de la solicitud. El enfoque más común es HMAC (Hash-based Message Authentication Code) con un secreto compartido. Así es como funciona:

  1. Secreto compartido: Tanto el emisor (tu servicio) como el receptor (suscriptor) comparten una clave secreta. Esta clave se genera cuando el suscriptor registra su endpoint de webhook.
  2. Generación de firma: Para cada solicitud de webhook, el emisor calcula un HMAC del cuerpo de la solicitud (y a veces encabezados como la marca de tiempo) usando el secreto. La firma se envía en un encabezado, por ejemplo, X-Signature.
  3. Verificación de firma: El receptor recalcula el HMAC usando el mismo secreto y lo compara con la firma recibida. Si coinciden, el payload es auténtico y no ha sido modificado.

Veamos un ejemplo de código en Node.js para generar y verificar firmas.

const crypto = require('crypto');

// Sender: generate signature
function generateSignature(payload, secret) {
  return crypto.createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');
}

// Receiver: verify signature
function verifySignature(payload, secret, receivedSignature) {
  const expectedSignature = generateSignature(payload, secret);
  // Use timingSafeEqual to prevent timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(expectedSignature, 'hex'),
    Buffer.from(receivedSignature, 'hex')
  );
}

Importante: Siempre usa una función de comparación de tiempo constante como timingSafeEqual para evitar ataques de temporización. Además, incluye una marca de tiempo en los datos firmados para prevenir ataques de repetición. Por ejemplo, firma timestamp + '.' + payload y rechaza solicitudes con más de unos minutos de antigüedad.

Mejores prácticas para payloads firmados

Implementando lógica de reintentos para una entrega confiable

Incluso con la seguridad implementada, los webhooks pueden fallar debido a problemas de red, errores del servidor o tiempos de espera. Un mecanismo de reintentos robusto asegura que los eventos se entreguen eventualmente. Aquí tienes un enfoque paso a paso:

  1. Establece un tiempo de espera: Define un tiempo de espera razonable para las solicitudes de webhook (por ejemplo, 10 segundos). Si el suscriptor no responde a tiempo, considéralo un fallo.
  2. Clasifica las respuestas: Trata los códigos de estado 2xx como éxito. Para errores 4xx (excepto 429), reintentar generalmente no tiene sentido porque la solicitud es inválida. Para errores 5xx y tiempos de espera, reintenta con retroceso exponencial.
  3. Retroceso exponencial con jitter: Programa reintentos a intervalos crecientes (por ejemplo, 1s, 5s, 30s, 5m, 30m) y añade jitter aleatorio para evitar problemas de estampida.
  4. Limita los reintentos: Establece un número máximo de intentos (por ejemplo, 5) y luego marca la entrega como fallida. Opcionalmente, notifica al suscriptor por correo electrónico o mediante un panel.
  5. Idempotencia: Dado que los reintentos pueden causar entregas duplicadas, incluye un ID de evento único en el payload y en el encabezado. Los suscriptores deben usar este ID para deduplicar el procesamiento.

Aquí tienes una implementación simplificada de cola de reintentos en Python usando requests y time:

import requests
import time
import random

def deliver_webhook(url, payload, secret, max_retries=5):
    signature = generate_signature(payload, secret)
    headers = {'X-Signature': signature, 'Content-Type': 'application/json'}
    for attempt in range(max_retries):
        try:
            response = requests.post(url, json=payload, headers=headers, timeout=10)
            if 200 <= response.status_code < 300:
                return True
            elif response.status_code == 429 or 500 <= response.status_code < 600:
                # Retryable error
                pass
            else:
                # Non-retryable error
                return False
        except requests.exceptions.RequestException:
            # Network error, retry
            pass
        # Exponential backoff with jitter
        delay = min(30, 2 ** attempt) + random.uniform(0, 1)
        time.sleep(delay)
    return False

Idempotencia: manejando entregas duplicadas

Debido a que los reintentos pueden causar que el mismo evento se entregue varias veces, los suscriptores deben implementar un procesamiento idempotente. El enfoque estándar es incluir un ID de evento único (por ejemplo, un UUID) en el payload y hacer que el receptor almacene los IDs procesados durante un período determinado. Si se recibe un ID duplicado, el receptor lo confirma sin reprocesar.

Aquí tienes una verificación de idempotencia simple en un manejador de webhooks:

processed_events = set()

def handle_webhook(event):
    event_id = event['id']
    if event_id in processed_events:
        return 'already processed'
    # Process event
    processed_events.add(event_id)
    return 'success'

En producción, usa un almacén persistente como Redis o una base de datos con TTL para rastrear los IDs procesados.

Comparación: payloads firmados vs. otras medidas de seguridad

Enfoque Ventajas Desventajas
Payloads firmados con HMAC Fuerte integridad y autenticidad; no se transmiten secretos Requiere gestión de secretos compartidos
Autenticación básica Simple de implementar Credenciales enviadas en cada solicitud; menos seguro
Lista de IPs permitidas Bloquea fuentes no autorizadas Las IPs pueden cambiar; no adecuado para entornos dinámicos
Credenciales de cliente OAuth2 Estandarizado; basado en tokens Excesivo para webhooks simples; sobrecarga de gestión de tokens

Los payloads firmados con HMAC logran un buen equilibrio entre seguridad y simplicidad para la mayoría de los casos de uso de webhooks.

Poniéndolo todo junto

Para construir un sistema de webhooks confiable y seguro:

Al depurar entregas de webhooks, inspeccionar las solicitudes y respuestas HTTP sin procesar es invaluable. Si estás trabajando con registros de Nginx para rastrear solicitudes de webhook entrantes, el Analizador de registros de Nginx puede ayudarte a analizar y filtrar entradas de registro para identificar entregas fallidas o patrones sospechosos.

Preguntas frecuentes

¿Qué es un payload de webhook firmado?

Un payload de webhook firmado incluye una firma criptográfica (generalmente HMAC) que permite al receptor verificar la autenticidad e integridad de la solicitud. El emisor calcula la firma usando un secreto compartido y la incluye en un encabezado; el receptor la recalcula y compara.

¿Cuántas veces debo reintentar un webhook fallido?

Una práctica común es de 3 a 5 reintentos con retroceso exponencial. Después de eso, marca la entrega como fallida y notifica al suscriptor. El número exacto depende de tu SLA y la criticidad del evento.

¿Por qué es importante la idempotencia para los webhooks?

Los reintentos pueden causar entregas duplicadas. La idempotencia asegura que procesar el mismo evento varias veces tenga el mismo efecto que procesarlo una vez, evitando corrupción de datos o acciones duplicadas.