Skip to main content
Antes de este documento conviene leer la introducción: explica por qué el contrato usa el vocabulario de FIRE y no el del ente tributario.

1. Autenticación

Son dos direcciones distintas y conviene no confundirlas.

1.1 Cómo se consume el Fiscal Gateway de FIRE

API key, y nada más. Es el único mecanismo, hoy y siempre. No hay OAuth, ni JWT de usuario, ni sesión. La key es account + vendor scoped y debe tener el scope fiscal:write. El tenant se deriva de la key, nunca del cuerpo: un payload puede mentir, una credencial no. Por eso el request no lleva accountId ni vendorId. La Idempotency-Key la genera quien llama y la reutiliza en cada reintento de la misma venta. Generarla nosotros sería idempotencia decorativa: cada intento traería una llave distinta y no habría nada que comparar.
No es lo que evita el documento duplicado — eso lo hace el orderCode, la llave natural (país + orderCode + operación). Un reintento con el mismo orderCode devuelve el mismo documento aunque el canal regenere la llave, que es el error de implementación más común.Lo que la llave aporta es detectar que se reusó para otra venta: misma llave con cuerpo distinto responde 409 en vez de numerar.
Esta llave no viaja al proveedor. La llamada que sale hacia él lleva solo x-api-key y Content-Type. Si implementás la deduplicación del lado del proveedor, hacela por orderCode.

1.2 Cómo consumimos al proveedor

También API key. Mismo mecanismo en las dos direcciones: el proveedor entrega una key por ambiente y FIRE la envía en x-api-key en cada llamada. No hay OAuth, ni endpoint de token, ni audiencia que configurar. La key se guarda cifrada en la configuración de la cuenta y no sale de la instancia. Es write-only en el backoffice: se carga, nunca se muestra. El proveedor debe poder rotarla sin cortar el servicio — aceptando la key anterior durante una ventana de solapamiento. Sin eso, rotar significa una interrupción de facturación coordinada, y en la práctica se traduce en keys que no se rotan nunca.

2. Endpoint — uno por país

{country} es el código ISO 3166-1 alpha-2 en minúsculas.
Una sola integración. El proveedor recibe un baseUrl y una credencial; las rutas se derivan del país. FIRE resuelve el país antes de llamar —sale de la tienda— así que no hay nada que descubrir ni que configurar por separado.
Por qué por país y no una ruta única. Una integración fiscal se construye y se certifica contra un ente, y las normativas cambian por país. Con una ruta por país, un cambio en Ecuador es una versión del endpoint de Ecuador: no toca Colombia, no obliga a versionar todo, y no puede romperlo. El versionado queda con la misma granularidad que el cambio.También hace innecesario declarar capacidades: las rutas que existen son los países que atendés.
Un 404 en esta ruta significa “no atiendo ese país”, y así lo reportamos. No lo uses para otros errores: un país soportado que falla responde 4xx/5xx con el bloque failure.
Síncrono. Esta llamada está en el camino crítico de la venta: la caja está esperando los números para imprimir. Presupuesto de latencia objetivo: menos de 3 segundos.

3. Request

3.1 Numerar una venta

Un solo request, igual para todos los países. No cambia de forma según el ente: lo que cambia es qué usa cada proveedor. El de Ecuador arma la clave de acceso con la fecha, el emisor y el correlativo, y no mira los importes. El de Colombia los necesita todos, porque su identificador es un hash de la factura. Los dos ejemplos de abajo son el mismo contrato: mismos campos, mismo orden. Lo único que cambia son los valores.
storeFiscalConfig.metadata va vacío: en Ecuador el establecimiento y el punto de emisión los resolvés vos contra tu catálogo, a partir de store.code y device.uid.

3.2 Anular

Idéntico, con "operation": "CANCEL". Mismos campos, client y totals incluidos: la anulación emite un documento nuevo y necesita los mismos datos que la emisión. No enviamos referencia al documento original. El proveedor resuelve qué compensa buscando la emisión del mismo orderCode — que es su propia llave de idempotencia, ya indexada.

3.3 Campos

