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

# API v2: what changes

> Foreign currency, taxes by id and errors with a stable code in sales and purchases

Version 2 of the External API adds **multi-currency** and **taxes by id** to sales and purchases, and unifies the error format with a stable `code` that your integration can evaluate without reading the message text.

## Base URL

```
https://api.filexpress.app/api/external/v2
```

It uses the **same API Key** (`X-API-Key`) and the same permissions as v1, plus the new `currencies` permission. Authentication, the [rate limit](https://docs.filexpress.app/en/docs/api/introduccion.md#rate-limit) and the [business scope](https://docs.filexpress.app/en/docs/api/introduccion.md#data-scope) are identical.

> [!NOTE]
>
> v2 **does not replace** v1: it only exposes the endpoints that change. Everything else (customers, suppliers, products, inventory, the rest of HR, accounting, accounts receivable and payable, etc.) remains only in `/api/external/v1`. An integration can use v1 and v2 at the same time with the same API Key.

## Available endpoints

| Endpoint | Permission | Description |
| - | - | - |
| `GET /health` | None | Checks the connection. Responds `Working` without authentication. |
| `GET /currencies` | `currencies` | Base currency and active currencies of the business. See [Currencies and rates](https://docs.filexpress.app/en/docs/api/v2/monedas-y-tasas.md#list-currencies). |
| `GET /exchange-rates` | `currencies` | Exchange rates of the business, from most recent to oldest. |
| `GET /exchange-rates/resolve` | `currencies` | The rate that would be applied to a document on a given date. |
| `GET /countries/{code}` | `countries` | Documents, taxes, payment methods and capabilities of a country. See [Countries and capabilities](https://docs.filexpress.app/en/docs/api/v2/paises.md). |
| `POST /sales` | `sales` | [Create sale](https://docs.filexpress.app/en/docs/api/v2/ventas.md#create-sale) with currency and taxes by id. |
| `GET /sales/{id}` | `sales` | [Get sale](https://docs.filexpress.app/en/docs/api/v2/ventas.md#get-sale) with its currency reference. |
| `POST /expenses` | `expenses` | [Register purchase](https://docs.filexpress.app/en/docs/api/v2/compras.md#register-a-purchase) with currency. |
| `GET /expenses/{id}` | `expenses` | [Get purchase](https://docs.filexpress.app/en/docs/api/v2/compras.md#get-a-purchase) with its currency reference. |
| `GET /hr/payrolls`, `GET /hr/payrolls/{id}` | `hr.payrolls` | [Payrolls](https://docs.filexpress.app/en/docs/api/v2/planillas.md) with `branch_id` and branch filter. |
| `PUT /hr/payrolls/{id}/branch` | `hr.payrolls.write` | [Assign the branch](https://docs.filexpress.app/en/docs/api/v2/planillas.md#assign-branch) of a payroll. |

The `currencies` permission is assigned in **External integrations**, just like `sales` and `expenses`: an integration with full (inherited) access already includes it; one with selected permissions must have it checked. If you call a currency endpoint without it, you receive `403` with `required_scope: "currencies"`.

## What changes compared to v1

| Topic | v1 | v2 |
| - | - | - |
| Document currency | Does not accept a currency: amounts are always in the business base currency. Ignores `currency_code` and `exchange_rate`. | Accepts `currency_code` and `exchange_rate` in sales and purchases. Amounts are in the document currency. |
| `tax_ids` per line | Optional. | **Required** on every line of `products` (it can be empty: `[]`). |
| Taxes in sales | If the amount sent differs from the recalculated one, it is left as an internal warning. | **Strict mode**: an amount different from the recalculated one responds `422` with `code: "tax_invalid"`. |
| Totals | The client declares all amounts. | The client **can omit the total to pay** and FileXpress calculates lines, taxes, withholdings and totals (`data.calculated_totals`). See [sales](https://docs.filexpress.app/en/docs/api/v2/ventas.md#let-filexpress-calculate-the-totals) and [purchases](https://docs.filexpress.app/en/docs/api/v2/compras.md#let-filexpress-calculate-the-totals). Not available for `fc`. |
| Document response | No currency data. | Adds `currency_code`, `exchange_rate`, `foreign_total` and `base_currency_code`. |
| Errors | `{ "status": false, "message", "errors"? }` | Adds a stable `code`: `{ "status": false, "code", "message", "errors"? }`. |

Within v2, the rest of the contract (sale and purchase fields, totals, cost centers, point of sale, etc.) is the same as in v1: see [Sales](https://docs.filexpress.app/en/docs/api/facturacion/ventas.md) and [Purchases](https://docs.filexpress.app/en/docs/api/facturacion/compras.md) for the fields that don't change.

## How currency works

**1. The business has a base currency**

It is the currency in which FileXpress stores and posts everything. Query it with [`GET /currencies`](https://docs.filexpress.app/en/docs/api/v2/monedas-y-tasas#list-currencies) (`base_currency_code`).

**2. You send the document in its currency**

You pass `currency_code` (for example `GTQ`) and all the document amounts (lines, taxes, payments and totals) **in that currency**. If you omit `currency_code` or send the base currency, the document is treated as in v1.

**3. The server converts to the base currency**

It applies the document rate (the `exchange_rate` sent, or the one in effect on the document date), validates the taxes and issues. The electronic tax document (DTE) is issued in the **country's legal currency**, always in the base currency.

The **exchange rate** is expressed as *base currency units per 1 unit of foreign currency*. With a USD base and a rate of `0.128` for GTQ, 1 GTQ = 0.128 USD and 1,000 GTQ = 128.00 USD.

> [!WARNING]
>
> The currency must be **active** for the business (multi-currency setting in FileXpress) and, for electronic documents, the business base currency must be the legal currency of the branch's country. Otherwise the API responds `422` with `currency_not_enabled` or `base_currency_not_legal`.

## Safe retries with `Idempotency-Key`

If a `POST /sales` or `POST /expenses` request is cut off (timeout, network), you don't know whether the document was created. Sending it again could **duplicate it**. To avoid this, add the `Idempotency-Key` header with a unique value per operation (for example, a UUID that you generate and store):

```bash theme={null}
curl -X POST https://api.filexpress.app/api/external/v2/sales \
  -H "X-API-Key: fx_xxxxxxxx" \
  -H "Idempotency-Key: 6f1a2c3e-8b7d-4e9a-a1b2-0c3d4e5f6a7b" \
  -H "Content-Type: application/json" \
  -d '{ ... }'
```

| Situation | Result |
| - | - |
| First time with that key | Processed normally. |
| Same key, same body | Returns **the same response** (same code and body) without issuing again, with the `Idempotent-Replay: true` header. |
| Same key, different body | `422` with `code: "idempotency_key_reused"`. |
| The first request is still in progress | `409` with `code: "idempotency_in_progress"`. Wait and retry. |
| Key longer than 120 characters | `422` with `code: "validation_error"`. |

* The header is **optional**: without it everything works as before.
* The key is unique per integration and is remembered for **24 hours**.
* Only successful responses (`2xx`) and validation errors (`422`) are stored. If the server responds `5xx`, the key is released so you can retry with the same one.
* The order of the JSON properties does not change the body fingerprint.

## Errors and codes

Every v2 error response follows this format:

```json theme={null}
{
  "status": false,
  "code": "exchange_rate_not_found",
  "message": "No hay tasa de cambio para GTQ a la fecha 2026-10-21 ni anterior. Registre una tasa de cambio o envíe exchange_rate en el documento.",
  "errors": { "exchange_rate": "Sin tasa de cambio para GTQ al 2026-10-21." }
}
```

`errors` is optional and its shape depends on the error: a `field → message(s)` object in validation and currencies, or a list of `{ field, code, message, tax_id? }` objects in `tax_invalid`. Always evaluate `code`; the `message` text may change.

| `code` | HTTP | When |
| - | - | - |
| `currency_not_enabled` | `422` (`403` in `GET /currencies` and `/exchange-rates*`) | Multi-currency is not enabled for the business, the currency does not exist in the catalog or is not active for the business. |
| `exchange_rate_not_found` | `422` on documents, `404` on `/exchange-rates/resolve` | There is no rate on the document date or earlier and you did not send `exchange_rate`. |
| `exchange_rate_invalid` | `422` | `exchange_rate` is not numeric or is less than or equal to zero. |
| `base_currency_not_legal` | `422` | **Electronic** document in a foreign currency and the business base currency is not the country's legal currency. |
| `country_not_supported` | `404` | `GET /countries/{code}` for a country without configured issuance. |
| `country_data_invalid` | `422` | A sale's `country_data` does not meet the country's rules. See [Countries and capabilities](https://docs.filexpress.app/en/docs/api/v2/paises.md#country_data-a-countrys-own-data). |
| `tax_not_supported_in_expenses` | `422` | Automatic calculation of a purchase with a fixed per-unit tax other than COTRANS and FOVIAL. Send `totals`. |
| `payment_method_invalid` | `422` | A payment method in `payment_details` does not exist, is not valid for the branch's country, or `payment_method_id` is missing. See [Payment methods](https://docs.filexpress.app/en/docs/api/v2/ventas.md#payment-methods-use-the-id). |
| `idempotency_key_reused` | `422` | The same `Idempotency-Key` with a different body. |
| `idempotency_in_progress` | `409` | The first request with that `Idempotency-Key` is still in progress. |
| `totals_required` | `422` | End-consumer document (`fc`) without `totals.saleTotalToPayment`: automatic calculation is not available for `fc`. |
| `validation_error` | `422` | An input field is missing or invalid (for example, `products.0.tax_ids` missing). |
| `tax_invalid` | `422` | Taxes by id are invalid or do not match the recalculation. Only in sales. See [Taxes by id](https://docs.filexpress.app/en/docs/api/v2/ventas.md#taxes-by-id). |
| `unauthorized` | `401` | `X-API-Key` is missing or invalid. |
| `forbidden` | `403` | The integration is disabled or does not have the required permission. |
| `not_found` | `404` | Nonexistent resource. |
| `rate_limited` | `429` | Rate limit exceeded. |
| `error` | Other | Any other error (for example `500`). |

> [!NOTE]
>
> Responses generated by the access layer itself **before** reaching the endpoint (missing or invalid API Key, disabled integration, missing permission, rate limit) and the `404` for a resource that belongs to another business (for example, `GET /sales/{id}` with a foreign id) keep the `{ "status": false, "message": ... }` format and **may not include `code`**. If `code` is missing, use the HTTP code (`401`, `403`, `404`, `429`) as a fallback. The `403` for a missing permission adds `required_scope`.

## Next steps

> [card] [Currencies and rates](https://docs.filexpress.app/en/docs/api/v2/monedas-y-tasas.md)
>
> Query active currencies and rates.

> [card] [Sales](https://docs.filexpress.app/en/docs/api/v2/ventas.md)
>
> Issue sales in a foreign currency.

> [card] [Purchases](https://docs.filexpress.app/en/docs/api/v2/compras.md)
>
> Register purchases in a foreign currency.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.