> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fire.rest/llms.txt
> Use this file to discover all available pages before exploring further.

# Endpoint contract

> What we send a fiscal provider and what we expect back. It is the requirement any Fiscal Gateway provider implements.

<Info>
  Before this document it helps to read [the introduction](/en/fiscal-providers/overview): it
  explains why the contract uses FIRE's vocabulary and not the tax authority's.
</Info>

## 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.

| Header            | Type   |              |
| ----------------- | ------ | ------------ |
| `x-api-key`       | string | **required** |
| `Idempotency-Key` | string | **required** |

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.

<Note>
  **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.
</Note>

<Warning>
  **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`.
</Warning>

### 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.

| Header      | Type   |              |
| ----------- | ------ | ------------ |
| `x-api-key` | string | **required** |

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

```
POST {baseUrl}/api/v1/fiscal/{country}/prekeys
Content-Type: application/json
x-api-key: <provider's api key>
```

`{country}` is the **ISO 3166-1 alpha-2 code in lowercase**.

```
POST {baseUrl}/api/v1/fiscal/ec/prekeys      Ecuador
POST {baseUrl}/api/v1/fiscal/co/prekeys      Colombia
```

**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.

<Note>
  **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**.
</Note>

<Warning>
  **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.
</Warning>

**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.

<Tabs>
  <Tab title="Ecuador (EC)">
    ```json theme={null}
    {
      "country": "EC",
      "operation": "INVOICE",
      "businessDayDate": "2026-08-12",
      "createdAt": "2026-08-12T17:26:09.386Z",
      "orderCode": "FUEL-EC-1786553720451",
      "store": {
        "code": "K0050",
        "storeFiscalConfig": {
          "govIdType": "RUC",
          "govIdNumber": "1791415132001",
          "company": {
            "govIdType": "RUC",
            "govIdNumber": "1791415132001",
            "legalName": "INT FOOD SERVICES CORP S.A.",
            "tradeName": "KFC"
          },
          "metadata": {}
        }
      },
      "device": {
        "uid": "52CAEA5A18D9B75F",
        "name": "KIOSK",
        "platform": "android",
        "metadata": { "ip": "10.0.0.0" }
      },
      "client": {
        "uid": "8Z35YvBbgKVj67AJwZ3nFmAqrtk1",
        "name": "CONSUMIDOR",
        "lastName": "FINAL",
        "email": "consumidor.final@ejemplo.com",
        "phone": "2222222",
        "govIdType": "FINAL_CONSUMER",
        "govIdNumber": "00000000000",
        "externalId": "",
        "additionalInfo": { "fiscal": "", "gender": "", "birthdate": "" },
        "billingInformation": {
          "email": "", "phone": "2222222", "address": "",
          "govIdType": "FINAL_CONSUMER", "externalId": "",
          "govIdNumber": "00000000000", "businessName": ""
        }
      },
      "totals": [
        {
          "currencyCode": "USD",
          "total": "100000",
          "subtotalWithoutTaxes": "87000",
          "taxValue": "13000",
          "taxes": [
            { "name": "IVA", "base": "87000", "rate": "0.15", "amount": "13000" }
          ]
        }
      ],
      "metadata": {}
    }
    ```

    `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`.
  </Tab>

  <Tab title="Colombia (CO)">
    ```json theme={null}
    {
      "country": "CO",
      "operation": "INVOICE",
      "businessDayDate": "2026-08-16",
      "createdAt": "2026-08-16T14:03:22.145Z",
      "orderCode": "CO-K039-1786901234",
      "store": {
        "code": "K039",
        "storeFiscalConfig": {
          "govIdType": "NIT",
          "govIdNumber": "9001234567",
          "company": {
            "govIdType": "NIT",
            "govIdNumber": "9001234567",
            "legalName": "COMERCIALIZADORA ANDINA S.A.S.",
            "tradeName": "KFC"
          },
          "metadata": {
            "claveTecnica": "fc8eac422eba16e22ffd8c6f94b3f40a6e38162c",
            "rangoFacturacion": {
              "prefijo": "SETP",
              "desde": "990000000",
              "hasta": "995000000",
              "resolucion": "18760000001",
              "vigenteHasta": "2027-08-16"
            },
            "rangoNotaCredito": { "prefijo": "NC", "desde": "1", "hasta": "100000" }
          }
        }
      },
      "device": {
        "uid": "52CAEA5A18D9B75F",
        "name": "CAJA 3",
        "platform": "android",
        "metadata": { "ip": "10.0.0.0" }
      },
      "client": {
        "uid": "usr_cf_001",
        "name": "Consumidor",
        "lastName": "final",
        "email": "consumidor.final@ejemplo.com",
        "phone": "2222222",
        "govIdType": "FINAL_CONSUMER",
        "govIdNumber": "00000000000",
        "externalId": "",
        "additionalInfo": { "fiscal": "", "gender": "", "birthdate": "" },
        "billingInformation": {
          "email": "", "phone": "2222222", "address": "",
          "govIdType": "FINAL_CONSUMER", "externalId": "",
          "govIdNumber": "00000000000", "businessName": ""
        }
      },
      "totals": [
        {
          "currencyCode": "COP",
          "total": "500000000",
          "subtotalWithoutTaxes": "420168100",
          "taxValue": "79831900",
          "taxes": [
            { "name": "IVA", "base": "420168100", "rate": "0.19", "amount": "79831900" }
          ]
        }
      ],
      "metadata": {}
    }
    ```

    Here `storeFiscalConfig.metadata` **does carry data**: the range prefix and the technical key
    the DIAN issues with the resolution. They belong to the store, are configured once and do not
    travel per sale — but without them you cannot compute the CUFE.
  </Tab>
</Tabs>

### 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

| Field                                          | Req. | What it is                                                                                                                                                                                                                                                                                                                                           |
| ---------------------------------------------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `country`                                      | yes  | ISO 3166-1 alpha-2. Routes to the country's fiscal system                                                                                                                                                                                                                                                                                            |
| `operation`                                    | yes  | `INVOICE` · `CANCEL` · `REVERSE` (future)                                                                                                                                                                                                                                                                                                            |
| `businessDayDate`                              | yes  | `YYYY-MM-DD`. **Business day of the sale**, local to the country. Not the authority's authorization date nor a UTC timestamp                                                                                                                                                                                                                         |
| `createdAt`                                    | yes  | ISO-8601, **always UTC, always with `Z`**. When the order was created. It does not replace `businessDayDate`: that one is the accounting day the document is issued under, this one is the wall-clock moment the sale happened, and for a late-night sale they differ. We normalize to UTC before sending, so you never have to interpret time zones |
| `orderCode`                                    | yes  | Sale code. Part of the idempotency key                                                                                                                                                                                                                                                                                                               |
| `store.code`                                   | yes  | The business's store code                                                                                                                                                                                                                                                                                                                            |
| `store.storeFiscalConfig.govIdType`            | yes  | Identification type of **whoever issues** (`RUC`, `CNPJ`, `NIT`…)                                                                                                                                                                                                                                                                                    |
| `store.storeFiscalConfig.govIdNumber`          | yes  | Identification of whoever issues — the branch                                                                                                                                                                                                                                                                                                        |
| `store.storeFiscalConfig.secondaryGovIdType`   | no   | Secondary identification (`INSCRICAO_ESTADUAL` and equivalents)                                                                                                                                                                                                                                                                                      |
| `store.storeFiscalConfig.secondaryGovIdNumber` | no   | Value of the above                                                                                                                                                                                                                                                                                                                                   |
| `store.storeFiscalConfig.company`              | no   | Legal entity owning the branch. In Brazil it differs from the issuer; in Ecuador it usually matches                                                                                                                                                                                                                                                  |
| `store.storeFiscalConfig.metadata`             | no   | Key-value **of the store**. Opaque                                                                                                                                                                                                                                                                                                                   |
| `device.uid`                                   | yes  | **Device identifier**, the business's. It is the only thing identifying the terminal                                                                                                                                                                                                                                                                 |
| `device.name`                                  | no   | Device name (`KIOSK`, `CAJA 3`)                                                                                                                                                                                                                                                                                                                      |
| `device.platform`                              | no   | `android`, `ios`, `web`… Informational                                                                                                                                                                                                                                                                                                               |
| `device.metadata`                              | no   | Key-value of the device (`ip`, …). Opaque                                                                                                                                                                                                                                                                                                            |
| `client`                                       | no   | **Who bought**, exactly as the point of sale holds it. See [3.5](#35-client-and-totals)                                                                                                                                                                                                                                                              |
| `totals`                                       | no   | **What was charged**, with the tax breakdown. Amounts are **integers in a string, ×10,000** — the same scale as the order event. Percentages are NOT scaled. See [3.5](#35-client-and-totals)                                                                                                                                                        |
| `metadata`                                     | no   | Key-value **of the sale**. Opaque                                                                                                                                                                                                                                                                                                                    |

**Empty fields are omitted.** We never send `""`. An absent field means "not configured"; an
empty string must not be read as a valid value.

<Warning>
  **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.
</Warning>

### 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

<Frame caption="Top: the issuer's NIT, the technical key and the invoicing range as a nested group.">
  <img src="https://mintcdn.com/firepos/IzYE_x-Eff6R13SN/images/fiscal/store-fiscal-metadata-1.jpg?fit=max&auto=format&n=IzYE_x-Eff6R13SN&q=85&s=7c581d5ef5b792862e3445a7f1687cb7" alt="Store fiscal configuration: the NIT and a key-value editor with claveTecnica and the rangoFacturacion group, with desde, hasta and prefijo." width="1522" height="784" data-path="images/fiscal/store-fiscal-metadata-1.jpg" />
</Frame>

<Frame caption="Further down the same screen: the credit note range and the preview of the JSON that will be sent.">
  <img src="https://mintcdn.com/firepos/IzYE_x-Eff6R13SN/images/fiscal/store-fiscal-metadata-2.jpg?fit=max&auto=format&n=IzYE_x-Eff6R13SN&q=85&s=27103438afb25fc34f484ce0b457bb5c" alt="Continuation of the same screen: the rangoNotaCredito group with desde, hasta and prefijo, and below it the preview of the resulting JSON." width="1522" height="784" data-path="images/fiscal/store-fiscal-metadata-2.jpg" />
</Frame>

What gets loaded there is **exactly** what you receive in `store.storeFiscalConfig.metadata`. In
the screenshot, that store is going to send you:

```json theme={null}
"metadata": {
  "claveTecnica": "fc8eac422eba16e22ffd8c6f94b3f40a6e38162c",
  "rangoFacturacion": {
    "prefijo": "SETP",
    "desde": "990000000",
    "hasta": "995000000",
    "resolucion": "18760000001",
    "vigenteHasta": "2027-08-16"
  },
  "rangoNotaCredito": {
    "prefijo": "NC",
    "desde": "1",
    "hasta": "100000"
  }
}
```

<Warning>
  **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.
</Warning>

<Note>
  **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**.
</Note>

<Info>
  **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`](/en/events/order-opened) ·
  [`order.completed`](/en/events/order-completed) ·
  [`order.cancelled`](/en/events/order-cancelled) ·
  [`order.invoiced`](/en/events/order-invoiced) ·
  [`order.reversed`](/en/events/order-reversed)

  It 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.

  ```json theme={null}
  "store": {
    "code": "K039",
    "storeFiscalConfig": {
      "enabled": true,
      "govIdType": "NIT",
      "govIdNumber": "9001234567",
      "company": { "…": "issuer's legal name" },
      "metadata": {
        "claveTecnica": "fc8eac…",
        "rangoFacturacion": { "prefijo": "SETP", "…": "" },
        "rangoNotaCredito": { "prefijo": "NC", "…": "" }
      }
    }
  }
  ```

  The field-by-field detail is in
  [`order.completed` → Fiscal data](/en/events/order-completed#fiscal-data).
</Info>

<Note>
  **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.
</Note>

**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

<Tabs>
  <Tab title="Ecuador (EC)">
    Currency **`USD`**. Today, a single tax: **`IVA` at 15%**.

    ```json theme={null}
    "totals": [
      {
        "currencyCode": "USD",
        "total": "100000",
        "subtotalWithoutTaxes": "87000",
        "taxValue": "13000",
        "taxes": [
          { "name": "IVA", "base": "87000", "rate": "0.15", "amount": "13000" }
        ]
      }
    ]
    ```

    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.
  </Tab>

  <Tab title="Colombia (CO)">
    Currency **`COP`**. Today, a single tax: **`IVA` at 19%**.

    ```json theme={null}
    "totals": [
      {
        "currencyCode": "COP",
        "total": "500000000",
        "subtotalWithoutTaxes": "420168100",
        "taxValue": "79831900",
        "taxes": [
          { "name": "IVA", "base": "420168100", "rate": "0.19", "amount": "79831900" }
        ]
      }
    ]
    ```

    **It goes into the CUFE hash.** The amount declared per tax is `ValImp`, and you translate the
    name into the DIAN's code — `IVA` → `01`. The slots that do not apply go as `0.00`: that is a
    rule of the technical annex, not something FIRE reports.
  </Tab>
</Tabs>

#### `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.

<Note>
  **`govIdType` comes from a closed catalog.** These are all the values FIRE emits, and no others
  will arrive:

  | Value            | What it is                          | Where it applies |
  | ---------------- | ----------------------------------- | ---------------- |
  | `FINAL_CONSUMER` | sale with no identified buyer       | every country    |
  | `CI`             | identity card                       | Ecuador          |
  | `RUC`            | Registro Único de Contribuyentes    | Ecuador          |
  | `CC`             | citizenship card                    | Colombia         |
  | `NIT`            | Número de Identificación Tributaria | Colombia         |

  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.
</Note>

<Note>
  **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.
</Note>

<Tabs>
  <Tab title="Ecuador (EC)">
    | Case           | `govIdType`          | `govIdNumber`              |
    | -------------- | -------------------- | -------------------------- |
    | Final consumer | `FINAL_CONSUMER`     | `00000000000`              |
    | Person         | `CI` — identity card | the card number, 10 digits |
    | Company        | `RUC`                | the RUC, 13 digits         |

    ```json theme={null}
    "client": {
      "uid": "8Z35YvBbgKVj67AJwZ3nFmAqrtk1",
      "name": "CONSUMIDOR",
      "lastName": "FINAL",
      "email": "consumidor.final@ejemplo.com",
      "phone": "2222222",
      "govIdType": "FINAL_CONSUMER",
      "govIdNumber": "00000000000",
      "externalId": "",
      "additionalInfo": { "fiscal": "", "gender": "", "birthdate": "" },
      "billingInformation": {
        "email": "", "phone": "2222222", "address": "",
        "govIdType": "FINAL_CONSUMER", "externalId": "",
        "govIdNumber": "00000000000", "businessName": ""
      }
    }
    ```

    The final consumer arrives as `FINAL_CONSUMER` with zeros. Translating that into what the SRI
    expects on the receipt is part of your implementation.
  </Tab>

  <Tab title="Colombia (CO)">
    | Case           | `govIdType`             | `govIdNumber`                    |
    | -------------- | ----------------------- | -------------------------------- |
    | Final consumer | `FINAL_CONSUMER`        | `00000000000`                    |
    | Person         | `CC` — citizenship card | the card number, no dots         |
    | Company        | `NIT`                   | the NIT, without the check digit |

    ```json theme={null}
    "client": {
      "uid": "usr_cf_001",
      "name": "Consumidor",
      "lastName": "final",
      "email": "consumidor.final@ejemplo.com",
      "phone": "2222222",
      "govIdType": "FINAL_CONSUMER",
      "govIdNumber": "00000000000",
      "externalId": "",
      "additionalInfo": { "fiscal": "", "gender": "", "birthdate": "" },
      "billingInformation": {
        "email": "", "phone": "2222222", "address": "",
        "govIdType": "FINAL_CONSUMER", "externalId": "",
        "govIdNumber": "00000000000", "businessName": ""
      }
    }
    ```

    **The final consumer does NOT arrive translated.** It arrives as `FINAL_CONSUMER` with the
    number in zeros, same as in any other country. That this resolves to the generic NIT
    `222222222222` with the name "Consumidor final" —Resolución 000042 of 2020— **is a DIAN rule,
    and therefore yours**.

    It is the same line as with the receipt number and the CUFE: FIRE sends the business fact
    —"this sale did not identify the buyer"— and you apply what the authority requires. And it is
    not an edge case: in restaurants it is the majority of sales.

    Mind the consequence: that identification goes into the CUFE hash as `NumAdq`. If you resolve
    it differently, the CUFE does not correspond to the invoice.
  </Tab>
</Tabs>

<Warning>
  **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:

  <CodeGroup>
    ```json In the request and in the event — the same scale theme={null}
    "totals": [
      {
        "currencyCode": "COP",
        "total": "500000000",
        "subtotalWithoutTaxes": "420168100",
        "taxValue": "79831900",
        "taxes": [
          { "name": "IVA", "base": "420168100", "rate": "0.19", "amount": "79831900" }
        ]
      }
    ]
    ```

    ```text How to read it theme={null}
    "500000000"  / 10,000 ->  50000.00  COP   <- total
    "420168100"  / 10,000 ->  42016.81  COP   <- taxable base
     "79831900"  / 10,000 ->   7983.19  COP   <- IVA declared
    ```
  </CodeGroup>

  **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](/en/fiscal-providers/in-events).