Los campos vacíos se omiten. Nunca enviamos "". Un campo ausente significa “no configurado”; un string vacío no debe interpretarse como valor válido.
No enviamos establecimiento ni punto de emisión. Los asigna el ente bajo el RUC del emisor y FIRE no tiene ese catálogo — el punto de venta tampoco, y pedírselo lo obligaría a hablar el idioma del SRI para poder facturar.Vos los resolvés: store.code → establecimiento, device.uid → punto de emisión, contra tu propio catálogo. Es el mismo trato que con la identidad fiscal: te mandamos identificadores del negocio y traducís a los del ente.Hasta hace poco el canal declaraba su punto de emisión en device.externalId. Se quitó: era un dato que le exigíamos sin poder validarlo, y que además podía no coincidir con el que terminaba emitido.

3.4 Los dos metadata

Hay dos bloques llave-valor, en niveles distintos y con propósitos distintos:
  • store.storeFiscalConfig.metadata — atributos de la tienda, constantes. Es donde viven los datos que el proveedor necesita y que no son parte del dominio compartido: por ejemplo la clave técnica y el rango de numeración que la DIAN entrega con la resolución. Se configuran una vez en el backoffice y viajan en cada llamada de esa tienda.
  • metadata (raíz) — atributos de la venta, variables: los que cambian en cada transacción y que algún régimen exige declarar.
Ambos son opacos: FIRE no los interpreta ni los valida.

Así se carga el de la tienda

Configuración fiscal de la tienda: el NIT y un editor de clave y valor con claveTecnica y el grupo rangoFacturacion, con desde, hasta y prefijo.

Arriba: el NIT del emisor, la clave técnica y el rango de facturación como grupo anidado.

Continuación de la misma pantalla: el grupo rangoNotaCredito con desde, hasta y prefijo, y debajo la vista previa del JSON resultante.

Más abajo, en la misma pantalla: el rango de notas crédito y la vista previa del JSON que se va a enviar.

Lo que se carga ahí es exactamente lo que recibís en store.storeFiscalConfig.metadata. En la captura, esa tienda va a mandarte:
Esto es un ejemplo, no el contrato. Ni los nombres de las claves ni la lista de campos están fijados por FIRE: se cargan como el proveedor los pida, y se agregan los que hagan falta. Si mañana tu régimen necesita un dato más, es una fila nueva en esta pantalla — no una versión nueva del contrato ni un despliegue nuestro.Publicá las claves que esperás, con el nombre exacto. Un claveTecnica contra un clave_tecnica es un dato que llega y que no vas a encontrar.
Por qué es llave-valor y no un formulario con campos fijos.Los datos que un proveedor necesita son del régimen de su país, no del dominio que compartimos: una clave técnica de la DIAN, un rango de numeración, lo que venga después. Tipificarlos en nuestra pantalla significaría que sumar un país —o que un ente agregue un requisito— obligue a desplegar el backoffice. Con llave-valor, es cargar una fila.Los valores pueden ser texto o un grupo anidado, sin límite de profundidad. Por eso un rango entero —prefijo, desde, hasta, resolución, vigencia— entra como un bloque, en vez de cinco claves con el prefijo pegado al nombre.El ejemplo lleva dos rangos porque son dos cosas distintas: el de facturación y el de notas crédito. Una tienda que solo tenga el primero puede emitir pero no anular.
Ese mismo bloque viaja también en los eventos de la orden, no solo en la numeración. Aparece como data.store.storeFiscalConfig —con su metadata adentro— en:order.opened · order.completed · order.cancelled · order.invoiced · order.reversedEs el mismo dato en los dos caminos, y a propósito: quien consume eventos para conciliar ve con qué configuración se emitió esa venta, sin tener que preguntárselo a nadie.
El detalle campo por campo está en order.completed → Datos fiscales.
Los nombres de las claves los definís vos, no nosotros. El campo es libre: quien configura la tienda escribe la clave que tu integración espera. Por eso conviene que publiques cuáles necesitás y con qué nombre exacto — un claveTecnica contra un clave_tecnica es un dato que llega y que no vas a encontrar.FIRE no valida esos nombres a propósito: el vocabulario es del régimen y del proveedor, y tipificarlo de nuestro lado significaría desplegar el backoffice cada vez que un país nuevo pide un dato distinto.
Ninguna clave dentro de metadata puede pisar un campo de dominio. Si aparece una clave storeCode o country dentro de metadata, debe ignorarse. De lo contrario el llave-valor se convierte en la puerta trasera por la que se redefine el contrato.

