Webhooks explicados: cargas firmadas y reintentos

Backend2026-10-22TryQuickToolBox

Has creado un endpoint de webhook para recibir eventos de un servicio de terceros. Todo funciona en las pruebas, pero en producción notas eventos duplicados, notificaciones perdidas y ocasionalmente preocupaciones de seguridad sobre solicitudes falsificadas. Estos son errores comunes al implementar webhooks sin las protecciones adecuadas.

Los webhooks son callbacks HTTP que permiten a los servicios notificar a tu aplicación cuando ocurre algo. A diferencia del polling, proporcionan actualizaciones en tiempo real. Pero para usarlos de manera confiable, debes abordar dos aspectos críticos: autenticidad (asegurar que la solicitud provino del remitente esperado) y entrega (manejar fallos y reintentos). Este artículo explica ambos con ejemplos de código prácticos.

Cómo funcionan los webhooks

Un webhook es un endpoint HTTP POST definido por el usuario. Cuando ocurre un evento, el servicio de origen envía una solicitud a esa URL con una carga útil que describe el evento. Por ejemplo, un procesador de pagos podría enviar un webhook cuando un cargo se completa con éxito.

El flujo es simple:

  1. Registras una URL de webhook con el servicio.
  2. El servicio envía un HTTP POST a esa URL cuando se dispara un evento.
  3. Tu endpoint procesa la carga útil y devuelve un estado 2xx para confirmar la recepción.

Sin una verificación adecuada, cualquiera puede enviar solicitudes a tu endpoint. Sin manejo de reintentos, los fallos transitorios pueden causar pérdida de eventos. Abordemos cada uno.

Asegurando webhooks con cargas firmadas

Para asegurar que un webhook se originó del remitente esperado, los servicios firman la carga útil usando un secreto compartido. La firma es típicamente un HMAC (código de autenticación de mensajes basado en hash) del cuerpo de la solicitud, enviado en un encabezado como X-Signature.

Cómo funciona la firma HMAC

El remitente calcula un HMAC usando una clave secreta y el cuerpo crudo de la solicitud, luego incluye la firma codificada en hexadecimal en un encabezado. Tu endpoint recalcula el HMAC con el mismo secreto y lo compara con la firma recibida. Si coinciden, la solicitud es auténtica.

Aquí hay un ejemplo mínimo en Node.js usando Express:

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

// Usar cuerpo crudo para verificación de firma
app.use(express.raw({ type: 'application/json' }));

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

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

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

  // Procesar el evento
  const event = JSON.parse(payload);
  // ... manejar evento
  res.status(200).send('OK');
});

Puntos clave:

Medidas de seguridad adicionales

Implementando reintentos confiables

La entrega de webhooks no está garantizada. Las redes fallan, tu servidor podría estar caído o podrías devolver un estado no 2xx. Para manejar esto, los remitentes típicamente reintentan con retroceso exponencial. Como receptor, debes diseñar tu endpoint para ser idempotente y responder rápidamente.

Del lado del remitente (si envías webhooks)

Si estás construyendo un servicio que envía webhooks, implementa reintentos con retroceso. Una estrategia común:

Ejemplo de lógica de reintentos en Python:

import time
import requests
from requests.exceptions import RequestException

def send_webhook(url, payload, max_retries=5):
    for attempt in range(max_retries):
        try:
            response = requests.post(url, json=payload, timeout=5)
            if 200 <= response.status_code < 300:
                return True
            elif 400 <= response.status_code < 500 and response.status_code != 429:
                # Error del cliente, no reintentar
                return False
        except RequestException:
            pass
        # Retroceso exponencial con jitter
        delay = (2 ** attempt) + random.uniform(0, 1)
        time.sleep(delay)
    return False

Del lado del receptor

Tu endpoint debería:

Ejemplo de idempotencia usando una restricción única de base de datos:

CREATE TABLE processed_events (
  event_id VARCHAR(255) PRIMARY KEY,
  processed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Antes de procesar, intenta insertar el ID del evento. Si viola la clave primaria, omite el procesamiento.

Probando webhooks localmente

Durante el desarrollo, necesitas una URL pública para que el servicio alcance tu servidor local. Herramientas como ngrok o localtunnel exponen tu localhost. Siempre usa secretos de prueba y verifica firmas en desarrollo para detectar problemas temprano.

Comparación: Polling vs Webhooks

Aspecto Polling Webhooks
Latencia Depende del intervalo Casi en tiempo real
Carga del servidor Alta (solicitudes frecuentes) Baja (solo en eventos)
Complejidad Simple de implementar Requiere verificación de firma y manejo de reintentos
Confiabilidad Tú controlas los reintentos Depende de la política de reintentos del remitente

Preguntas frecuentes

¿Qué es una carga útil de webhook firmada?

Una carga útil firmada incluye una firma criptográfica (generalmente HMAC) generada con un secreto compartido. Permite al receptor verificar que la solicitud provino del remitente esperado y no fue manipulada.

¿Cómo manejo las entregas duplicadas de webhooks?

Haz tu endpoint idempotente usando un ID de evento único de la carga útil. Almacena los IDs procesados e ignora los duplicados. Esto asegura que los reintentos o problemas de red no causen doble procesamiento.

¿Debería reintentar entregas fallidas de webhooks?

Sí, pero solo para errores transitorios (problemas de red, 5xx). Usa retroceso exponencial con jitter y establece un límite máximo de reintentos. Evita reintentar en errores 4xx (excepto 429) ya que indican un problema con la solicitud en sí.

Al depurar cargas útiles de webhooks, a menudo necesitas inspeccionar datos JSON. El JSON Formatter puede ayudarte a formatear y validar cargas útiles JSON rápidamente.