Webhooks explicados: payloads firmados y reintentos
Acabas de integrar el webhook de un proveedor de pagos. La documentación dice "enviaremos una petición POST a tu endpoint cuando un pago se complete". Sencillo, ¿verdad? Pero luego notas eventos duplicados, notificaciones perdidas y una alerta de seguridad sobre peticiones falsificadas. Los webhooks son simples en concepto pero complicados en producción. Este artículo explica cómo implementar webhooks correctamente usando payloads firmados y reintentos, para que puedas construir integraciones fiables y seguras.
¿Qué son los webhooks?
Un webhook es una API inversa: en lugar de que tu aplicación consulte periódicamente a un servicio para obtener actualizaciones, el servicio envía eventos a tu endpoint mediante HTTP POST. Es eficiente y casi en tiempo real. Algunos usos habituales incluyen notificaciones de pagos, disparadores de CI/CD e integraciones de chat.
Pero los webhooks introducen desafíos: ¿cómo sabes que la petición proviene realmente del proveedor? ¿Qué pasa si tu servidor está caído? ¿Y si el mismo evento llega dos veces? Vamos a abordarlos uno por uno.
Proteger webhooks con payloads firmados
Cualquiera puede enviar una petición HTTP a tu endpoint. Sin verificación, los atacantes podrían falsificar eventos, provocando fraude o corrupción de datos. La solución estándar es firmar el payload con un secreto compartido.
Cómo funcionan las firmas HMAC
El proveedor y tú compartís una clave secreta. Para cada evento, el proveedor calcula un HMAC (código de autenticación de mensajes basado en hash) del cuerpo de la petición usando ese secreto y envía la firma en una cabecera (por ejemplo, X-Signature). Tú recalculas el HMAC por tu lado y lo comparas. Si coinciden, la petición es auténtica y no ha sido manipulada.
Aquí tienes un ejemplo mínimo en Python usando Flask:
import hmac
import hashlib
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = b'your_shared_secret'
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-Signature')
if not signature:
abort(400, 'Missing signature')
body = request.get_data()
expected = hmac.new(SECRET, body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
abort(401, 'Invalid signature')
# Process the event
return '', 200
Usa siempre hmac.compare_digest para una comparación en tiempo constante y así prevenir ataques de temporización. Además, verifica la firma antes de parsear el cuerpo JSON para evitar procesamiento innecesario.
Buenas prácticas para las firmas
- Usa un secreto robusto: genera una clave aleatoria de al menos 32 bytes y guárdala de forma segura (por ejemplo, en una variable de entorno).
- Incluye una marca de tiempo: para prevenir ataques de repetición, los proveedores suelen incluir un timestamp en el payload firmado. Rechaza peticiones con más de unos minutos de antigüedad.
- Rota los secretos periódicamente: admite varios secretos activos durante la rotación para evitar caídas.
- Documenta el algoritmo de firma: especifica la función hash (SHA-256 es el estándar) y el nombre de la cabecera.
Gestionar fallos con reintentos
Desde la perspectiva del proveedor, los webhooks son de tipo "dispara y olvida", pero tu endpoint podría no estar disponible temporalmente. Para garantizar la entrega, los proveedores implementan políticas de reintentos. Como receptor, debes estar preparado para reintentos y duplicados.
Estrategias de reintento usadas por los proveedores
La mayoría de los proveedores reintentan con backoff exponencial: por ejemplo, inmediatamente, luego tras 1 minuto, 5 minutos, 30 minutos, etc., hasta un número máximo de intentos (a menudo entre 5 y 10). Algunos también permiten configurar un calendario de reintentos. El comportamiento exacto varía, así que consulta la documentación del proveedor.
Diseñar tu endpoint para los reintentos
Tu endpoint debería:
- Responder rápido: confirma la recepción con un código 2xx en pocos segundos. Realiza el procesamiento pesado de forma asíncrona (por ejemplo, encolando el evento).
- Ser idempotente: el mismo evento puede entregarse varias veces. Usa una clave de idempotencia (a menudo proporcionada en el payload o en una cabecera) para deduplicar. Almacena los IDs de eventos procesados durante un periodo razonable.
- Devolver códigos de estado adecuados: 2xx para éxito, 4xx para errores del cliente (el proveedor podría no reintentar), 5xx para errores del servidor (el proveedor reintentará).
Ejemplo de comprobación de idempotencia en Python:
processed_events = set() # In production, use a database or cache
@app.route('/webhook', methods=['POST'])
def webhook():
# ... signature verification ...
event = request.json
event_id = event['id']
if event_id in processed_events:
return '', 200 # Already processed
# Process event
processed_events.add(event_id)
return '', 200
Probar tu implementación de webhooks
Antes de pasar a producción, prueba tu endpoint a fondo:
- Simula peticiones firmadas: escribe pruebas unitarias que generen firmas válidas e inválidas.
- Prueba escenarios de reintento: simula fallos y asegúrate de que tu endpoint gestiona los duplicados.
- Usa las herramientas del proveedor: muchos proveedores ofrecen una CLI o un panel para enviar eventos de prueba.
- Monitoriza y registra: registra los eventos entrantes (sin datos sensibles) y configura alertas para fallos de firma o tasas de error elevadas.
Comparación: polling vs webhooks
| Aspecto | Polling | Webhooks |
|---|---|---|
| Latencia | Depende del intervalo (por ejemplo, minutos) | Casi en tiempo real |
| Carga del servidor | Alta (peticiones frecuentes) | Baja (solo ante eventos) |
| Complejidad | Sencillo de implementar | Requiere verificación de firma y gestión de reintentos |
| Fiabilidad | Tú controlas los reintentos | El proveedor controla los reintentos; tú debes gestionar los duplicados |
Preguntas frecuentes
¿Cómo verifico la firma de un webhook?
Calcula el HMAC del cuerpo de la petición sin procesar usando el secreto compartido y compáralo con la cabecera de firma usando una función de comparación en tiempo constante. Verifica siempre antes de procesar el payload.
¿Qué debería devolver mi endpoint para confirmar un webhook?
Devuelve un código de estado 2xx (por ejemplo, 200 OK) en cuanto hayas recibido y encolado el evento para su procesamiento. Evita procesamientos largos en el manejador de la petición para prevenir timeouts.
¿Cómo puedo gestionar entregas duplicadas de webhooks?
Usa una clave de idempotencia del payload del evento (o un ID de evento único) para rastrear los eventos procesados. Almacena estos IDs en una base de datos o caché y omite el procesamiento si el ID ya se ha visto antes.
¿Listo para probar tus payloads de webhook? Usa nuestro JSON Formatter para formatear y validar el cuerpo JSON de los webhooks entrantes, lo que facilita la depuración.