Para desarrolladores
Conectá tu sistema a fluxa.
Una API REST para que tu punto de venta, tu ERP o tu aplicación propia consulte y prepare comprobantes contra fluxa.
El reparto
Quién hace qué.
Tu sistema le habla a fluxa. El comprobante lo arma, lo firma con Firma Electrónica Avanzada y lo transmite a DGI el Proveedor Habilitado inscripto en el registro de DGI con el que vamos a operar: ese tramo no lo tocás vos, y tampoco lo hacemos nosotros.
Del lado tuyo hay una sola superficie. Todo cuelga de /api/v1/, versionado
en la propia dirección. La empresa por la que estás operando va en la ruta —/api/v1/empresas/{empresaId}/…
— así que nunca hay ambigüedad sobre a nombre de quién estás pidiendo algo. Un
CFE es eso: la factura o el ticket
en su forma electrónica, la que vale ante DGI.
Autenticación
Una credencial, en cada pedido.
Te autenticás con una credencial propia de tu aplicación, que mandás en cada request en
el header Authorization: Bearer. No hay danza de tokens, ni login, ni
refresh: es un valor fijo que guardás en tu configuración y usás siempre.
- fct_…
- La credencial arranca con un prefijo reconocible y dice en su propio texto si es de producción o de prueba. Eso la hace fácil de detectar con un escáner de secretos si alguna vez se te escapa a un repositorio.
- Se muestra una sola vez
- El secreto se ve en claro cuando se emite y no hay forma de recuperarlo después: en la base queda sólo su hash SHA-256, que es contra lo que se verifica cada request.
- Producción y prueba
-
Las credenciales de prueba son de sólo lectura por diseño. Si intentás cualquier
escritura con una, la respuesta es
403 CREDENCIAL_TEST_SOLO_LECTURA— no depende de que te hayas olvidado un permiso. - GET /api/v1/ping
- Confirma que tu credencial es válida y que la plataforma responde, sin tocar datos de ninguna empresa. Sirve como chequeo de arranque de tu integración.
Descubrimiento · ejemplo recortado
GET /api/v1/empresas
Authorization: Bearer fct_live_…
200 OK
{
"data": [
{
"empresa_id": "9f1c8b0e-…",
"ruc": "219900450019",
"razon_social": "Ferretería Aguiar",
"scopes": ["empresa:read", "cfe:read", "catalogos:read"]
}
]
}
Permisos
El permiso vive en la conexión, no en la credencial.
Una misma aplicación puede estar conectada a varias empresas, y cada conexión tiene sus propios permisos. No están adentro de la credencial: están en el vínculo entre tu aplicación y esa empresa, y se revalidan en cada request. Son seis y se conceden por separado.
| Permiso | Habilita |
|---|---|
empresa:read |
La identidad del emisor: RUT y razón social. |
cfe:read |
Los comprobantes: listado, detalle y feed de eventos. |
catalogos:read |
Los catálogos de artículos y de clientes. |
cfe:write |
Preparar y cancelar borradores. |
cfe:emit |
Dar la orden de emisión. |
clientes:write |
Dar de alta un receptor. Exige además catalogos:read, porque la
respuesta devuelve la ficha fiscal completa del cliente.
|
Poder preparar un borrador no te habilita a emitirlo: son autorizaciones distintas.
Yclientes:write no se puede otorgar solo — al crear o editar la conexión,
la plataforma lo rechaza con 422 SCOPE_DEPENDENCIA_FALTANTE antes de
guardar nada.
Lo que ningún permiso alcanza
Hay dos cosas que ninguna aplicación puede pedir porque el permiso no existe: el certificado digital de la empresa y su numeración autorizada por DGI. Tampoco se llega a:
- usuarios y contadores de la empresa;
- la configuración del emisor;
- el panel de administración.
Lectura
Qué podés leer.
-
Tus empresas.
GET /api/v1/empresasdevuelve las empresas que tu credencial puede operar, cada una con su RUT, su razón social y los permisos concedidos. No hace falta cablear nada a mano. -
La identidad del emisor. Antes de facturar contra una empresa podés
pedir
/api/v1/empresas/{empresaId}y comparar RUT y razón social con lo que tenés cargado de tu lado. Es un chequeo barato contra el error de conectar la empresa equivocada. -
El listado de comprobantes.
/api/v1/empresas/{empresaId}/cfefiltra por estado, tipo, cliente, moneda, serie, número y rango de fechas, y puede traer las líneas embebidas en la misma respuesta. -
El detalle.
/api/v1/empresas/{empresaId}/cfe/{cfeId}da líneas, receptor, montos, moneda, la referencia al comprobante original cuando es una nota de crédito o débito, y el bloque de retención cuando es un resguardo. Es una vista curada: no expone plomería interna ni datos del proveedor. -
Los catálogos.
/api/v1/empresas/{empresaId}/articuloscon código interno, GTIN, unidad y precio;/api/v1/empresas/{empresaId}/clientescon documento, nombre y contacto. Y podés resolver de a uno: el artículo por su código interno exacto, el cliente por tipo y número de documento.
Paginación y montos
Los listados paginan por cursor, no por número de página: pedís, guardás elnext_cursor
que te devuelve y seguís desde ahí. Cada página trae hasta 200 filas — 50 si no pedís
otra cosa — y un has_more que te dice si queda más.
Todos los importes viajan como texto decimal, nunca como número de punto flotante, para que no haya redondeos raros en el camino. Los enteros de verdad —el número de comprobante, la posición del feed— sí viajan como número.
Sincronización
Cómo te enterás de que DGI respondió.
La respuesta de DGI no llega en el mismo momento en que sale el comprobante. Para
enterarte no consultás comprobante por comprobante: hay un feed incremental de eventos
en/api/v1/empresas/{empresaId}/cfe/eventos. Guardás un número de posición
—el seq—, lo mandás en el siguiente pedido y recibís sólo lo que cambió
desde ahí.
Feed de eventos · ejemplo recortado
GET /api/v1/empresas/9f1c8b0e-…/cfe/eventos?desde_seq=4187&limit=50
Authorization: Bearer fct_live_…
200 OK
{
"data": [
{
"seq": 4187,
"tipo": "ACEPTADO_DGI",
"cfe_id": "3ac07d92-…",
"estado_anterior": "ENVIADO_PROVEEDOR",
"estado_nuevo": "ACEPTADO_DGI",
"cfe": {
"tipo": "E_TICKET",
"serie": "A",
"numero": 1204,
"fecha_emision": "2026-07-29",
"moneda": "UYU",
"monto_total": "12480.00",
"receptor": null
},
"created_at": "2026-07-29T14:02:11.318Z"
}
],
"pagination": { "next_seq": 4188, "has_more": false }
}
Cada uno de esos hechos llega como un evento aparte. Los estados intermedios de preparación no generan ruido en el feed.
Las cuatro reglas del feed
- El evento no miente. Se escribe en la misma transacción que el cambio de estado que lo produjo. Si el cambio no quedó firme, el evento no existe: no vas a recibir avisos de cosas que no pasaron.
-
Entrega al-menos-una-vez. Podés recibir el mismo evento dos veces y
tenés que descartar duplicados por su
seq. A cambio, no se pierde nada si tu proceso se cae en el medio. - Viene con el resumen adentro. Cada evento trae tipo, serie, número, fecha, moneda, monto total, receptor, referencia y retención, para que no tengas que pedir el detalle de a uno detrás de cada aviso.
- Retardo deliberado. El feed sirve los eventos unos diez segundos después: es para no saltearse un evento que confirma un instante más tarde. Preferimos que llegue completo y ordenado antes que instantáneo.
Doce meses de historia. Si volvés después de una pausa larga con una
posición más vieja que eso, recibís 410 CURSOR_VENCIDO y te resincronizás
desde el listado. Nunca quedás con un hueco silencioso.
Si la empresa queda suspendida, tu integración recibe un código propio y distinguible
(403 EMPRESA_SUSPENDIDA). Es una pausa, no un final: cuando se reactiva, tu
cursor retoma donde estaba sin perder eventos.
Escritura
Preparar y emitir son dos pasos.
Escribir tiene dos pasos separados a propósito. Primero creás un borrador: eso todavía no es un hecho fiscal. Después das la orden explícita de emitir, que sí lo es. Por eso son permisos distintos.
- Resolvés catálogos. Buscás el artículo por su código interno exacto y te quedás con su identificador.
-
Das de alta el receptor, si hace falta.
/api/v1/empresas/{empresaId}/clientescrea o recupera por RUT en una sola llamada: si no existe lo creamos (201,created: true) y si ya existía te devolvemos el que hay (200,created: false), sin pisarle el nombre ni el contacto aunque los mandes distintos. El RUT se valida y se normaliza con las mismas reglas que la pantalla de clientes: si el dígito verificador no cierra, la respuesta es400 CLIENTE_RUC_INVALIDOantes de crear nada. -
Creás el borrador con una
Idempotency-Key. Mandás el artículo y la cantidad, y fluxa congela descripción, precio, IVA, código y unidad desde la ficha. Vos no mandás importes ni tasas. Cada línea lleva unareferencia_externatuya, obligatoria: un texto corto con el que identificás esa línea en tu sistema, que vuelve tal cual en las lecturas. - Revisás lo que quedó armado: montos, receptor y líneas ya vienen en la respuesta.
-
Ordenás la emisión.
/api/v1/empresas/{empresaId}/cfe/{cfeId}/emitirno lleva cuerpo ni key, y sólo aplica a un borrador creado por tu propia aplicación. Te respondemos202con el comprobante y un identificador de trabajo; el resultado fiscal llega después por el feed.
Crear el borrador · ejemplo recortado
POST /api/v1/empresas/9f1c8b0e-…/cfe
Authorization: Bearer fct_live_…
Idempotency-Key: 6b3f2d10-8e5a-4c77-9d21-0f4b8e2a1c33
Content-Type: application/json
{
"tipo": "E_TICKET",
"fecha_emision": "2026-07-29",
"moneda": "UYU",
"forma_pago": "CONTADO",
"lineas": [
{
"articulo_id": "b21e4f6a-…",
"cantidad": "2",
"referencia_externa": "venta-8841-linea-1"
}
]
}
201 Created
{
"data": {
"cfe": {
"id": "3ac07d92-…",
"estado": "BORRADOR",
"moneda": "UYU",
"monto_neto": "10229.51",
"monto_iva": "2250.49",
"monto_total": "12480.00"
},
"idempotency_replayed": false
}
}
Dar la orden de emitir · ejemplo recortado
POST /api/v1/empresas/9f1c8b0e-…/cfe/3ac07d92-…/emitir
Authorization: Bearer fct_live_…
202 Accepted
{
"data": {
"cfe": { "id": "3ac07d92-…", "estado": "EN_COLA" },
"job_id": "emision-3ac07d92-…"
}
}
Un borrador creado por tu aplicación queda bajo tu control: la interfaz web no lo edita, no lo borra ni lo emite por su cuenta. No hay dos manos operando el mismo documento.
Idempotencia y cancelación.
La Idempotency-Key es un identificador estable de esa venta —un UUID tuyo—.
Si repetís el mismo pedido con la misma key te devolvemos el borrador que ya existía en
vez de duplicarlo, y la respuesta te avisa que fue una repetición. Si usás la misma key
con otro contenido, te frenamos con 409 IDEMPOTENCY_KEY_REUSADA.
Si el POST se te cortó por timeout y no sabés si el borrador quedó creado,
podés cancelar por la key
—/api/v1/empresas/{empresaId}/cfe/idempotencia/{idempotencyKey}— sin
conocer ningún identificador nuestro. Si el borrador existía, se borra; si todavía no
existía, queda marcado para que no pueda nacer después.
Las cancelaciones son repetibles: si volvés a mandar la misma, te contestamos204
igual. Y una key cancelada no se recicla nunca más: un reintento tardío recibeIDEMPOTENCY_KEY_CANCELADA
en vez de crear una venta fantasma.
El alcance de la escritura
Por API se preparan y se emiten e-Tickets y e-Facturas, en pesos uruguayos, al contado, con líneas tomadas de tu catálogo y tu referencia externa en cada una. El máximo es de 700 líneas por e-Ticket y 200 por e-Factura. El cuerpo del pedido es estricto: un campo que no corresponde se rechaza, no se ignora en silencio.
La lectura es más amplia que la escritura. Las notas de crédito, las notas de débito y los resguardos se leen con el mismo detalle —con su referencia al comprobante original y su bloque de retención— y llegan por el mismo feed.
Errores
Un código estable por cada decisión.
Toda respuesta viene envuelta: los datos en data y la paginación aparte.
Los errores usan siempre la misma forma —un código estable, un mensaje, un detalle
opcional y el identificador del request para que podamos rastrearlo— y están pensados
para que tu código decida solo si frenar o reintentar.
Envoltorio de error
403 Forbidden
{
"error": {
"code": "EMPRESA_SUSPENDIDA",
"message": "La empresa está suspendida.",
"requestId": "01J8Q…"
}
}
| Decisión | Códigos | Qué pasó |
|---|---|---|
| Parar |
401 CREDENCIAL_INVALIDA401 CREDENCIAL_REVOCADA403 SCOPE_INSUFICIENTE403 VINCULO_REVOCADO
|
Problema de credencial o de permiso. Reintentar no cambia nada: hace falta que alguien rote la credencial o ajuste la conexión. |
| Pausar y volver |
403 EMPRESA_SUSPENDIDA429 RATE_LIMIT
|
Situación reversible. La suspensión se levanta y el cursor retoma; el exceso de
tasa trae Retry-After y se resuelve esperando.
|
| Corregir |
400 VALIDATION_ERROR400 CLIENTE_RUC_INVALIDO409 CAE_NO_DISPONIBLE409 CERTIFICADO_VENCIDO
|
O el pedido está mal armado, o falta una precondición fiscal de la empresa. Se arregla el dato o la configuración y recién ahí se reintenta. |
| Resincronizar | 410 CURSOR_VENCIDO |
Tu posición del feed quedó fuera de la retención: se rehace desde el listado. |
Cada aplicación tiene además su propio límite de tasa, separado del de las personas que usan la plataforma desde el navegador. Un pico de tu integración no le pisa la sesión a nadie, y una persona navegando no te consume a vos. El número lo acordamos al dar de alta la aplicación.
Seguridad
Si te roban la credencial.
- Una aplicación puede tener varias credenciales activas a la vez. Eso es lo que te permite rotar sin cortar servicio: emitís la nueva, convivís unos días con las dos, y recién ahí revocás la vieja.
- Revocar corta el acceso en el siguiente request. No hay ventana de gracia por token vigente, porque la credencial se verifica en cada llamada.
-
El corte puede ser total o quirúrgico. La revocación de la aplicación devuelve
401en todas las empresas a la vez; si una sola empresa revoca su conexión, recibís403nada más que con esa y seguís funcionando normal con las demás. - Hay aviso por correo. Cuando se conecta una aplicación a una empresa, el administrador de esa empresa recibe la novedad con la aplicación y los permisos concedidos; al rotar una credencial se avisa al contacto técnico de cada empresa vinculada.
- Cada escritura queda atribuida a tu aplicación como autor, nunca a un usuario prestado. El alta de conexiones, los cambios de permisos y las revocaciones también quedan registrados.
El contrato
Sólo se suma.
El contrato completo está publicado como OpenAPI y se sirve desde la propia API, enGET /api/v1/openapi.json
— el único endpoint que no pide credencial. Con eso generás el cliente tipado en tu
lenguaje. La dirección de la API va junto con la credencial.
La regla de evolución es aditiva: se agregan endpoints y campos de respuesta, y en los
pedidos sólo campos opcionales. Nada existente se saca ni cambia de tipo. Un cambio que
rompa no se edita encima de /v1: es /v2.
Esa promesa está atada al build. Si el código se desvía del contrato publicado, la integración continua falla y el cambio no entra. No depende de que alguien se acuerde de actualizar la documentación.
Aislamiento entre empresas
La API reusa el mismo aislamiento que el resto de la plataforma, no una capa aparte hecha para integraciones: la empresa de la ruta se valida contra tu conexión en cada request y la base filtra por debajo. Un dato de otra empresa no aparece ni siquiera como error: aparece como inexistente.
Pedir acceso
Contanos qué querés conectar.
Escribinos contando qué sistema tenés y qué necesitás leer o escribir, y te avisamos apenas podamos emitir. El alta la hacemos nosotros: registramos tu aplicación, te emitimos la credencial y creamos la conexión con la empresa con los permisos que correspondan.
Los permisos de escritura se conceden uno por uno y de forma explícita — ninguna actualización del sistema te agrega permisos que no pediste. Y los de una conexión se pueden ajustar sin cortarla: no hay que revocar y volver a crear para sumar uno nuevo.