Webhooks explicados: cargas firmadas y reintentos
Acabas de integrar un proveedor de pagos externo. El usuario completa una compra, pero tu base de datos nunca se actualiza. ¿Por qué? Porque el webhook que debía notificar a tu aplicación falló silenciosamente. Sin una verificación de firma adecuada y una lógica de reintentos, los webhooks pueden convertirse en una fuente de errores misteriosos y vulnerabilidades de seguridad.
En este artículo, explicaremos cómo funcionan los webhooks, por qué debes verificar las cargas firmadas y cómo diseñar un mecanismo de reintentos robusto. También te mostraremos cómo probar y depurar webhooks de manera efectiva.
¿Qué son los webhooks?
Los webhooks son callbacks HTTP definidos por el usuario. Permiten que un sistema envíe datos en tiempo real a otro cuando ocurre un evento. Por ejemplo, cuando se procesa un pago, el proveedor de pagos envía una solicitud HTTP POST a una URL que especifiques, con detalles sobre el evento.
A diferencia del polling, donde tu aplicación pregunta repetidamente por actualizaciones, los webhooks te envían los datos en cuanto están disponibles. Esto los hace eficientes y casi en tiempo real.
El problema de seguridad: ¿por qué verificar firmas?
Los endpoints de webhooks son URLs accesibles públicamente. Cualquiera puede enviarles una solicitud POST. Sin verificación, un atacante podría falsificar una solicitud para, por ejemplo, marcar un pedido como pagado sin que se haya realizado el pago. Para evitarlo, los proveedores firman cada carga con una clave secreta.
Cómo funcionan las firmas HMAC
La mayoría de los proveedores utilizan HMAC (Código de Autenticación de Mensajes basado en Hash) con SHA-256. Calculan un hash del cuerpo de la solicitud usando un secreto compartido y lo incluyen en un encabezado, como X-Signature. Tu servidor recalcula el hash y lo compara con el encabezado. Si coinciden, la solicitud es auténtica.
Aquí tienes un ejemplo simple en Python:
import hmac
import hashlib
def verify_signature(payload, secret, signature):
expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
Utiliza siempre una función de comparación de tiempo constante como hmac.compare_digest para evitar ataques de temporización.
Errores comunes que debes evitar
- Usar el secreto incorrecto: Cada entorno (pruebas, producción) debe tener su propio secreto.
- No verificar la marca de tiempo: Algunos proveedores incluyen una marca de tiempo para prevenir ataques de repetición. Rechaza solicitudes con más de unos minutos de antigüedad.
- Registrar secretos: Nunca registres el secreto ni la firma completa en producción.
Diseñando reintentos confiables
Los webhooks se envían por internet, por lo que los fallos son inevitables. Las redes fallan, los servidores se reinician y los endpoints se agotan. Un buen sistema de webhooks reintenta las entregas fallidas con retroceso exponencial.
Cómo funcionan típicamente los reintentos
Los proveedores suelen reintentar con retrasos crecientes: después de 1 minuto, 5 minutos, 30 minutos, etc., hasta un número máximo de intentos (por ejemplo, 5-10). Si todos los reintentos fallan, el evento se marca como fallido y puede estar disponible para reproducción manual.
Tu endpoint debe responder rápidamente con un código de estado 2xx para confirmar la recepción. Si devuelves un código que no sea 2xx o se agota el tiempo, el proveedor reintentará.
Implementando idempotencia
Dado que los reintentos pueden causar entregas duplicadas, tu endpoint debe ser idempotente. Utiliza el ID del evento (incluido en la carga o en los encabezados) para rastrear los eventos procesados. Antes de procesar, verifica si ya has manejado ese ID. Almacena los IDs procesados en una base de datos con un TTL.
Ejemplo de verificación de idempotencia:
def process_event(event_id, data):
if db.exists(f"processed:{event_id}"):
return # ya procesado
# procesar datos
db.setex(f"processed:{event_id}", 86400, "1") # expira después de 24h
Construyendo un receptor de webhooks: paso a paso
- Expón un endpoint HTTPS: Usa TLS para proteger los datos en tránsito.
- Verifica la firma: Rechaza solicitudes con firmas inválidas con un 401.
- Verifica la marca de tiempo: Si se proporciona, asegúrate de que esté dentro de una ventana aceptable (por ejemplo, 5 minutos).
- Analiza la carga: Valida la estructura JSON y los campos requeridos.
- Verifica la idempotencia: Usa el ID del evento para evitar duplicados.
- Procesa de forma asíncrona: Responde con 200 inmediatamente, luego procesa en un trabajo en segundo plano para evitar tiempos de espera.
- Registra y monitorea: Registra fallos y configura alertas.
Probando webhooks localmente
Durante el desarrollo, necesitas una forma de recibir webhooks en tu máquina local. Herramientas como ngrok o localtunnel crean una URL pública que redirige a tu localhost. Alternativamente, muchos proveedores ofrecen una CLI para activar eventos de prueba.
Prueba siempre la verificación de firma con firmas válidas e inválidas. Simula reintentos devolviendo códigos de error y observando el comportamiento de reintento del proveedor.
Comparación: Webhooks vs. Polling
| Aspecto | Webhooks | Polling |
|---|---|---|
| Tiempo real | Casi en tiempo real | Retrasado por el intervalo |
| Eficiencia | Solo envía datos cuando ocurre el evento | Solicitudes frecuentes incluso sin datos |
| Complejidad | Requiere endpoint, verificación de firma, reintentos | Más simple de implementar |
| Confiabilidad | Depende del mecanismo de reintentos | Tú controlas el horario |
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 proviene del remitente esperado y no fue manipulada.
¿Cuántas veces se debe reintentar un webhook?
La mayoría de los proveedores reintentan de 5 a 10 veces con retroceso exponencial. El número exacto depende de la política del proveedor. Tu endpoint debe ser idempotente para manejar duplicados.
¿Qué código de estado HTTP debe devolver mi webhook?
Devuelve un código de estado 2xx (por ejemplo, 200 OK) para confirmar la recepción exitosa. Cualquier otro código (4xx o 5xx) indica fallo y activa reintentos.
¿Listo para probar las cargas de tus webhooks? Usa nuestro Formateador JSON para validar y formatear cargas JSON rápidamente.