Webhooks y reintentos
Recibí notificaciones cuando cambia el estado de los comprobantes. Configurá un endpoint HTTPS y elegí eventos desde Integraciones → Webhooks. El secreto de firma se muestra una sola vez y se puede regenerar.
Eventos recomendados
| Evento | Significado |
|---|---|
document.created | Borrador creado. |
document.signed | Se firmó el documento; no equivale a aceptación. |
document.sent | Se registró el envío/recepción del sobre. |
document.accepted | La respuesta fiscal indica aceptación. |
document.observed | DGI observó el comprobante. |
document.rejected | DGI rechazó el comprobante. |
dgi.transport_unknown | Resultado de transmisión incierto: conciliar antes de reenviar. |
Catálogo de eventos disponibles
Además de los eventos resumidos arriba, el backend permite suscribirse a los siguientes eventos según la configuración de la organización. No todos se generan para cada tipo de CFE:
| Nombre del evento | Suscripción |
|---|---|
document.created | Evento específico, document.* o * donde corresponda |
document.number_reserved | Evento específico, document.* o * donde corresponda |
document.ready_for_xml | Evento específico, document.* o * donde corresponda |
document.xml_generated | Evento específico, document.* o * donde corresponda |
document.signed | Evento específico, document.* o * donde corresponda |
document.xsd_validated | Evento específico, document.* o * donde corresponda |
document.xsd_rejected | Evento específico, document.* o * donde corresponda |
document.xsd_pending | Evento específico, document.* o * donde corresponda |
document.xsd_unavailable | Evento específico, document.* o * donde corresponda |
document.enveloped | Evento específico, document.* o * donde corresponda |
document.sent | Evento específico, document.* o * donde corresponda |
document.accepted | Evento específico, document.* o * donde corresponda |
document.observed | Evento específico, document.* o * donde corresponda |
document.rejected | Evento específico, document.* o * donde corresponda |
document.cancelled | Evento específico, document.* o * donde corresponda |
document.number_cancelled | Evento específico, document.* o * donde corresponda |
dgi.envelope_received | Evento específico, document.* o * donde corresponda |
dgi.envelope_rejected | Evento específico, document.* o * donde corresponda |
dgi.envelope_unknown_ack | Evento específico, document.* o * donde corresponda |
dgi.transport_unknown | Evento específico, document.* o * donde corresponda |
dgi.soap_fault | Evento específico, document.* o * donde corresponda |
dgi.cfe_status_checked | Evento específico, document.* o * donde corresponda |
contingency.cfc_registered | Evento específico, document.* o * donde corresponda |
contingency.cfc_signed | Evento específico, document.* o * donde corresponda |
contingency.cfc_enveloped | Evento específico, document.* o * donde corresponda |
interchange.outbound_envelope_created | Evento específico, document.* o * donde corresponda |
interchange.outbound_ws_sent | Evento específico, document.* o * donde corresponda |
interchange.ack_cfe_received | Evento específico, document.* o * donde corresponda |
Formato de entrega real
El webhook contiene id, type, createdAt y data. Los eventos fiscales incluyen data.event y data.document; las pruebas desde el panel generan un evento webhook.test con otro contenido en data.
{
"id": "evt_<identificador>_document.accepted",
"type": "document.accepted",
"createdAt": "2026-10-12T12:00:00.000Z",
"data": {
"event": { "sourceType": "dgi.cfe_status_checked", "status": "ACCEPTED",
"message": null, "payload": {} },
"document": { "id": "<document-id>", "externalId": "pedido-123",
"environment": "TEST", "status": "ACCEPTED",
"documentType": "111", "total": "1220.00" }
}
}Ejemplo ilustrativo de estructura, no es respuesta certificada por DGI. El webhook es una petición HTTP saliente de Factumel: su contrato está definido en webhooks.fiscalEvent.post dentro de OpenAPI 3.1. La administración de endpoints puede realizarse desde el panel o por API REST con una clave organizacional que tenga el permiso webhooks:manage. Las claves restringidas a una empresa no pueden administrar destinos de toda la organización.
Gestionar webhooks desde la API
El secreto de firma aparece sólo al crear el endpoint y al rotarlo. Un GET nunca lo devuelve. El API permite configurar suscripciones con HTTP JSON y proteger los destinos HTTPS contra SSRF.
| Operación | Ruta |
|---|---|
| Listar / Crear | GET /api/v1/webhooks y POST /api/v1/webhooks |
| Detalle / Actualizar / Archivar | GET/PATCH/DELETE /api/v1/webhooks/{id} |
| Rotar secreto | POST /api/v1/webhooks/{id}/rotate-secret |
curl -sS -X POST https://factumel.com/api/v1/webhooks \
-H "Authorization: Bearer $FACTUMEL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://tu-servidor.uy/factumel","events":["document.accepted","document.rejected"]}'Estas operaciones requieren el scope webhooks:manage y una API key de alcance organizacional. Desactivar un endpoint detiene nuevas entregas; archivarlo cancela las pendientes. Antes de rotar el secreto, coordiná el cambio con el receptor: durante la transición puede existir una entrega firmada con el secreto anterior.
Firma de cada entrega
El cuerpo llega en JSON. Los headers x-factumel-timestamp y x-factumel-signature permiten validar integridad. La firma es v1=HMAC_SHA256(secret, timestamp + "." + rawBody) en hexadecimal. Verificá con comparación constante y rechazá timestamps antiguos (por ejemplo, más de cinco minutos).
Ejemplo Node.js: verificación segura
import { createHmac, timingSafeEqual } from "node:crypto";
function verifyFactumelWebhook(rawBody, headers, secret) {
const timestamp = headers["x-factumel-timestamp"];
const received = headers["x-factumel-signature"];
if (!timestamp || !/^v1=[0-9a-f]{64}$/.test(received || "")) return false;
const value = Number(timestamp);
if (!Number.isSafeInteger(value) ||
Math.abs(Date.now() / 1000 - value) > 300) return false;
const expected = createHmac("sha256", secret)
.update(timestamp + "." + rawBody, "utf8")
.digest("hex");
return timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(received.slice(3), "hex"),
);
}
// rawBody: los bytes originales UTF-8, antes de JSON.parse.At-least-once e idempotencia
Un mismo evento puede entregarse más de una vez por fallos de red o reintentos. Guardá x-factumel-event-id o id del payload como clave única antes de aplicar el cambio comercial. Respondé HTTP 2xx sólo después de persistir el procesamiento. Los eventos pueden llegar en distinto orden: consultá el recurso para resolver estados dudosos.
Respondé 2xx sólo después de guardar el evento y el resultado de negocio. Comprobá x-factumel-event-id frente al id del JSON, rechazá discrepancias, almacená IDs procesados con una restricción UNIQUE y nunca aceptes una firma sobre un JSON reserializado.
El dispatcher aplica reintentos con límites y administra entregas fallidas desde el panel. Los destinos se validan contra SSRF, direcciones privadas y protocolos no permitidos. Para integraciones de terceros usá HTTPS público y no expongas el secreto.