3.5 client y totals — lo que viene de la venta

Estos dos bloques son los mismos que el punto de venta arma para inyectar la orden, y viajan tal cual: FIRE no los recorta ni los renombra. Por eso no llevan vocabulario fiscal — llevan el del negocio. Van siempre, en todos los países. Lo que cambia es quién los usa: el proveedor de Ecuador los ignora, porque la clave de acceso se arma con fecha, emisor y correlativo. El de Colombia los necesita enteros, porque el CUFE es un hash de la factura: entran los importes, cada impuesto por separado, la fecha con hora y el documento del adquiriente.

totals — lo cobrado

Moneda USD. Hoy, un solo impuesto: IVA al 15%.
El SRI no los mira: la clave de acceso se arma con fecha, emisor y secuencial. Viajan igual, por si los necesitás para tu propio control.

client — quién compró

Llega entero, tal como lo armó el punto de venta. No es un subconjunto fiscal: trae también datos que no le sirven a ningún ente.
govIdType sale de un catálogo cerrado. Estos son todos los valores que FIRE emite, y no van a llegar otros:Traducirlos al código que exige tu ente es parte de tu implementación, igual que el resto de la traducción al idioma del régimen.
Leé solo lo fiscal y descartá el resto. Lo que necesita un régimen está en govIdType, govIdNumber, name y —para empresas— billingInformation.businessName y additionalInfo.fiscal. El uid, el correo y el teléfono son del negocio, no del ente.govIdType y govIdNumber aparecen dos veces: en la raíz y en billingInformation. Cuando difieren, manda el de facturación — es el documento que el cliente pidió para su factura.
El consumidor final llega con FINAL_CONSUMER y ceros. Traducirlo a lo que el SRI espera en el comprobante es parte de tu implementación.
Los importes viajan en la escala del spec: entero, en string, ×10.000. Un total de 50.000 COP llega como "500000000"; uno de 8,70 USD, como "87000".No es una rareza de este endpoint: es como FIRE almacena y publica todo importe, así que es la MISMA escala que vas a ver en los eventos de la orden. Un solo formato en las dos superficies, y ninguna conversión que dependa de por dónde leíste el dato.Antes este request llevaba decimales ("total": 50000) mientras el evento llevaba "500000000". Quien se confundía de superficie declaraba diez mil veces el monto, con un documento bien formado que el ente aceptaba igual. Esa clase de error ya no existe.Para volver al importe real, dividí por 10.000. Y como la escala son 4 decimales fijos, la conversión a la cadena que pide tu régimen es exacta: corrés el punto cuatro posiciones desde la derecha y recortás a los decimales de tu moneda. Nada de floats.Eso importa si tu identificador es un hash sobre una cadena: el CUFE se calcula sobre "50000.00", y ese string lo armás vos. FIRE no lo formatea porque no conoce la regla de tu régimen — pero partir de un entero exacto es más seguro que partir de un decimal JSON, donde 8.70 llega como 8.7 y los ceros a la derecha se pierden.Una venta de 50.000 COP con IVA de 7.983,19 COP — cada país viaja en su moneda y con sus impuestos, pero la regla de la escala es la misma:
Ojo: solo los importes se escalan. rate, taxesPercentage y discountPercentage son proporciones, no dinero, y viajan tal cual — "0.19" sigue siendo "0.19".Y esto importa especialmente porque vas a consumir los eventos de la orden. No es opcional: la numeración te da los identificadores, pero la venta que emitís al ente sale del evento — y de ahí volvés con el callback. Sin ese circuito, nadie sabe si el documento se emitió.Ver Qué llega al integrador.
taxes trae siempre el desglose, un elemento por impuesto, cada uno con name, base, rate y amount. El monto por impuesto es el dato que importa: amount es lo que declarás al ente, y taxValue de arriba es apenas su suma.Recorré el array, no leas taxes[0]. Hoy en Ecuador y Colombia es un solo IVA, pero un régimen puede declarar varios tributos por comprobante y el array los trae todos, sin que el contrato cambie.
Los dos bloques viajan en las dos operaciones, INVOICE y CANCEL. Es un solo request canónico y no se recorta por operación.No es simetría por prolijidad: la anulación produce un documento nuevo. Una nota crédito colombiana tiene su propio identificador calculado sobre los importes y el adquiriente, así que sin client y totals no habría con qué armarlo.Lo que no cambia es el alcance: la anulación es total. No existen anulaciones parciales en ningún país que atendemos, así que los importes que llegan son los de la venta completa, y qué documento compensás lo resolvés por el orderCode.

