Skip to main content
Before this document it helps to read the introduction: it explains why the contract uses FIRE’s vocabulary and not the tax authority’s.

1. Authentication

These are two distinct directions and it is worth not confusing them.

1.1 How FIRE’s Fiscal Gateway is consumed

API key, and nothing else. It is the only mechanism, today and always. There is no OAuth, no user JWT, no session. The key is account + vendor scoped and must carry the fiscal:write scope. The tenant is derived from the key, never from the body: a payload can lie, a credential cannot. That is why the request carries neither accountId nor vendorId. The Idempotency-Key is generated by the caller and reused on every retry of the same sale. Generating it ourselves would be decorative idempotency: each attempt would bring a different key and there would be nothing to compare.
It is not what prevents a duplicate document — that is the job of orderCode, the natural key (country + orderCode + operation). A retry with the same orderCode returns the same document even if the channel regenerates the key, which is the most common implementation mistake.What the key adds is detecting that it was reused for a different sale: the same key with a different body answers 409 instead of numbering.
This key does not travel to the provider. The call going out to them carries only x-api-key and Content-Type. If you implement deduplication on the provider side, do it by orderCode.

1.2 How we consume the provider

Also an API key. The same mechanism in both directions: the provider issues one key per environment and FIRE sends it in x-api-key on every call. There is no OAuth, no token endpoint, no audience to configure. The key is stored encrypted in the account configuration and never leaves the instance. It is write-only in the backoffice: it is loaded, never displayed.

2. Endpoint — one per country

{country} is the ISO 3166-1 alpha-2 code in lowercase.
A single integration. The provider receives one baseUrl and one credential; the routes are derived from the country. FIRE resolves the country before calling —it comes from the store— so there is nothing to discover and nothing to configure separately.
Why per country and not a single route. A fiscal integration is built and certified against one authority, and regulations change per country. With one route per country, a change in Ecuador is a version of Ecuador’s endpoint: it does not touch Colombia, it does not force versioning everything, and it cannot break it. Versioning ends up with the same granularity as the change.It also makes declaring capabilities unnecessary: the routes that exist are the countries you serve.
A 404 on this route means “I do not serve that country”, and that is how we report it. Do not use it for other errors: a supported country that fails answers 4xx/5xx with the failure block.
Synchronous. This call sits on the critical path of the sale: the register is waiting for the numbers to print. Target latency budget: under 3 seconds.

3. Request

3.1 Numbering a sale

One single request, the same for every country. Its shape does not change per authority: what changes is what each provider uses. Ecuador’s builds the access key from the date, the issuer and the sequential, and never looks at the amounts. Colombia’s needs all of them, because its identifier is a hash of the invoice. The two examples below are the same contract: same fields, same order. The only thing that changes are the values.
storeFiscalConfig.metadata travels empty: in Ecuador the establishment and the point of emission are resolved on your side against your catalog, from store.code and device.uid.

3.2 Cancelling

Identical, with "operation": "CANCEL". Same fields, client and totals included: a cancellation issues a new document and needs the same data as the issuance. We do not send a reference to the original document. The provider resolves what it offsets by looking up the issuance with the same orderCode — which is its own idempotency key, already indexed.

3.3 Fields

Empty fields are omitted. We never send "". An absent field means “not configured”; an empty string must not be read as a valid value.
We do not send establishment or point of emission. The authority assigns them under the issuer’s tax ID and FIRE does not have that catalog — neither does the point of sale, and demanding it would force the POS to speak the SRI’s language just to invoice.You resolve them: store.code → establishment, device.uid → point of emission, against your own catalog. It is the same deal as with fiscal identity: we send you the business’s identifiers and you translate into the authority’s.Until recently the channel declared its point of emission in device.externalId. It was removed: it was a value we demanded without being able to validate it, and one that could differ from the one actually issued.

3.4 The two metadata

