Skip to main content

Webhooks (Interacción Asincrónica de Servicios)

Patrón arquitectónico de interacción asincrónica entre servicios (Event-driven Push), donde el proveedor de eventos envía una solicitud HTTP POST con datos a una URL registrada del consumidor al ocurrir un evento del sistema.

1. Visión general del concepto y problema sistémico

La integración tradicional de sistemas a través de polling constante (Polling / Short Polling) sufre de dos problemas mutuamente excluyentes:

  1. Alta latencia de respuesta: Si el cliente verifica cambios cada 60 segundos, los datos se entregan en promedio con un retraso de 30 segundos.
  2. Colosal desperdicio de recursos computacionales: El 99% de las solicitudes HTTP devuelven una respuesta vacía 304 Not Modified o [], sobrecargando la base de datos y la pila de red.

Webhooks (Llamadas inversas a través de HTTP) cambian la paradigma de Pull a Push. En lugar de preguntar: "¿Hay nuevos datos?", el consumidor registra su URL (Webhook Listener). Tan pronto como ocurre un evento en el sistema del proveedor (transacción exitosa de Stripe, nuevo commit en GitHub, mensaje entrante de Telegram), el proveedor envía una solicitud HTTP POST con un payload JSON directamente al servidor del suscriptor.

Polling tradicional (Desperdicio de recursos):
Cliente  ---> GET /events?since=... ---> Servidor (Vacío: [])
Cliente  ---> GET /events?since=... ---> Servidor (Vacío: [])
Cliente  ---> GET /events?since=... ---> Servidor (¡Nuevo evento!)

Webhook impulsado por eventos (Push inmediato):
Proveedor de eventos (Stripe / GitHub / Telegram)
       |
       |  Evento: charge.succeeded (Event Payload + HMAC firma)
       |  HTTP POST https://api.mysite.com/webhooks/stripe
       v
Consumidor (Validación de firma -> 200 OK -> Cola en segundo plano RabbitMQ/Redis)

2. Taxonomía arquitectónica y modelo mental

El ciclo de vida de un webhook se basa en tres pilares arquitectónicos:

  1. Autenticación criptográfica (Firma HMAC):
    • El proveedor genera un hash criptográfico del cuerpo de la solicitud (raw body) utilizando una clave secreta compartida (Webhook Secret), añadiéndolo en el encabezado (por ejemplo, Stripe-Signature o X-Hub-Signature-256).
    • El consumidor calcula el mismo hash y lo valida, eliminando el riesgo de falsificación de la solicitud por un atacante.
  2. Idempotencia y deduplicación (Motor de Idempotencia):
    • Cada evento tiene un ID único global (evt_xxxxxxxxxx).
    • El consumidor verifica atómicamente la existencia del ID en la caché/BD (por ejemplo, a través de INSERT ... ON CONFLICT DO NOTHING o Redis SET key NX EX 86400).
  3. Política de entrega garantizada y reintentos (Retry & Dead-Letter Queue):
    • En caso de que el servidor del consumidor no esté disponible, el proveedor reintenta las solicitudes con una escala exponencial (Exponential Backoff con jitter): después de 5 minutos, 30 minutos, 2 horas, 12 horas.
    • Los eventos que no se pudieron procesar después del número máximo de intentos se envían a la Dead-Letter Queue (DLQ) para análisis manual por ingenieros.

3. Pipeline técnico y mecánica interna

Manejador de producción con verificación HMAC (Node.js / Express / TypeScript)

[!IMPORTANTE] Para verificar la firma, es necesario obtener el cuerpo sin procesar de la solicitud (raw buffer), y no el objeto parseado req.body, de lo contrario, el hash no coincidirá debido a discrepancias en los espacios y el orden de las claves JSON.

import express, { Request, Response } from "express";
import crypto from "crypto";

const app = express();

