Docs / Conceptos / Webhooks firmados
Concepto

Webhooks firmados

Cada entrega de webhook viene firmada con HMAC-SHA256 sobre el cuerpo crudo, usando tu signing secret como clave. Verificar la firma es la única forma de estar seguro de que el POST viene de Emitoo y no de un atacante que descubrió tu URL.

Cabeceras que recibirás

Header Descripción
X-Emitoo-Signature t={timestamp},v1={hmac-hex}. La firma que debes verificar.
X-Emitoo-Event Tipo de evento. Ej: factura.autorizada.
X-Emitoo-Delivery ID único de la entrega. Úsalo como idempotency key.
User-Agent Emitoo-Webhooks/1.0.

Algoritmo de verificación

Los SDKs oficiales lo implementan por ti: verificarFirma (TypeScript), verificar_firma (Python) y VerificarFirma (Go). Esto es lo que hace el helper — y lo que debes implementar si usas otro lenguaje:

  1. Lee el header X-Emitoo-Signature. Sepáralo por coma en dos partes: t y v1.
  2. Lee el cuerpo crudo (sin parsear JSON antes — la firma es sobre los bytes exactos).
  3. Calcula HMAC-SHA256(signing_secret, "{t}.{rawBody}").
  4. Compara con timingSafeEqual el resultado contra el v1 recibido.
  5. Verifica que t no tenga más de 5 minutos de antigüedad (anti-replay).
  6. Si todo OK, procesa y responde 2xx rápido.
import { verificarFirma } from "emitoo";

// Express: conserva el body crudo con express.raw()
app.post("/webhooks/emitoo", express.raw({ type: "*/*" }), async (req, res) => {
  const valida = await verificarFirma({
    header: req.headers["x-emitoo-signature"],
    body: req.body, // crudo, sin JSON.parse
    secret: process.env.EMITOO_WEBHOOK_SECRET,
    // toleranciaSegundos: 300 (default; 0 = sin anti-replay)
  });
  if (!valida) return res.status(401).end();

  const evento = JSON.parse(req.body.toString());
  // ... tu lógica ...
  res.sendStatus(204);
});

El cuerpo crudo importa

Si tu framework re-serializa el JSON antes de pasarlo a la verificación, los espacios y el orden de claves cambiarán y la firma no coincidirá. Guarda el body crudo (Express: express.raw(), FastAPI: request.body(), Flask: request.get_data()).

Dónde encuentro el signing secret

Cada suscripción a un webhook tiene su propio secret. Lo encuentras en el dashboard bajo Webhooks → [suscripción] → Signing secret. Puedes rotarlo en cualquier momento; las entregas nuevas se firman con el nuevo secret y las firmas viejas dejan de validarse.

Anti-replay

El timestamp dentro de la firma te protege contra replays: si un atacante captura un POST válido y lo reenvía 10 minutos después, tu verificación falla porque el timestamp es viejo. Recomendamos un margen de 5 minutos para absorber drift de relojes.

Idempotencia con X-Emitoo-Delivery

Si el sistema reintenta una entrega (por ejemplo, tu endpoint tardó más de 15 s en responder o devolvió un error), recibirás el mismo evento con el mismo X-Emitoo-Delivery en todos los reintentos. Guarda los delivery IDs procesados para descartar duplicados.