Saltearse al contenido

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.

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:

CampoDescripción
urlURL de destino. HTTPS obligatorio. Máx. 2048 caracteres.
eventosLista de tipos de evento a recibir (al menos uno). Ver Eventos disponibles.
sucursalIdnull = todos los eventos de la cuenta. UUID = solo eventos de esa sucursal.
descripcionEtiqueta libre (máx. 255 caracteres).
activotrue / false. Una suscripción inactiva no recibe entregas.
incluirMetadatatrue / 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.

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": { ... }
}
CampoTipoDescripción
idUUID v7Identificador estable del evento; igual en todos los reintentos de la misma entrega.
tipostringTipo de evento (ver tabla de eventos).
creadoEnISO 8601Timestamp con offset America/Montevideo.
cuentaIdUUIDCuenta que originó el evento.
sucursalIdUUID | nullSucursal del evento, o null para eventos de cuenta entera.
datosobjectPayload específico según el tipo de evento.

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.

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

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 estadoSignificado
aceptadoDGI aceptó el CFE.
rechazadoDGI lo rechazó (ver motivoDgi).
observadoDGI lo observó (ver motivoDgi).
pendienteAceptado localmente, respuesta definitiva de DGI pendiente.

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
}

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 tipoActualizacionCuándo se produce
"comercial"El receptor aceptó o rechazó el CFE comercialmente.
"dgi"DGI actualizó el estado del CFE recibido.

Tipos: reporte_diario.aceptado, reporte_diario.rechazado, reporte_diario.error

{
"reporteDiarioId": "uuid",
"fecha": "2025-11-14",
"estado": "aceptado",
"nombreArchivo": "reporte_20251114.xml"
}

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
}

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
}

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
}

Tipo: recurrente.emitido — Se dispara cuando se emite automáticamente un CFE de una plantilla recurrente.

{
"cfeId": "uuid",
"tipo": 101,
"serie": "A",
"numero": 1234
}

Cada POST que Host Factura envía a tu endpoint incluye los siguientes headers:

HeaderDescripción
Content-Typeapplication/json
User-AgentHostFactura-Webhooks/1.0
X-HostFactura-EventTipo del evento (ej. cfe.emitido).
X-HostFactura-Event-IdUUID del evento (estable entre reintentos).
X-HostFactura-DeliveryUUID único del intento de entrega.
X-HostFactura-AttemptNúmero de intento (empieza en 1).
X-HostFactura-SignatureFirma HMAC-SHA256 del cuerpo. Ver 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:

  1. Extraé el timestamp t y el valor v1 del header.
  2. Armá el string a firmar: "<t>.<cuerpo raw del POST>".
  3. Calculá el HMAC-SHA256 con tu secret (en base64url) sobre ese string.
  4. Comparalo con v1 usando comparación en tiempo constante.
  5. 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
}

Si tu endpoint no responde 2xx en 10 segundos, o devuelve un error HTTP, Host Factura reintenta con backoff exponencial:

IntentoEspera aprox.
15 segundos
230 segundos
35 minutos
430 minutos
52 horas
612 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).

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.

  • 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é 200 de 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.