</Warning>

<Note>
  **`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.
</Note>

<Note>
  **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`.
</Note>

***

## 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**.

```json theme={null}
{
  "status": "INVOICED",
  "country": "EC",
  "orderCode": "FUEL-EC-1786553720451",
  "reused": false,
  "retryable": false,
  "authorizationMode": "ONLINE",
  "issuedAt": "2026-08-12T16:55:29Z",

  "document": { "…": "YOUR country's block — see 5.2" },

  "graphic": { "qr": "https://…" },

  "failure": null,
  "provider": { "name": "…", "version": "…", "reference": "…" },
  "metadata": {}
}
```

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](#52-document--the-numbered-document),
with one section per country. If you are implementing Ecuador, the block that concerns you is
Ecuador's and no other.

<Info>
  **`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.
</Info>

### 5.1 `status`

Two values, one per operation:

| Value       | When                               |
| ----------- | ---------------------------------- |
| `INVOICED`  | Response to `operation: "INVOICE"` |
| `CANCELLED` | Response to `operation: "CANCEL"`  |

**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.

<Tabs>
  <Tab title="Ecuador (EC) — SRI">
    ```json theme={null}
    "document": {
      "numeroComprobante": "001-020-000000123",
      "claveAcceso": "1208202601179141513200110010200000001231234567813",
      "establecimiento": "001",
      "puntoEmision": "020",
      "secuencial": "000000123",
      "ambiente": "2"
    }
    ```

    | Field               | Rule                                                                           |
    | ------------------- | ------------------------------------------------------------------------------ |
    | `numeroComprobante` | Required. The **visible** number, already assembled: `estab-ptoEmi-sequential` |
    | `claveAcceso`       | Required. Exactly **49 digits**                                                |
    | `establecimiento`   | Required. 3 digits                                                             |
    | `puntoEmision`      | Required. 3 digits — the point of emission the invoice was issued from         |
    | `secuencial`        | Required. 9 digits                                                             |
    | `ambiente`          | Required. `"1"` testing · `"2"` production                                     |

    <Warning>
      **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**.
    </Warning>

    <Note>
      **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.
    </Note>

    <Warning>
      **`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.
    </Warning>
  </Tab>

  <Tab title="Colombia (CO) — DIAN">
    ```json theme={null}
    "document": {
      "numeroComprobante": "SETP990000001",
      "cufe": "a2b4c6d8e0f2…  ← 96 hexadecimal characters",
      "prefijo": "SETP",
      "numeroDian": "990000001",
      "qrCode": "https://catalogo-vpfe.dian.gov.co/document/searchqr?documentkey=a2b4...",
      "ambiente": "1"
    }
    ```

    | Field               | Rule                                                                                                 |
    | ------------------- | ---------------------------------------------------------------------------------------------------- |
    | `numeroComprobante` | Required. The **visible** number, already assembled and exactly as printed. FIRE does not compose it |
    | `cufe`              | Required. The **CUFE** you computed: SHA-384 → **96 hexadecimal characters**                         |
    | `prefijo`           | Required. Prefix of the authorized numbering range                                                   |
    | `numeroDian`        | Required. The consecutive **alone**, without the prefix                                              |
    | `qrCode`            | Required. The DIAN catalog URL printed as a QR. **Colombia does not send `graphic`**                 |
    | `ambiente`          | Required. `"1"` production · `"2"` testing                                                           |

    <Warning>
      **Careful with `ambiente`: in Colombia it is the opposite of Ecuador.** The DIAN uses `1` for
      production and `2` for testing; the SRI uses `1` for testing and `2` for production. Send
      **your** authority's code, without normalizing — FIRE already knows how each country reads
      it, and this is exactly why the `document` block belongs to the country and is not a common
      model.
    </Warning>

    <Note>
      **You compute the CUFE, entirely.** It is a SHA-384 hash over a string concatenating amounts,
      dates, identifications and the technical key, in the order set by the DIAN's technical annex.

      FIRE **neither assembles that string nor hashes it**: it sends you the data —amounts in
      `totals`, buyer in `client`— and you apply the rule. It is the same criterion as with
      `numeroComprobante`: whoever is certified before the authority is the one who knows the
      algorithm, and if the annex changes, it changes on your side without FIRE deploying.

      The **technical key** does not travel in the request either: the DIAN hands it to you along
      with the authorized numbering range, so it lives on your side just like the establishment in
      Ecuador.
    </Note>

    <Warning>
      **Final consumer: the `222222222222` is yours to put in.**

      When the sale does not identify the buyer, `client` arrives with `FINAL_CONSUMER` and the
      number in zeros — the business fact, untranslated. Resolving it to the generic NIT
      `222222222222` with the name "Consumidor final" is a **DIAN rule** (Resolución 000042 of
      2020\) and it goes into the CUFE hash as `NumAdq`. See [3.5](#35-client-and-totals).

      FIRE does not translate it on purpose: the same criterion as with the receipt number and the
      CUFE. What belongs to the regime is resolved by whoever is certified before the authority.

      It is not an edge case: in real restaurant operations it is the majority of sales.
    </Warning>
  </Tab>
</Tabs>

#### 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.

```json theme={null}
"graphic": { "qr": "https://…" }
```

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.

<Warning>
  **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.
</Warning>

**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

```json theme={null}
"provider": { "name": "hio.fiscalization", "version": "2026.08.1", "reference": "HIO-91f3c2" }
```

| Field       | Type           | Required | What to put                                                                                                                         |
| ----------- | -------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `name`      | string \| null | ✓        | Who resolved this numbering. A stable identifier of the service, not a commercial brand nor a text that changes with the deployment |
| `version`   | string \| null | ✓        | Which version resolved it. It is what lets you say "as of 2026.08.1 it stopped happening" instead of comparing it against the date  |
| `reference` | string \| null | ✓        | **The provider's support reference**: the identifier you quote back to them so they can find this operation in their own records    |

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.

<Note>
  **`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.
</Note>

#### `metadata` — the opaque bag

```json theme={null}
"metadata": { "externalStoreCode": "K004", "externalDeviceUid": "52CAEA5A18D9B75F" }
```

**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**.

<Warning>
  **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.
</Warning>

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 code** — `422` for a configuration or data problem, `5xx` for a
transient one. Never with `200`.

```json theme={null}
{
  "orderCode": "FUEL-EC-1786553720451",
  "retryable": false,
  "failure": {
    "code": "UNMAPPED_STORE_IDENTITY",
    "message": "identidad fiscal de tienda no configurada: EC / tienda K0050",
    "details": [{ "field": "store.code", "issue": "not found in catalog" }]
  },
  "provider": { "name": "…", "version": "…", "reference": "…" },
  "metadata": {}
}
```

**`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.

<Note>
  **Use the same names as in the numbering.** Colombia's callback declares `cufe`, `prefijo`,
  `numeroDian`, `numeroComprobante`, `qrCode` and `ambiente` — exactly those of
  [5.2](#52-document--the-numbered-document). 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.
</Note>

<Warning>
  **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.
</Warning>
