Docs / API Reference / Webhooks
API Reference

Webhooks

Recibe notificaciones push cuando ocurren eventos en tu organización. Cada entrega se firma con HMAC-SHA256 y se reintenta con backoff exponencial hasta que confirmes con un 2xx.

Suscripciones

Una suscripción es la asociación entre un evento y un endpoint tuyo. Un endpoint puede estar suscrito a varios eventos; un evento puede entregarse a varios endpoints.

Listar suscripciones

GET /api/v1/webhooks — Endpoints suscritos a eventos

Suscribir un endpoint

POST /api/v1/webhooks — Crea una nueva suscripción

Body

Parámetro Tipo Descripción
nombre required string Etiqueta legible de la suscripción. Máx 120 caracteres.
url required string URL pública HTTPS a la que enviaremos el POST (http no se acepta).
eventos optional string-array Eventos a suscribir. Si se omite (o va vacío), recibes TODOS.
secret optional string Signing secret propio. Si se omite, generamos uno y lo mostramos una sola vez.
activo optional boolean Default: true.
curl -X POST https://api.emitoo.io/api/v1/webhooks/ \
  -H "X-API-Key: $EMITOO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "nombre": "Produccion - app principal",
    "url": "https://tu-app.com/webhooks/emitoo",
    "eventos": ["factura.autorizada", "factura.rechazada"]
  }'

Los eventos siguen el patrón {documento}.{verbo} para los tres comprobantes: factura, guia y retencion.

Evento Cuándo se dispara
factura.creadaRecibimos el comprobante (estado PENDIENTE).
factura.autorizadaEl SRI autorizó el comprobante.
factura.rechazadaEl SRI lo procesó y lo marcó NO AUTORIZADO. Revisa data.estado_sri.
factura.devueltaEl SRI no lo aceptó en recepción (XML/firma/esquema). Revisa data.estado_sri.
guia.*Mismos verbos para guías de remisión: guia.creada, guia.autorizada, guia.rechazada, guia.devuelta.
retencion.*Mismos verbos para retenciones: retencion.creada, retencion.autorizada, retencion.rechazada, retencion.devuelta.

Formato del payload

Cada POST trae un JSON con evento (nombre del evento) y data (los datos del comprobante). Los campos de data son un superset común a los tres comprobantes: para una factura vienen razon_social/identificacion/email; para una guía, transportista_identificacion/placa; para una retención, sujeto_identificacion/periodo_fiscal. Los que no apliquen llegan en null.

// POST https://tu-app.com/webhooks/emitoo
// X-Emitoo-Signature: t=1751558400,v1=4f3b…
// X-Emitoo-Event:     factura.autorizada
// X-Emitoo-Delivery:  dlv_9f2c8a1b4d6e0f3a2b7c5d1e

{
  "evento": "factura.autorizada",
  "data": {
    "id": 177,
    "numero_comprobante": "001-001-000000177",
    "estado": "AUTORIZADO",
    "clave_acceso": "0307202601099999999900110010010000001771234567818",
    "numero_autorizacion": "0307202601099999999900110010010000001771234567818",
    "fecha_autorizacion": "2026-07-03T15:12:48",
    "fecha_emision": "2026-07-03T00:00:00",
    "razon_social": "COMERCIAL EL BUEN PRECIO S.A.",
    "identificacion": "0991234567001",
    "email": "cliente@ejemplo.com",
    "estado_sri": "[AUTORIZADO]"
  }
}

Firma y verificación

Cada entrega incluye un header X-Emitoo-Signature con el formato t={timestamp},v1={hmac}. El hmac se calcula sobre {timestamp}.{rawBody} usando tu signing secret como clave. Los SDKs oficiales traen el helper listo — verifica con una llamada, siempre sobre el body crudo (sin parsear):

import { verificarFirma } from "emitoo";

// Express: usa express.raw() para conservar el body crudo
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,
  });
  if (!valida) return res.status(401).end();

  const evento = JSON.parse(req.body.toString());
  res.sendStatus(200);
});

Anti-replay: los SDKs lo hacen solos

El helper compara en tiempo constante y rechaza entregas cuyo t difiera más de 5 minutos de tu reloj (configurable). Si verificas a mano en otro lenguaje, aplica la misma regla para evitar que un atacante reuse una firma capturada.

Reintentos y orden de entrega

Consideramos exitosa una entrega cuando respondes un 2xx dentro de 15 segundos. Si no, reintentamos con backoff — y de forma durable: el estado de cada entrega vive en nuestra base de datos, así que sobrevive reinicios nuestros y caídas prolongadas de tu servidor (no se pierde en un hilo en memoria).

  • Intento inmediato
  • Reintento 1: +30 s
  • Reintento 2: +5 min
  • Reintento 3: +30 min
  • Reintento 4: +2 h
  • Reintento 5: +12 h

Los reintentos son byte-idénticos: mismo cuerpo y mismo X-Emitoo-Delivery (la firma se recalcula con un timestamp fresco). Tras agotar los intentos, la entrega queda en estado fallida (visible en Webhooks → [suscripción] → Entregas) y puedes reintentarla manualmente desde el dashboard. La entrega es at-least-once y el orden entre eventos no está garantizado — deduplica con X-Emitoo-Delivery.

Solo HTTPS

La URL de un webhook debe ser https:// (validamos el esquema y que resuelva a una IP pública, al crear la suscripción y antes de cada entrega). El payload lleva datos del comprobante, así que no lo enviamos por http en claro.

Reconciliación (pull): no pierdas eventos nunca

Además del push por webhook, cada evento queda registrado y puedes traerlos tú — útil si tu sistema estuvo caído o quieres una verificación de respaldo. Pagina hacia adelante con el cursor desde_id: guarda el id del último evento que procesaste y pide los siguientes.

GET /api/v1/eventos?desde_id={ultimo_id} — Eventos de tu empresa posteriores a un id (orden ascendente)

# Trae los eventos nuevos desde el último que procesaste
curl -H "X-API-Key: $EMITOO_API_KEY" \
  "https://api.emitoo.io/api/v1/eventos?desde_id=482&limit=100"

Guarda el id más alto que recibas y úsalo como desde_id en la siguiente llamada. Así, aunque un webhook se pierda, tu sistema puede reconciliar por su cuenta y quedar siempre al día.