// Obtención de raw buffer para endpoints de webhooks
app.post(
  "/api/v1/stripe-webhook",
  express.raw({ type: "application/json" }),
  async (req: Request, res: Response) => {
    const signature = req.headers["stripe-signature"] as string;
    const secret = process.env.STRIPE_WEBHOOK_SECRET!;

    if (!signature) {
      return res.status(400).send("Falta el encabezado de firma");
    }

    // 1. Validación criptográfica de la firma a través de HMAC SHA-256
    const computedHmac = crypto
      .createHmac("sha256", secret)
      .update(req.body)
      .digest("hex");

    // Comparación segura en tiempo fijo
    const isValid = crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(computedHmac)
    );

    if (!isValid) {
      return res.status(401).send("Firma inválida");
    }

    const payload = JSON.parse(req.body.toString());
    const eventId = payload.id;

    // 2. Verificación idempotente de deduplicación
    const isNew = await redis.set(`webhook:processed:${eventId}`, "1", "NX", "EX", 86400);
    if (!isNew) {
      // El evento ya ha sido procesado — devolvemos 200 OK sin reejecución
      return res.status(200).json({ status: "already_processed" });
    }

    // 3. Envío de trabajo pesado a la cola BullMQ
    await eventQueue.add("process_payment", payload);

    // 4. Confirmación inmediata de recepción
    return res.status(200).json({ received: true });
  }
);

4. Escenarios prácticos de ingeniería en producción

01. Pasarela de pago para AI SaaS (Stripe / Paddle / LiqPay)

El usuario paga una suscripción al plan Pro. El sistema de pago genera un evento invoice.payment_succeeded. El webhook actualiza instantáneamente el estado del usuario en la base de datos, restablece los límites de tokens en Redis y envía un correo de bienvenida, permitiendo al usuario continuar sin recargar la página.

02. Disparador de pipeline CI/CD autónomo (GitHub Webhooks)

Un ingeniero realiza git push en la rama main. GitHub envía un payload con el hash del commit a su propio servidor Coolify o ArgoCD. El servidor verifica el secreto, clona el código modificado, ejecuta pruebas unitarias en un contenedor Docker aislado y actualiza el servicio de producción según la estrategia Blue/Green.

03. Recolección asincrónica de resultados de inferencia de modelos AI pesados

Al invocar modelos de generación de video (Runway, Kling) o entrenamiento de LoRA, la solicitud dura varios minutos. En lugar de mantener una conexión HTTP abierta, el cliente proporciona webhook_url. Tan pronto como el clúster de GPU completa la generación, envía un webhook con el enlace final de S3 al medio generado.


5. Errores comunes, trampas y seguridad

  1. Parsing JSON antes de la verificación de la firma: Si antes de la verificación de la firma se ejecutó el middleware estándar express.json(), el serializador puede cambiar el orden de los campos, escapar caracteres Unicode o formatear números, lo que resultará en una discrepancia en la suma de verificación HMAC y un fallo de Invalid Signature.
  2. Bloqueo de ejecución sincrónica (Webhook Deadlock): La ejecución de transacciones pesadas o solicitudes HTTP prolongadas a APIs externas dentro del manejador de webhook garantiza un timeout en el lado del proveedor. Devuelva 200 OK inmediatamente después de validar la firma y guardar en la cola.
  3. Falta de validación de marcas de tiempo (Replay Attacks): Un atacante que interceptó una solicitud de webhook legítima firmada puede volver a enviarla una hora después. Los proveedores añaden una marca de tiempo (timestamp) a la firma. Verifique que la diferencia entre el tiempo de la solicitud y el reloj del sistema del servidor no exceda la ventana permitida (por ejemplo, 5 minutos).
/ Preguntas frecuentesSchema.org FAQPage

FAQ: Webhooks (Interacción Asincrónica de Servicios)

La comparación clásica de cadenas mediante los operadores == o === se interrumpe en el primer carácter no coincidente. Un atacante puede medir la latencia de la respuesta del servidor con precisión (Timing Attack) y adivinar byte a byte un hash de firma válido. La función timingSafeEqual realiza la comparación en un tiempo fijo, garantizando resistencia criptográfica.
/ Enlaces internos
Todos los términos