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.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 inx-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.
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.
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.- Ecuador (EC)
- Colombia (CO)
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.
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.
This is how the store’s is loaded

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

Further down the same screen: the credit note range and the preview of the JSON that will be sent.
store.storeFiscalConfig.metadata. In
the screenshot, that store is going to send you:
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 The field-by-field detail is in
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.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.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
- Ecuador (EC)
- Colombia (CO)
Currency 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.
USD. Today, a single tax: IVA at 15%.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.- Ecuador (EC)
- Colombia (CO)
FINAL_CONSUMER with zeros. Translating that into what the SRI
expects on the receipt is part of your implementation.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 iscountry + 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.
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 thefailureblock. A rejection with200 OKand the reason hidden in a field is a contract where someone does not validate and believes they numbered. - There is no
REUSED. That isreused: true, an orthogonal boolean. You can haveINVOICEDwithreused: true— an idempotent retry of an already numbered sale — and that distinction is lost ifREUSEDwere 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.
- Ecuador (EC) — SRI
- Colombia (CO) — DIAN
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.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.
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.
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
{}. 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 code —422 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 byorderCode + 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.