There are two key-value blocks, at different levels and with different purposes:
  • store.storeFiscalConfig.metadata — attributes of the store, constant. This is where the data a provider needs and that is not part of the shared domain lives: for example the technical key and the numbering range the DIAN issues with the resolution. They are configured once in the backoffice and travel on every call from that store.
  • metadata (root) — attributes of the sale, variable: the ones that change on every transaction and that some regime requires declaring.
Both are opaque: FIRE neither interprets nor validates them.

This is how the store’s is loaded

Store fiscal configuration: the NIT and a key-value editor with claveTecnica and the rangoFacturacion group, with desde, hasta and prefijo.

Top: the issuer's NIT, the technical key and the invoicing range as a nested group.

Continuation of the same screen: the rangoNotaCredito group with desde, hasta and prefijo, and below it the preview of the resulting JSON.

Further down the same screen: the credit note range and the preview of the JSON that will be sent.

What gets loaded there is exactly what you receive in store.storeFiscalConfig.metadata. In the screenshot, that store is going to send you:
This is an example, not the contract. Neither the key names nor the list of fields are fixed by FIRE: they are loaded as the provider asks for them, and whatever is needed gets added. If tomorrow your regime needs one more value, that is a new row on this screen — not a new version of the contract nor a deployment on our side.Publish the keys you expect, with their exact names. A claveTecnica against a clave_tecnica is data that arrives and that you will not find.
Why key-value and not a form with fixed fields.The data a provider needs belongs to its country’s regime, not to the domain we share: a DIAN technical key, a numbering range, whatever comes next. Typing them into our screen would mean that adding a country —or an authority adding a requirement— forces a backoffice deployment. With key-value, it is one row.Values can be text or a nested group, with no depth limit. That is why a whole range —prefix, from, to, resolution, validity— goes in as a block, instead of five keys with the prefix glued to the name.The example carries two ranges because they are two different things: the invoicing one and the credit note one. A store that only has the first can issue but cannot cancel.
That same block also travels in the order’s events, not only in the numbering. It shows up as data.store.storeFiscalConfig —with its metadata inside— in:order.opened · order.completed · order.cancelled · order.invoiced · order.reversedIt is the same data on both paths, and deliberately so: whoever consumes events to reconcile sees which configuration that sale was issued with, without having to ask anyone.
The field-by-field detail is in order.completed → Fiscal data.
You define the key names, not us. The field is free: whoever configures the store types the key your integration expects. That is why it pays to publish which ones you need and with what exact name — a claveTecnica against a clave_tecnica is data that arrives and that you will not find.FIRE does not validate those names on purpose: the vocabulary belongs to the regime and to the provider, and typing it on our side would mean deploying the backoffice every time a new country asks for a different value.
No key inside metadata may override a domain field. If a storeCode or country key shows up inside metadata, it must be ignored. Otherwise the key-value bag becomes the back door through which the contract gets redefined.

3.5 client and totals — what comes from the sale

These two blocks are the same ones the point of sale builds to inject the order, and they travel as is: FIRE neither trims nor renames them. That is why they carry no fiscal vocabulary — they carry the business’s. They travel always, in every country. What changes is who uses them: Ecuador’s provider ignores them, because the access key is built from date, issuer and sequential. Colombia’s needs them whole, because the CUFE is a hash of the invoice: the amounts go in, each tax separately, the date with its time and the buyer’s identification.

totals — what was charged

Currency USD. Today, a single tax: IVA at 15%.
The SRI does not look at them: the access key is built from date, issuer and sequential. They travel anyway, in case you need them for your own control.

client — who bought

