Estamos terminando la conexión con DGI. Dejanos tus datos y arrancás con nosotros el primer día.

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.

Permisos de una conexión aplicación ↔ empresa
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:

Lectura

Qué podés leer.

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 }
}
salió observado por DGI aceptado por DGI rechazado por DGI anulado

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

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.

  1. Resolvés catálogos. Buscás el artículo por su código interno exacto y te quedás con su identificador.
  2. Das de alta el receptor, si hace falta./api/v1/empresas/{empresaId}/clientes crea 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_INVALIDO antes de crear nada.
  3. 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 una referencia_externa tuya, obligatoria: un texto corto con el que identificás esa línea en tu sistema, que vuelve tal cual en las lecturas.
  4. Revisás lo que quedó armado: montos, receptor y líneas ya vienen en la respuesta.
  5. Ordenás la emisión./api/v1/empresas/{empresaId}/cfe/{cfeId}/emitir no lleva cuerpo ni key, y sólo aplica a un borrador creado por tu propia aplicación. Te respondemos 202 con 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…"
  }
}
Qué hacer según el código
Decisión Códigos Qué pasó
Parar 401 CREDENCIAL_INVALIDA
401 CREDENCIAL_REVOCADA
403 SCOPE_INSUFICIENTE
403 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_SUSPENDIDA
429 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_ERROR
400 CLIENTE_RUC_INVALIDO
409 CAE_NO_DISPONIBLE
409 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.

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.