4. Idempotencia

La llave es country + orderCode + operation. Repetir esa terna debe devolver el mismo documento con "reused": true, sin consumir otro secuencial. Es la misma llave que usa FIRE de su lado, para que un choque se detecte en ambos extremos a la vez. Esto asume que el orderCode es único por cuenta y país. Es la misma suposición que ya hacen las dos partes.

5. Respuesta exitosa

La respuesta tiene dos partes con reglas distintas:
  • El sobre — idéntico en todos los países. Es con lo que FIRE opera: decide si reintentar, si hubo comprobante, qué error reportar.
  • document — el idioma fiscal del país. Cada uno manda lo que existe en su régimen, con los nombres de su ente, y nada más.
Todo lo de arriba es igual para cualquier país. document es lo único que cambia, y por eso acá va elidido: su contenido está en 5.2, con una sección por país. Si estás implementando Ecuador, el bloque que te toca es el de Ecuador y ninguno más.
country viaja aunque esté en la ruta. No es redundancia: FIRE compara country y orderCode contra lo que pidió y descarta la respuesta si no coinciden. Es lo que evita imprimir el documento de otra venta cuando hay un cruce de respuestas o un proxy con caché.

5.1 status

Dos valores, uno por operación: No hay más estados, y es deliberado. Este endpoint produce la representación fiscal —los identificadores para imprimir— y nada más. El envío al ente y su autorización ocurren después, del lado del proveedor, y su desenlace llega por el callback. Modelar aquí estados de autorización mezcla dos ciclos de vida distintos. status es casi un eco de operation, y existe por un solo caso: cuando la operación no produce documento. La cancelación en Brasil es un evento de cancelamento, no un documento nuevo, así que la respuesta llega con document: null y sin graphic. Ahí status es lo único que afirma que la operación se completó, en vez de dejar una respuesta exitosa y vacía que no se puede distinguir de un error silencioso. En particular:
  • No existe PENDING. Inmediatamente después de numerar, el documento siempre está pendiente de autorización: es la condición normal, no un estado que informar. La caja imprime con los identificadores que acaba de recibir.
  • No existe REJECTED. Si no se pudo numerar es un error: HTTP no-2xx con el bloque failure. Un rechazo con 200 OK y el motivo escondido en un campo es un contrato donde alguien no valida y cree que numeró.
  • No existe REUSED. Eso es reused: true, un booleano ortogonal. Se puede tener INVOICED con reused: true — un reintento idempotente de una venta ya numerada — y esa distinción se pierde si REUSED fuera un estado.

5.2 document — el documento numerado