It arrives whole, exactly as the point of sale built it. It is not a fiscal subset: it also brings data no authority has any use for.
govIdType comes from a closed catalog. These are all the values FIRE emits, and no others will arrive:Translating them into the code your authority requires is part of your implementation, just like the rest of the translation into the regime’s language.
Read only what is fiscal and discard the rest. What a regime needs is in govIdType, govIdNumber, name and —for companies— billingInformation.businessName and additionalInfo.fiscal. The uid, the email and the phone belong to the business, not to the authority.govIdType and govIdNumber appear twice: at the root and inside billingInformation. When they differ, the billing one wins — it is the document the customer asked for on their invoice.
The final consumer arrives as FINAL_CONSUMER with zeros. Translating that into what the SRI expects on the receipt is part of your implementation.
Amounts travel at spec scale: an integer, in a string, ×10,000. A total of 50,000 COP arrives as "500000000"; one of 8.70 USD, as "87000".This is not a quirk of this endpoint: it is how FIRE stores and publishes every amount, so it is the SAME scale you will see in the order events. One format across both surfaces, and no conversion that depends on where you read the value from.This request used to carry decimals ("total": 50000) while the event carried "500000000". Anyone who mixed up the two surfaces declared ten thousand times the amount, in a well-formed document the tax authority accepted anyway. That class of error is now gone.To get back to the real amount, divide by 10,000. And because the scale is a fixed 4 decimals, converting to the string your regime requires is exact: move the point four places from the right and trim to your currency’s decimals. No floats involved.That matters if your identifier is a hash over a string: the CUFE is computed over "50000.00", and you build that string yourself. FIRE does not format it because it does not know your regime’s rule — but starting from an exact integer is safer than starting from a JSON decimal, where 8.70 arrives as 8.7 and trailing zeros are lost.A sale of 50,000 COP with 7,983.19 COP of IVA — each country travels in its own currency and with its own taxes, but the scale rule is the same:
Careful: only amounts are scaled. rate, taxesPercentage and discountPercentage are ratios, not money, and travel as-is — "0.19" stays "0.19".And this matters especially because you will consume the order events. It is not optional: numbering gives you the identifiers, but the sale you report to the tax authority comes from the event — and you come back from there with the callback. Without that loop, nobody knows whether the document was issued.See What reaches the integrator.
taxes always carries the breakdown, one element per tax, each with name, base, rate and amount. The per-tax amount is the value that matters: amount is what you declare to the authority, and taxValue above is merely their sum.Iterate the array, do not read taxes[0]. Today in Ecuador and Colombia it is a single IVA, but a regime can declare several taxes per receipt and the array brings them all, without the contract changing.
Both blocks travel on both operations, INVOICE and CANCEL. It is one canonical request and it is not trimmed per operation.It is not symmetry for tidiness: a cancellation produces a new document. A Colombian credit note has its own identifier computed over the amounts and the buyer, so without client and totals there would be nothing to build it from.What does not change is the scope: cancellation is total. There are no partial cancellations in any country we serve, so the amounts arriving are those of the complete sale, and which document you offset is resolved through the orderCode.

4. Idempotency

The key is country + orderCode + operation. Repeating that triple must return the same document with "reused": true, without consuming another sequential. It is the same key FIRE uses on its side, so a clash is detected at both ends at once.

5. Successful response

The response has two parts with different rules:
  • The envelope — identical in every country. It is what FIRE operates on: whether to retry, whether there is a receipt, which error to report.
  • document — the country’s fiscal language. Each one sends what exists in its regime, with its authority’s names, and nothing else.
Everything above is the same for any country. document is the only thing that changes, and that is why it is elided here: its contents are in 5.2, with one section per country. If you are implementing Ecuador, the block that concerns you is Ecuador’s and no other.
country travels even though it is in the route. It is not redundancy: FIRE compares country and orderCode against what it asked for and discards the response if they do not match. It is what prevents printing another sale’s document when there is a crossed response or a caching proxy.

5.1 status

