Ir al contenido

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.

comprobante.autorizado el SRI lo autorizó
comprobante.no_autorizado el SRI lo evaluó y lo rechazó
comprobante.devuelto el SRI lo rechazó en recepción
comprobante.fallido fallo interno tras agotar los reintentos

No 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.

Ventana de terminal
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:

Ventana de terminal
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.

POST /hooks/facto HTTP/1.1
content-type: application/json; charset=utf-8
facto-firma: t=1786493450,v1=6e2f...
facto-evento: comprobante.autorizado
facto-entrega: 901640d4-0adc-437b-a183-4723f10f47a3
facto-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.

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:

  1. El cuerpo crudo, no el JSON reserializado. JSON.parse + JSON.stringify reordena claves y la firma deja de cuadrar. En Express: express.raw().
  2. El instante va dentro de lo firmado (t + "." + cuerpo), para que nadie reproduzca una entrega vieja cambiando la cabecera.
  3. Comparación en tiempo constante (timingSafeEqual), nunca ===.
  • 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.

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}.

Ventana de terminal
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.

Ventana de terminal
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.