Acá se habla el idioma fiscal del país. Es el único bloque de la respuesta que cambia entre países, y cambia entero: los nombres son los del ente, no una traducción nuestra. Un país manda lo que existe en su régimen y nada más. Un campo que no aplica no viaja en null: sencillamente no está.
El número visible lo armás vos, ya listo para imprimir.Quince dígitos en tres tramos separados por guion —establecimiento(3), puntoEmision(3), secuencial(9)— según el art. 18 del Reglamento de Comprobantes de Venta.Antes lo componía FIRE con las tres piezas. Se movió acá a propósito: el formato es regla del régimen, no presentación, y quien está certificado ante el SRI sos vos. Si el Reglamento cambia la convención, cambia de tu lado sin que FIRE despliegue.Hay además una razón concreta: el Reglamento permite omitir los ceros a la izquierda del secuencial. 001-020-123 puede ser tan legal como 001-020-000000123. Armándolo nosotros estaríamos eligiendo una variante en tu nombre. Mandá el que emitiste — FIRE lo imprime tal cual, sin reformatearlo.
Las tres piezas siguen viajando igual, como las nombra el SRI y sin concatenarlas en una serie de 6 dígitos: se usan para conciliar, no para componer el número.El puntoEmision que devuelvas es el que quedó emitido, que puede no ser el que se pidió en device.uid a través de tu catálogo. Lo que vale es siempre lo que vuelve, nunca lo que se mandó.
ambiente no es informativo. FIRE lo compara contra el ambiente configurado para el vendor y corta si no coinciden. Es lo que atrapa a un proveedor emitiendo contra el ambiente de pruebas del SRI mientras la operación cree estar en producción — sin ese chequeo las ventas salen con claves de acceso que el ente no reconoce, y se descubre cuando un cliente reclama su factura.

Por qué el bloque es del país y no un modelo común

Se evaluó un bloque plano con nombres por rol —accessKey, sequential, controlNumber— y se descartó. El costo no era un campo nulo: era que cada país nuevo agregaba un campo que todos los demás cargaban vacío para siempre, y que el mismo identificador tenía dos nombres según entrara por el prekey o por el callback. Este es además el mismo mecanismo que ya usa el callback de resultado, que valida por país sobre el countryCode de la raíz. Un solo patrón en las dos direcciones.

Lo que document NO lleva

No lleva documentType. Con qué instrumento fiscal se materializa la operación —una nota de crédito en Ecuador, un evento de cancelamento en Brasil— es asunto del país y del proveedor. Lo que se pidió ya lo dice status. No lleva lo que asigna el ente al autorizar — el numeroAutorizacion del SRI, el protocolo de la SEFAZ. Eso llega por el callback; declararlo acá lo condena a venir siempre en null. No lleva authorizationMode ni issuedAt. Son comunes a todos los países y viven en la raíz de la respuesta.

5.3 graphic — lo imprimible

Llave-valor, con las claves que cada país necesite. {} o null cuando la operación no produce nada que imprimir — la cancelación en Brasil, por ejemplo.
Cada valor es el string exacto a codificar, ya listo para renderizar. FIRE no lo interpreta ni lo transforma: lo pasa al punto de venta, que lo renderiza con su propia librería y lo manda a la impresora. No se generan imágenes de este lado — el tamaño y la resolución dependen de la impresora, y eso solo lo sabe quien imprime. Es un mapa abierto y no un campo fijo porque el comprobante de cada país no lleva siempre lo mismo: Ecuador imprime el código de la clave de acceso, Brasil el QR de la NFC-e, Chile el timbre electrónico (TED). Un país puede necesitar más de uno. Con un mapa, agregar uno es enviarlo; con campos fijos, es versionar el contrato. Las claves son estables y descriptivas del propósito — qr, barcode, ted — no de la simbología del momento. Este bloque es el único de la respuesta que el proveedor aporta y no se puede derivar, por un caso concreto: el QR de la NFC-e brasileña es una URL firmada con un hash que solo puede construir el emisor. No se deriva de la chave. Si no llega, no hay QR. En Ecuador el valor va a coincidir con document.claveAcceso. Esa redundancia es deliberada: la alternativa es que el punto de venta sepa que en Ecuador se codifica la clave, en Brasil la URL y en Chile el TED.
Colombia no manda graphic. Su QR ya es una URL lista para imprimir y viaja en document.qrCode; repetirla acá sería el mismo dato en dos lugares que pueden discrepar, y ante la duda nadie sabría cuál gana.La diferencia con Ecuador no es capricho: allá el QR se deriva de la clave de acceso, y entregarlo explícito le ahorra al punto de venta tener que saberlo. Acá no se deriva de nada — ya viene resuelto.
No incluye pdfUrl, xmlUrl ni lookupUrl. Los dos primeros solo existen después de que el ente autorizó y llegan por el callback; declararlos acá los condena a venir siempre en null, y un campo que siempre es nulo enseña a ignorarlo. lookupUrl es una constante por país y ambiente, no un dato del documento.