Two values, one per operation: There are no more states, and that is deliberate. This endpoint produces the fiscal representation —the identifiers to print— and nothing else. Submission to the authority and its authorization happen afterwards, on the provider’s side, and the outcome arrives via the callback. Modelling authorization states here mixes two distinct lifecycles. status is almost an echo of operation, and it exists for a single case: when the operation produces no document. Cancellation in Brazil is a cancellation event, not a new document, so the response arrives with document: null and no graphic. There, status is the only thing asserting the operation completed, instead of leaving a successful, empty response that cannot be told apart from a silent error. In particular:
  • There is no PENDING. Immediately after numbering, a document is always pending authorization: that is the normal condition, not a state to report. The register prints with the identifiers it just received.
  • There is no REJECTED. If it could not be numbered, it is an error: non-2xx HTTP with the failure block. A rejection with 200 OK and the reason hidden in a field is a contract where someone does not validate and believes they numbered.
  • There is no REUSED. That is reused: true, an orthogonal boolean. You can have INVOICED with reused: true — an idempotent retry of an already numbered sale — and that distinction is lost if REUSED were a state.

5.2 document — the numbered document

This is where the country’s fiscal language is spoken. It is the only block of the response that changes between countries, and it changes entirely: the names are the authority’s, not a translation of ours. A country sends what exists in its regime and nothing more. A field that does not apply does not travel as null: it simply is not there.
You assemble the visible number, ready to print.Fifteen digits in three hyphen-separated segments —establecimiento(3), puntoEmision(3), secuencial(9)— per art. 18 of the Reglamento de Comprobantes de Venta.FIRE used to compose it from the three pieces. It was moved here on purpose: the format is a rule of the regime, not presentation, and you are the one certified before the SRI. If the Regulation changes the convention, it changes on your side without FIRE deploying.There is also a concrete reason: the Regulation allows omitting the leading zeros of the sequential. 001-020-123 can be just as legal as 001-020-000000123. Assembling it ourselves would mean picking a variant on your behalf. Send the one you issued — FIRE prints it as is, without reformatting it.
The three pieces still travel the same, as the SRI names them and without concatenating them into a 6-digit serie: they are used to reconcile, not to compose the number.The puntoEmision you return is the one actually issued, which may not be the one requested through device.uid via your catalog. What counts is always what comes back, never what was sent.
ambiente is not informational. FIRE compares it against the environment configured for the vendor and stops if they do not match. It is what catches a provider issuing against the SRI’s testing environment while the operation believes it is in production — without that check, sales go out with access keys the authority does not recognize, and it is discovered when a customer asks for their invoice.

Why the block belongs to the country and is not a common model

A flat block with role-based names —accessKey, sequential, controlNumber— was evaluated and discarded. The cost was not a null field: it was that every new country added a field all the others would carry empty forever, and that the same identifier had two names depending on whether it came in through the prekey or the callback. This is also the same mechanism the result callback already uses, validating per country against the root’s countryCode. One single pattern in both directions.

What document does NOT carry

It does not carry documentType. Which fiscal instrument materializes the operation —a credit note in Ecuador, a cancellation event in Brazil— is the country’s and the provider’s business. What was requested is already stated by status. It does not carry what the authority assigns on authorization — the SRI’s numeroAutorizacion, the SEFAZ’s protocolo. That arrives via the callback; declaring it here condemns it to always come as null. It does not carry authorizationMode or issuedAt. They are common to every country and live at the root of the response.

5.3 graphic — what is printable

Key-value, with whatever keys each country needs. {} or null when the operation produces nothing printable — cancellation in Brazil, for example.
Each value is the exact string to encode, ready to render. FIRE neither interprets nor transforms it: it passes it to the point of sale, which renders it with its own library and sends it to the printer. No images are generated on this side — size and resolution depend on the printer, and only whoever prints knows that. It is an open map and not a fixed field because each country’s receipt does not always carry the same thing: Ecuador prints the access key code, Brazil the NFC-e QR, Chile the electronic stamp (TED). A country may need more than one. With a map, adding one is sending it; with fixed fields, it is versioning the contract. The keys are stable and describe the purpose — qr, barcode, ted — not the symbology of the moment. This block is the only one in the response that the provider contributes and that cannot be derived, for one concrete case: the Brazilian NFC-e QR is a URL signed with a hash only the issuer can build. It is not derived from the chave. If it does not arrive, there is no QR. In Ecuador the value will match document.claveAcceso. That redundancy is deliberate: the alternative is for the point of sale to know that Ecuador encodes the key, Brazil the URL and Chile the TED.
Colombia does not send graphic. Its QR is already a URL ready to print and it travels in document.qrCode; repeating it here would be the same data in two places that can disagree, and in case of doubt nobody would know which one wins.The difference with Ecuador is not a whim: there the QR is derived from the access key, and handing it over explicitly saves the point of sale from having to know that. Here it is derived from nothing — it already arrives resolved.
It does not include pdfUrl, xmlUrl or lookupUrl. The first two only exist after the authority has authorized and they arrive via the callback; declaring them here condemns them to always come as null, and a field that is always null teaches you to ignore it. lookupUrl is a constant per country and environment, not a value of the document.

