Webhooks
Los webhooks permiten que Host Factura notifique a tu sistema cuando ocurren eventos relevantes (CFE
emitidos, reportes diarios, alertas de CAE) sin que tengas que hacer polling. Tu servidor recibe un
POST con el payload firmado; vos respondés 2xx en menos de 10 segundos.
Suscripciones
Sección titulada «Suscripciones»Gestioná tus suscripciones desde el panel de Host Factura (Configuración → Webhooks). La gestión de suscripciones es funcionalidad del panel web (sesión de usuario) y no está expuesta en la API pública v1. Cada suscripción define:
| Campo | Descripción |
|---|---|
url | URL de destino. HTTPS obligatorio. Máx. 2048 caracteres. |
eventos | Lista de tipos de evento a recibir (al menos uno). Ver Eventos disponibles. |
sucursalId | null = todos los eventos de la cuenta. UUID = solo eventos de esa sucursal. |
descripcion | Etiqueta libre (máx. 255 caracteres). |
activo | true / false. Una suscripción inactiva no recibe entregas. |
incluirMetadata | true / false (default false). Adjunta los metadatos personalizados del CFE en el evento cfe.emitido. Ver Metadatos en el evento cfe.emitido. |
Límite: máximo 20 suscripciones activas por cuenta.
Estructura del evento (envelope)
Sección titulada «Estructura del evento (envelope)»Todos los eventos comparten el mismo envelope:
{ "id": "019612e4-...", "tipo": "cfe.emitido", "creadoEn": "2025-11-14T10:30:00.000-03:00", "cuentaId": "uuid-de-la-cuenta", "sucursalId": "uuid-de-la-sucursal", "datos": { ... }}| Campo | Tipo | Descripción |
|---|---|---|
id | UUID v7 | Identificador estable del evento; igual en todos los reintentos de la misma entrega. |
tipo | string | Tipo de evento (ver tabla de eventos). |
creadoEn | ISO 8601 | Timestamp con offset America/Montevideo. |
cuentaId | UUID | Cuenta que originó el evento. |
sucursalId | UUID | null | Sucursal del evento, o null para eventos de cuenta entera. |
datos | object | Payload específico según el tipo de evento. |
Eventos disponibles
Sección titulada «Eventos disponibles»CFE emitido
Sección titulada «CFE emitido»Tipo: cfe.emitido — Se dispara cuando un CFE es emitido exitosamente.
{ "cfeId": "uuid", "tipo": 101, "serie": "A", "numero": 1234, "estadoDgi": "AC", "codigoSucursalDgi": 1}codigoSucursalDgi es el código de sucursal ante DGI (CdgDGISucur); no confundir con sucursalId del
envelope, que es el UUID interno de Host Factura.
Metadatos en el evento cfe.emitido
Sección titulada «Metadatos en el evento cfe.emitido»Si tu suscripción tiene incluirMetadata: true, el payload de cfe.emitido (y solo ese evento —
cfe.actualizado nunca lo incluye) agrega el campo metadata con los
metadatos personalizados capturados al emitir:
{ "cfeId": "uuid", "tipo": 101, "serie": "A", "numero": 1234, "estadoDgi": "AC", "codigoSucursalDgi": 1, "metadata": { "numero_transaccion": "TXN-8827461", "metodo_pago": "tarjeta", "referencia_externa": "sub_9F2K1" }}CFE actualizado
Sección titulada «CFE actualizado»Tipo: cfe.actualizado — Se dispara cuando cambia el estado de un CFE ante DGI (por ejemplo, cuando
llega la respuesta definitiva de aceptación o rechazo).
{ "cfeId": "uuid", "tipo": 101, "serie": "A", "numero": 1234, "estadoDgi": "AC", "codigoSucursalDgi": 1, "estado": "aceptado", "estadoSobre": "...", "motivoDgi": null}Campo estado | Significado |
|---|---|
aceptado | DGI aceptó el CFE. |
rechazado | DGI lo rechazó (ver motivoDgi). |
observado | DGI lo observó (ver motivoDgi). |
pendiente | Aceptado localmente, respuesta definitiva de DGI pendiente. |
CFE recibido creado
Sección titulada «CFE recibido creado»Tipo: cfe_recibido.creado — Se dispara cuando se registra un CFE recibido de otro emisor (intercambio electrónico).
{ "cfeRecibidoId": "uuid", "tipo": 101, "serie": "A", "numero": 5000, "rutEmisor": "21234567890", "moneda": "UYU", "total": 1220.00, "pendienteAceptacion": true, "aceptadoComercial": null, "estadoDgi": "AC", "anulado": false}CFE recibido actualizado
Sección titulada «CFE recibido actualizado»Tipo: cfe_recibido.actualizado — Se dispara cuando cambia el estado comercial o de DGI de un CFE recibido.
{ "cfeRecibidoId": "uuid", "tipo": 101, "serie": "A", "numero": 5000, "rutEmisor": "21234567890", "moneda": "UYU", "total": 1220.00, "pendienteAceptacion": false, "aceptadoComercial": true, "estadoDgi": "AC", "anulado": false, "tipoActualizacion": "comercial"}Campo tipoActualizacion | Cuándo se produce |
|---|---|
"comercial" | El receptor aceptó o rechazó el CFE comercialmente. |
"dgi" | DGI actualizó el estado del CFE recibido. |
Reporte diario
Sección titulada «Reporte diario»Tipos: reporte_diario.aceptado, reporte_diario.rechazado, reporte_diario.error
{ "reporteDiarioId": "uuid", "fecha": "2025-11-14", "estado": "aceptado", "nombreArchivo": "reporte_20251114.xml"}Alertas operativas
Sección titulada «Alertas operativas»CAE por vencer
Sección titulada «CAE por vencer»Tipo: cae.por_vencer — Se dispara cuando un CAE está próximo a su fecha de vencimiento.
{ "caeId": "uuid", "tipoCfe": 101, "serie": "A", "numeroDesde": 1, "numeroHasta": 999, "fechaVencimiento": "2026-01-31", "diasRestantes": 15}CAE por agotarse
Sección titulada «CAE por agotarse»Tipo: cae.por_agotarse — Se dispara cuando el stock de numeración de un CAE está casi agotado.
{ "caeId": "uuid", "tipoCfe": 101, "serie": "A", "numeroDesde": 1, "numeroHasta": 999, "porcentajeUsado": 92.5, "restantes": 75}Certificado por vencer
Sección titulada «Certificado por vencer»Tipo: certificado.por_vencer — Se dispara cuando el certificado digital (CVA) está próximo a vencer.
{ "cvaId": "uuid", "ruc": "21234567890", "fechaVencimiento": "2026-03-15", "diasRestantes": 30}Recurrente emitido
Sección titulada «Recurrente emitido»Tipo: recurrente.emitido — Se dispara cuando se emite automáticamente un CFE de una plantilla
recurrente.
{ "cfeId": "uuid", "tipo": 101, "serie": "A", "numero": 1234}Headers de la entrega
Sección titulada «Headers de la entrega»Cada POST que Host Factura envía a tu endpoint incluye los siguientes headers:
| Header | Descripción |
|---|---|
Content-Type | application/json |
User-Agent | HostFactura-Webhooks/1.0 |
X-HostFactura-Event | Tipo del evento (ej. cfe.emitido). |
X-HostFactura-Event-Id | UUID del evento (estable entre reintentos). |
X-HostFactura-Delivery | UUID único del intento de entrega. |
X-HostFactura-Attempt | Número de intento (empieza en 1). |
X-HostFactura-Signature | Firma HMAC-SHA256 del cuerpo. Ver Verificar la firma. |
Verificar la firma
Sección titulada «Verificar la firma»Cada entrega incluye el header X-HostFactura-Signature con el formato:
t=1731574200,v1=a3f8c...Donde t es un timestamp Unix (segundos) y v1 es el HMAC-SHA256 en hexadecimal.
Para verificar:
- Extraé el timestamp
ty el valorv1del header. - Armá el string a firmar:
"<t>.<cuerpo raw del POST>". - Calculá el HMAC-SHA256 con tu
secret(en base64url) sobre ese string. - Comparalo con
v1usando comparación en tiempo constante. - Verificá que el timestamp no sea más antiguo de 5 minutos (protección contra replay).
import { createHmac, timingSafeEqual } from "node:crypto";
function verifyWebhook(rawBody, signatureHeader, secret) { if (!signatureHeader?.includes(",")) return false; const [tPart, v1Part] = signatureHeader.split(","); if (!tPart?.startsWith("t=") || !v1Part?.startsWith("v1=")) return false;
const timestamp = tPart.slice(2); const expected = v1Part.slice(3);
const hmac = createHmac("sha256", Buffer.from(secret, "base64url")) .update(`${timestamp}.${rawBody}`) .digest("hex");
const hmacBuf = Buffer.from(hmac, "hex"); const expectedBuf = Buffer.from(expected, "hex"); if (hmacBuf.length !== expectedBuf.length) return false; const isValid = timingSafeEqual(hmacBuf, expectedBuf);
const ts = parseInt(timestamp, 10); if (isNaN(ts)) return false; const age = Math.floor(Date.now() / 1000) - ts; return isValid && age >= 0 && age < 300; // rechaza futuros y más de 5 min pasados}Reintentos y backoff
Sección titulada «Reintentos y backoff»Si tu endpoint no responde 2xx en 10 segundos, o devuelve un error HTTP, Host Factura reintenta con
backoff exponencial:
| Intento | Espera aprox. |
|---|---|
| 1 | 5 segundos |
| 2 | 30 segundos |
| 3 | 5 minutos |
| 4 | 30 minutos |
| 5 | 2 horas |
| 6 | 12 horas |
Después del intento 6, la entrega queda en estado DEAD y no se reintenta automáticamente. Podés reintentarla manualmente desde el panel (Configuración → Webhooks → Deliveries).
Probar un webhook
Sección titulada «Probar un webhook»Desde el panel, en el detalle de una suscripción, podés enviar un evento de prueba (webhook.test)
para verificar que tu endpoint recibe y procesa correctamente las entregas antes de que ocurran eventos
reales.
El payload de prueba tiene la misma estructura de envelope que los eventos reales pero con datos mínimos.
Consideraciones de seguridad
Sección titulada «Consideraciones de seguridad»- Solo HTTPS. No se aceptan URLs con protocolo
http://. - Sin redirecciones. Host Factura no sigue redirects; si tu URL redirige, la entrega falla.
- Timeout de 10 s. Tu endpoint debe responder en menos de 10 segundos. Procesá el evento de forma
asíncrona si necesitás más tiempo; respondé
200de inmediato y encolá el trabajo. - IPs privadas bloqueadas. No podés registrar URLs que resuelvan a rangos RFC 1918 (10.x, 172.16-31.x, 192.168.x), loopback ni metadata de nube (169.254.169.254).
La gestión de suscripciones (crear, listar, actualizar, eliminar y ver deliveries) se hace desde el panel de Host Factura (Configuración → Webhooks). No es parte de la API pública v1 documentada en la Referencia OpenAPI.