Tres formas de recibir eventos
| Modelo | Cómo | Ventajas | Inconvenientes |
|---|---|---|---|
| Polling | Llamas a GET /events?since=... periódicamente |
Simple, sin endpoint público | Latencia, peticiones desperdiciadas |
| Webhooks | El proveedor hace POST a tu URL |
Tiempo real, apto para serverless | Requiere endpoint HTTPS público y verificación |
| WebSocket | Conexión persistente | Mínima latencia, bidireccional | Proceso de larga duración, lógica de reconexión |
Anatomía de un buen receptor
- Lee el cuerpo crudo como bytes antes de interpretar el JSON.
- Verifica la firma o la cabecera secreta. Rechaza con
401si falla. - Comprueba la frescura: si el proveedor envía una marca de tiempo, rechaza lo que tenga más de unos cinco minutos.
- Deduplica por el ID del evento.
- Encola el evento y devuelve 2xx de inmediato.
- Procesa en un worker con reintentos y luego llama a la API de la plataforma para responder.
Verificar firmas
La mayoría de proveedores usa HMAC-SHA256 con un secreto compartido. El detalle crítico es firmar los bytes exactos recibidos: si tu framework interpreta y vuelve a serializar el JSON, cambian los espacios y el orden de las claves y la firma falla.
import express from "express";
import crypto from "node:crypto";
const app = express();
app.post("/webhooks/github", express.raw({ type: "application/json", limit: "1mb" }), (req, res) => {
const sig = req.get("X-Hub-Signature-256") || "";
const mac = "sha256=" + crypto.createHmac("sha256", process.env.GITHUB_WEBHOOK_SECRET).update(req.body).digest("hex");
const a = Buffer.from(sig), b = Buffer.from(mac);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(401);
const event = JSON.parse(req.body.toString("utf8"));
queue.add({ id: req.get("X-GitHub-Delivery"), type: req.get("X-GitHub-Event"), event });
res.sendStatus(202);
});
Usa una comparación en tiempo constante (timingSafeEqual, hmac.compare_digest) para que nadie pueda adivinar la firma byte a byte midiendo tiempos. Algunas plataformas usan otros esquemas:
- Slack: HMAC de
v0:{timestamp}:{body}. - Standard Webhooks: HMAC de
{id}.{timestamp}.{body}, en base64 enwebhook-signature: v1,.... - Discord: firma de clave pública Ed25519 sobre
timestamp + body. - Telegram: un
secret_tokenfijo que eliges tú, enviado en una cabecera.
Depura las discrepancias con el probador de webhooks.
Confirma rápido, trabaja después
Los proveedores tienen tiempos de espera cortos (Slack y Discord: 3 segundos; otros de 5 a 30) y reintentan si fallas. Si tu manejador llama a un modelo de IA o a una API lenta antes de responder, superarás el tiempo, el proveedor reintentará y procesarás el mismo evento varias veces. Pon el evento en una cola (Cloudflare Queues, SQS, Redis, una tabla) y devuelve 200 o 202. En plataformas serverless que lo permiten, waitUntil deja que el trabajo continúe tras la respuesta.
Idempotencia
Asume entrega al menos una vez. Guarda los IDs procesados con una caducidad mayor que la ventana de reintentos del proveedor y omite los duplicados. En llamadas salientes que crean cosas (pagos, tickets), envía una clave de idempotencia si la API la admite, para que tus propios reintentos no dupliquen nada.
Reintentos con backoff y jitter
Cuando llames a otras APIs, reintenta solo ante fallos transitorios (errores de red, 429, 5xx), nunca ante 400 o 401. Espera cada vez exponencialmente más y añade aleatoriedad para que muchos clientes no reintenten a la vez:
import random, time, httpx
def call_with_retry(fn, attempts=5, base=0.5, cap=30):
for i in range(attempts):
try:
r = fn()
if r.status_code == 429:
time.sleep(float(r.headers.get("Retry-After", base * 2 ** i)))
continue
if r.status_code < 500:
return r
except httpx.TransportError:
pass
time.sleep(random.uniform(0, min(cap, base * 2 ** i))) # jitter completo
raise RuntimeError("abandonado tras varios reintentos")
Higiene de los payloads
- Limita el tamaño del cuerpo (1 MB es generoso para eventos de chat).
- Valida la forma con un esquema; ignora campos desconocidos en lugar de fallar.
- Nunca uses IDs del payload para autorizar sin contrastarlos con tus propios registros.
- Registra IDs y tipos de evento, no cuerpos completos con datos personales.
Pruebas en local
Expón tu servidor local con un túnel (cloudflared tunnel --url http://localhost:3000, ngrok o similar), registra la URL temporal como webhook y acuérdate de volver a la de producción. Guarda payloads reales (sin datos personales) como fixtures y reprodúcelos en pruebas unitarias. La mayoría de proveedores tiene además un botón de "reenviar" en su panel.
Llamar bien a las APIs
- Envía un
User-Agentdescriptivo y pon las credenciales en cabeceras, no en la query string. - Fija tiempos de espera en cada petición (conexión y lectura).
- Pagina con cursores y detente cuando la API lo indique.
- Respeta
Retry-Aftery los límites publicados; consulta fiabilidad y límites de tasa.