5.4 provider and metadata — the provider’s two blocks

They are two distinct, non-interchangeable blocks, and FIRE stores them in two different columns. The difference is whether the field has an agreed shape or not.

provider — identity, with a shape

All three are always present, with null when not applicable. null says “I do not have it”; absent forces distinguishing two forms of the same thing.
reference is not our Idempotency-Key. That one we send and the provider echoes elsewhere. This one is the provider’s, and it is what helps when a case has to be escalated to them: without it, the only way for them to find the operation is searching by orderCode in the right date range.

metadata — the opaque bag

No agreed shape. Whatever helps the provider diagnose goes in: the codes they use to name the store and the device, an identifier from their queue, whatever. FIRE stores it as is and publishes it as is, and nothing of ours programs against its keys.
Do not send here what already has its place. Repeating failure inside metadata, or the name from the block above, produces the same fact stored twice — and two copies drift apart. If a value has its own field in the contract, it goes in its field and not also here.
It can be {}. What it cannot do is change between an issuance and its idempotent retry: a 201 bringing a populated bag and a 200 REUSED bringing it empty describe the same operation in two different ways, and whoever reads the event will see that something “changed” that did not.

6. Error response

It travels with a non-2xx HTTP code422 for a configuration or data problem, 5xx for a transient one. Never with 200.
retryable is mandatory and the provider decides it. It is what lets us tell a configuration problem —which does not improve by retrying— from a transient one. Without that field you have to guess from the HTTP code, and guessing wrong means retrying at the register while the customer waits, or abandoning a sale that could have been numbered. failure.code must be a stable, actionable code, not free text. It is what makes alerts and support documentation possible. failure.message must describe the real problem, not a generality. "identidad fiscal de tienda no configurada: EC / tienda K0050" can be fixed; "document rejected" forces opening a ticket.

7. Integration rules

What we send wins. If the provider’s catalog holds a fiscal identity different from the one we send, it must reject with an explicit error, never issue with its own. A receipt issued under the wrong taxpayer is not fixed with a deploy. metadata is opaque in both directions and cannot override domain fields. The authority’s codes do not travel in the contract. No documentTypeCode: "01", tipoComprobante or equivalents. The provider derives them from operation + country. The contract is versioned. A breaking change requires a new version of the endpoint and a coexistence window; the meaning of an existing field is not changed.

8. The result callback

The result callback stays as it is. It is correlated by orderCode + operation, with the tenant derived from the API key it authenticates with. It must include the operation: without it, an invoice and its cancellation over the same order are indistinguishable. Returning additional identifiers in the callback is optional and welcome, but not required.
Use the same names as in the numbering. Colombia’s callback declares cufe, prefijo, numeroDian, numeroComprobante, qrCode and ambiente — exactly those of 5.2. It is the same document told twice, and if the names diverge, reconciling both paths stops being comparing fields and becomes translating, which is where mistakes slip in.
The callback is not rejected over a missing field. By the time it arrives, the document already exists before the authority: returning a 400 does not undo it, it only leaves us unaware of an authorized invoice — and that notice does not come back.That is why new fields always come in optional and whatever you send extra is preserved. It is the opposite of the numbering, which does validate strictly: there the value has just been computed and nothing has been printed yet. The asymmetry is deliberate, and it depends on where the error hurts.