5.4 provider y metadata — los dos bloques del proveedor

Son dos bloques distintos y no intercambiables, y FIRE los guarda en dos columnas distintas. La diferencia es si el campo tiene forma acordada o no.

provider — identidad, con forma

Los tres van siempre presentes, con null cuando no aplica. null dice “no tengo”; ausente obliga a distinguir dos formas de lo mismo.
reference no es nuestra Idempotency-Key. Esa la mandamos nosotros y el proveedor la ecoa por otro lado. Esta es del proveedor, y es la que sirve cuando hay que escalarle un caso: sin ella, la única forma de que encuentre la operación es que busque por orderCode en el rango de fechas correcto.

metadata — la bolsa opaca

Sin forma acordada. Va lo que le sirva al proveedor para diagnosticar: los códigos con que él nombra la tienda y el aparato, un identificador de su cola, lo que sea. FIRE lo guarda tal cual y lo publica tal cual, y nada nuestro programa contra sus claves.
No manden acá lo que ya tiene su lugar. Repetir failure dentro de metadata, o el name del bloque de arriba, produce el mismo hecho guardado dos veces — y dos copias se desincronizan. Si un dato tiene campo propio en el contrato, va en su campo y no también acá.
Puede ser {}. Lo que no puede es cambiar entre una emisión y su reintento idempotente: un 201 que trae la bolsa poblada y un 200 REUSED que la trae vacía describen la misma operación de dos maneras distintas, y quien lea el evento va a ver que “cambió” algo que no cambió.

6. Respuesta con error

Viaja con código HTTP no-2xx422 para un problema de configuración o de datos, 5xx para uno transitorio. Nunca con 200.
retryable es obligatorio y lo decide el proveedor. Es lo que nos permite distinguir un problema de configuración —que no mejora reintentando— de uno transitorio. Sin ese campo hay que adivinar por el código HTTP, y adivinar mal significa reintentar en la caja mientras el cliente espera, o abandonar una venta que se podía numerar. failure.code debe ser un código estable y accionable, no un texto libre. Es lo que permite construir alertas y documentación de soporte. failure.message debe describir el problema real, no una generalidad. "identidad fiscal de tienda no configurada: EC / tienda K0050" permite arreglar; "documento rechazado" obliga a abrir un ticket.

7. Reglas de la integración

Lo que enviamos manda. Si el catálogo del proveedor tiene una identidad fiscal distinta de la que enviamos, debe rechazar con error explícito, nunca emitir con la suya. Un comprobante emitido bajo el contribuyente equivocado no se corrige con un deploy. metadata es opaco en ambos sentidos y no puede pisar campos de dominio. Los códigos del ente no viajan en el contrato. Nada de documentTypeCode: "01", tipoComprobante ni equivalentes. El proveedor los deriva de operation + country. El contrato es versionado. Un cambio rompiente requiere una versión nueva del endpoint y una ventana de convivencia; no se cambia el significado de un campo existente.

8. El callback de resultado

El callback de resultado sigue como está. Se correlaciona por orderCode + operation, con el tenant derivado de la API key con la que se autentica. Debe incluir la operación: sin ella, una factura y su anulación sobre la misma orden son indistinguibles. Devolver identificadores adicionales en el callback es opcional y bienvenido, pero no requerido.
Usá los mismos nombres que en la numeración. El callback de Colombia declara cufe, prefijo, numeroDian, numeroComprobante, qrCode y ambiente — exactamente los de 5.2. Es el mismo documento contado dos veces, y si los nombres divergen, conciliar los dos caminos deja de ser comparar campos y pasa a ser traducir, que es donde se cuelan los errores.
El callback no se rechaza por un campo que falte. Cuando llega, el documento ya existe ante el ente: devolver un 400 no lo deshace, solo nos deja sin enterarnos de una factura autorizada — y ese aviso no vuelve.Por eso los campos nuevos entran siempre opcionales y lo que mandes de más se conserva. Es lo contrario de la numeración, que sí valida estricto: ahí el dato se acaba de calcular y todavía no se imprimió nada. La asimetría es deliberada, y depende de dónde duele el error.