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"]
}'Catálogo de eventos
Los eventos siguen el patrón {documento}.{verbo}
para los tres comprobantes: factura, guia y
retencion.
| Evento | Cuándo se dispara |
|---|---|
factura.creada | Recibimos el comprobante (estado PENDIENTE). |
factura.autorizada | El SRI autorizó el comprobante. |
factura.rechazada | El SRI lo procesó y lo marcó NO AUTORIZADO. Revisa data.estado_sri. |
factura.devuelta | El 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);
});from emitoo import verificar_firma
# FastAPI: request.body() es el cuerpo crudo
@app.post("/webhooks/emitoo")
async def webhook_emitoo(request: Request):
body = await request.body()
if not verificar_firma(
request.headers.get("X-Emitoo-Signature"),
body,
os.environ["EMITOO_WEBHOOK_SECRET"],
):
raise HTTPException(401, "firma inválida")
evento = json.loads(body)
return {"ok": True}import "github.com/Killkanacode/emitoo-go"
func webhookEmitoo(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body) // cuerpo crudo
if !emitoo.VerificarFirma(r.Header.Get("X-Emitoo-Signature"), body, os.Getenv("EMITOO_WEBHOOK_SECRET")) {
http.Error(w, "firma inválida", http.StatusUnauthorized)
return
}
var evento map[string]any
_ = json.Unmarshal(body, &evento)
w.WriteHeader(http.StatusOK)
}Anti-replay: los SDKs lo hacen solos
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
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.