Webhooks
La emisión responde 202 al instante y el SRI decide después. Preguntar cada
poco funciona, pero desperdicia llamadas y añade retraso. Con un webhook,
facto te avisa.
Los cuatro eventos
Sección titulada «Los cuatro eventos»comprobante.autorizado el SRI lo autorizócomprobante.no_autorizado el SRI lo evaluó y lo rechazócomprobante.devuelto el SRI lo rechazó en recepcióncomprobante.fallido fallo interno tras agotar los reintentosNo hay eventos para FIRMADO ni ENVIANDO — acabas de recibir el 202, ya
sabes que emitiste. El ruido hace que se dejen de leer los avisos que
importan.
Dar de alta un destino
Sección titulada «Dar de alta un destino»curl -X POST https://api-test.facto.lat/v1/webhooks \ -H "x-api-key: $LLAVE" -H "Content-Type: application/json" \ -d '{"url":"https://mi-sistema.ec/hooks/facto","descripcion":"Sistema contable"}'{ "id": "9151431b-4483-4230-9181-13ff85d7155c", "eventos": ["comprobante.autorizado", "..."], "activo": true, "secreto": "whsec_P1FndZ20He-C2LcH9UxSz1anflK_5zF6gl75qsX6PsU"}El secreto se muestra una sola vez. Guárdalo: no se puede volver a
consultar, solo rotar. Sin eventos, llegan los cuatro; para filtrar, lista
los que quieres.
La URL debe ser https hacia una dirección pública — se valida al alta, para
que un error de configuración aparezca ahora y no en un log tres horas
después.
Pruébalo antes de emitir nada:
curl -X POST https://api-test.facto.lat/v1/webhooks/{id}/probar \ -H "x-api-key: $LLAVE"Manda un evento con la forma exacta de uno real y datos ficticios, solo a ese destino.
Lo que llega
Sección titulada «Lo que llega»POST /hooks/facto HTTP/1.1content-type: application/json; charset=utf-8facto-firma: t=1786493450,v1=6e2f...facto-evento: comprobante.autorizadofacto-entrega: 901640d4-0adc-437b-a183-4723f10f47a3facto-intento: 1{ "id": "e3b0c442-98fc-1c14-9afb-f4c8996fb924", "tipo": "comprobante.autorizado", "ocurridoEn": "2026-08-12T00:10:50.914Z", "comprobante": { "id": "4d981d2d-384a-4d78-949f-64e4c36bea5f", "claveAcceso": "1108202601099999999900110010010000086586719953111", "numeroComprobante": "001-001-000008658", "tipoComprobante": "01", "ruc": "0999999999001", "fechaEmision": "11/08/2026", "importeTotal": "92.00", "estado": "AUTORIZADO", "numeroAutorizacion": "1108202601099999999900110010010000086586719953111", "fechaAutorizacion": "2026-08-12T00:10:44.000Z" }, "mensajes": []}Trae lo suficiente para actuar sin volver a preguntar. El XML no viaja
(pesa hasta 30 KB y se repetiría en cada reintento) — cuando lo necesites:
GET /v1/comprobantes/{id}/xml.
Verifica la firma. Siempre.
Sección titulada «Verifica la firma. Siempre.»Sin verificar, cualquiera que conozca tu URL puede decirte que una factura quedó autorizada.
import { createHmac, timingSafeEqual } from 'node:crypto';
function verificar(cabecera, secreto, cuerpoCrudo) { const t = Number(/t=(\d+)/.exec(cabecera)?.[1] ?? NaN); if (!Number.isFinite(t)) return false;
// Rechaza entregas viejas reproducidas por un tercero. if (Math.abs(Math.floor(Date.now() / 1000) - t) > 300) return false;
const esperada = createHmac('sha256', secreto) .update(`${t}.${cuerpoCrudo}`) .digest('hex');
return [...cabecera.matchAll(/v1=([0-9a-f]+)/g)] .map((m) => m[1]) .some( (firma) => firma.length === esperada.length && timingSafeEqual(Buffer.from(firma), Buffer.from(esperada)), );}Las tres cosas que la gente se salta:
- El cuerpo crudo, no el JSON reserializado.
JSON.parse+JSON.stringifyreordena claves y la firma deja de cuadrar. En Express:express.raw(). - El instante va dentro de lo firmado (
t + "." + cuerpo), para que nadie reproduzca una entrega vieja cambiando la cabecera. - Comparación en tiempo constante (
timingSafeEqual), nunca===.
Garantías, sin rodeos
Sección titulada «Garantías, sin rodeos»- Al menos una vez. Vas a recibir duplicados. Desduplica por
facto-entrega. - Sin orden garantizado. Cada evento trae el estado completo, así que no hace falta.
- Se espera un 2xx en menos de 10 segundos. Encola y contesta
200; procesa después.
Reintentos
Sección titulada «Reintentos»Backoff exponencial con jitter, hasta unas 24 horas. 2xx entrega; 408,
425, 429, 5xx y fallos de red se reintentan; el resto de 4xx no — un
400 no mejora insistiendo.
Tras 20 fallos seguidos el destino se desactiva. Para reactivarlo:
PATCH /v1/webhooks/{id} con {"activo": true}.
Cuando algo no llega
Sección titulada «Cuando algo no llega»curl -s https://api-test.facto.lat/v1/webhooks/{id}/entregas -H "x-api-key: $LLAVE"Devuelve lo que se envió y lo que contestaste: código, cuerpo y duración.
Convierte «no me llegó tu webhook» en una consulta en vez de una discusión.
Cualquier entrega, incluso agotada, se reintenta con
POST /v1/webhooks/entregas/{entregaId}/reintentar.
Rotar el secreto
Sección titulada «Rotar el secreto»curl -X POST https://api-test.facto.lat/v1/webhooks/{id}/rotar-secreto -H "x-api-key: $LLAVE"Durante 24 horas las entregas llevan dos firmas —la nueva y la anterior— y tu verificación acepta cualquiera. Cambias el secreto cuando te venga bien, sin ventana de caída.