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

# Sales (v2)

> Issue sales in foreign currency with taxes by id and query their currency reference

**Required permission:** `sales`

The v2 sale is the same as [v1](https://docs.filexpress.app/en/docs/api/facturacion/ventas.md) (same fields, point of sale, cost centers, and document types) with three differences: it accepts a **currency** (`currency_code`, `exchange_rate`), it requires **`tax_ids` per line**, and it validates taxes in **strict mode**. Its errors carry a `code`; see [API v2: what changes](https://docs.filexpress.app/en/docs/api/v2/introduccion.md#errors-and-codes).

> [!NOTE]
>
> Fields not described here (`observation`, `is_credit`, `credit_days`, `point_of_sale_id`, `cost_center_id`, `project_id`, etc.) are the ones from [Create sale in v1](https://docs.filexpress.app/en/docs/api/facturacion/ventas.md#create-sale). The `GET /sales` listing remains v1 only.

## Create sale

```http theme={null}
POST /sales
```

Example: a sale (CCF, tax credit invoice) for a USD-based business, **in quetzales (GTQ)**, with 13 % VAT (IVA) and FOVIAL. The current GTQ rate is `0.128` (1 GTQ = 0.128 USD).

```json theme={null}
{
  "branch_id": "9d1c5a3e-4b7f-4e2a-9c1d-2f6b8a0e7c11",
  "customer_id": "3f8a7b21-5c4d-4e6f-8a9b-0c1d2e3f4a5b",
  "document_type": "ccf",
  "date": "2026-10-21T10:00:00",
  "currency_code": "GTQ",
  "exchange_rate": 0.128,
  "payment_details": [
    { "payment_method_id": "<id-del-medio-de-pago>", "amount": 863.13 }
  ],
  "products": [
    {
      "id": "b1c2d3e4-1111-4a2b-9c3d-4e5f6a7b8c9d",
      "name": "Servicio de transporte",
      "quantity": 5,
      "price_before_taxes": 100.00,
      "discount": 0,
      "subtotal": 500.00,
      "sale_taxed": 500.00,
      "sale_exent": 0,
      "sale_not_suject": 0,
      "tax_ids": ["<id-del-IVA>", "<id-del-FOVIAL>"]
    },
    {
      "id": "b1c2d3e4-2222-4a2b-9c3d-4e5f6a7b8c9d",
      "name": "Servicio de carga",
      "quantity": 5,
      "price_before_taxes": 50.00,
      "discount": 0,
      "subtotal": 250.00,
      "sale_taxed": 250.00,
      "sale_exent": 0,
      "sale_not_suject": 0,
      "tax_ids": ["<id-del-IVA>", "<id-del-FOVIAL>"]
    }
  ],
  "totals": {
    "netTotals": 750.00,
    "saleTotalNotSuject": 0,
    "saleTotalExent": 0,
    "saleTotalTax": 750.00,
    "totalTax": 97.50,
    "totalRetein": 0,
    "totalRent": 0,
    "saleTotalToPayment": 863.13,
    "taxes": [
      { "tax_id": "<id-del-IVA>", "amount": 97.50 },
      { "tax_id": "<id-del-FOVIAL>", "amount": 15.63 }
    ],
    "withholdings": []
  }
}
```

> [!WARNING]
>
> **All document amounts are in the currency of `currency_code`**: prices, subtotals, taxes, payments, and totals. Do not convert them yourself. The server applies the rate, converts each amount to the base currency, and issues the DTE (electronic tax document) in the country's legal currency.

In the example, 750.00 GTQ taxed equals 96.00 USD; the 97.50 GTQ VAT, 12.48 USD; and the 15.63 GTQ FOVIAL, 2.00 USD. The total to pay is 863.13 GTQ = 110.48 USD.

### New or different fields from v1

| Field | Type | Required | Description |
| - | - | - | - |
| `currency_code` | string | No | 3-letter code of the document's currency (for example `GTQ`). It must be active for the business; see [Currencies and rates](https://docs.filexpress.app/en/docs/api/v2/monedas-y-tasas.md). If omitted or equal to the base currency, the document is in base currency. |
| `exchange_rate` | number | No | Units of **base** currency per 1 unit of `currency_code`. Must be greater than zero. If omitted, the rate in effect on the document's `date` is used (that day's rate or the most recent earlier one). Only applies to foreign currency. |
| `products[].tax_ids` | array of uuid | **Yes** | IDs of the taxes that apply to the line. The key must be present on every line; it can be `[]` (for example, exempt lines). Get the IDs with [`GET /taxes/branch/{branch_id}`](https://docs.filexpress.app/en/docs/api/facturacion/catalogos) (v1). |
| `totals.taxes[]` | array | No | Taxes applied to the document: `{ "tax_id", "amount" }` per tax, in the document's currency. |
| `totals.withholdings[]` | array | No | Withholdings and income tax applied: `{ "tax_id", "amount" }`, in the document's currency. |

### `200` response

```json theme={null}
{
  "status": true,
  "message": "Invoice created successfully",
  "data": {
    "sale_id": "6c0e9f0a-7d41-4d58-b0a6-2b1f5e7d9c33",
    "generation_code": "6F1E2D3C-8A4B-4C5D-9E6F-0A1B2C3D4E5F",
    "control_number": "DTE-03-M001P001-000000000000045",
    "status": "PROCESADO",
    "issue_time": "2026-10-21 10:00:00",
    "stamp": "2026A1B2C3D4E5F6",
    "point_of_sale_id": "7c20e1a9-3b5d-4f8e-a6c1-9d2e4f6a8b0c",
    "establishment_code": "M001",
    "point_of_sale_code": "P001",
    "total": 110.48,
    "currency_code": "GTQ",
    "exchange_rate": "0.12800000",
    "foreign_total": "863.13",
    "base_currency_code": "USD"
  }
}
```

| Field | Description |
| - | - |
| `total` | Total to pay **in base currency** (here 110.48 USD = 863.13 GTQ × 0.128). It is a number. |
| `currency_code` | Document currency. If the document is in base currency, it is the base currency. |
| `exchange_rate` | Rate applied, as text with 8 decimals. `"1.00000000"` on base-currency documents. |
| `foreign_total` | Total to pay in the document's currency, as text with 2 decimals. `null` if the document is in base currency. |
| `base_currency_code` | The business's base currency. |

The remaining response fields (`sale_id`, `generation_code`, `control_number`, etc.) are those from [v1](https://docs.filexpress.app/en/docs/api/facturacion/ventas.md#200-response).

A document in base currency responds the same way, with the base currency as `currency_code`, rate `"1.00000000"`, and `foreign_total` `null`:

```json theme={null}
{ "currency_code": "USD", "exchange_rate": "1.00000000", "foreign_total": null, "base_currency_code": "USD" }
```

## Taxes by id

In v2 each line declares the taxes that apply to it with `tax_ids`, and the document totals them in `totals.taxes` and `totals.withholdings`. Before transmitting the DTE, the server validates and **recalculates** the taxes:

* Each id must exist, be active, **belong to the branch's country**, and have the correct effect (those in `tax_ids` and `taxes` add; those in `withholdings` withhold).
* The sum of the declared amounts must match `totalTax`, `totalRetein`, and `totalRent`.
* In **strict mode** (always on in v2), if `totalTax`, `totalRetein`, or `totalRent` differ from what the server calculates by more than **0.02**, the sale is rejected and nothing is transmitted. This includes the 1 % VAT withholding and income tax when the customer's rules require them (for example, a large taxpayer with taxed sales of 100 or more).

Example of a 1 % withholding for a large taxpayer in USD (taxed 200.00, VAT 26.00, withholding 2.00, total to pay 224.00):

```json theme={null}
{
  "products": [
    {
      "id": "b1c2d3e4-1111-4a2b-9c3d-4e5f6a7b8c9d",
      "name": "Servicio",
      "quantity": 4,
      "price_before_taxes": 50.00,
      "discount": 0,
      "subtotal": 200.00,
      "sale_taxed": 200.00,
      "sale_exent": 0,
      "sale_not_suject": 0,
      "tax_ids": ["<id-del-IVA>"]
    }
  ],
  "totals": {
    "netTotals": 200.00,
    "saleTotalNotSuject": 0,
    "saleTotalExent": 0,
    "saleTotalTax": 200.00,
    "totalTax": 26.00,
    "totalRetein": 2.00,
    "totalRent": 0,
    "saleTotalToPayment": 224.00,
    "taxes": [ { "tax_id": "<id-del-IVA>", "amount": 26.00 } ],
    "withholdings": [ { "tax_id": "<id-de-la-retención-1%>", "amount": 2.00 } ]
  }
}
```

**Withheld income tax** is sent the same way: `totalRent` with its amount and a row in `withholdings` with the income tax's `tax_id`. Taxed sales of 300.00 with VAT of 39.00 and 10 % income tax (30.00) pay 309.00.

### `tax_invalid` error

```json theme={null}
{
  "status": false,
  "code": "tax_invalid",
  "message": "Impuestos inválidos",
  "errors": [
    {
      "field": "invoiceResumenDetail.totalTax",
      "code": "tax_amount_mismatch",
      "message": "totalTax enviado (20) difiere del calculado (13)."
    }
  ]
}
```

In `tax_invalid`, `errors` is a list; each element carries `field`, `message`, a detail `code`, and, when applicable, `tax_id`. The `field` names are the document's internal ones (`invoiceResumenDetail.*`, `invoiceProductsDetail.tax_ids`).

| Detail `code` | Cause |
| - | - |
| `tax_not_found` | The tax does not exist or is inactive. |
| `tax_wrong_effect` | The tax cannot be used in that field (for example, a withholding in `tax_ids`). |
| `tax_country_mismatch` | The tax does not belong to the branch's country. |
| `tax_not_in_lines` | A tax in `totals.taxes` is not in any line. |
| `tax_totals_inconsistent` | The sum of `totals.taxes` does not match `totalTax`. |
| `tax_amount_mismatch` | `totalTax` differs from the amount recalculated by the server. |
| `withholding_totals_inconsistent` | The sum of `totals.withholdings` does not match `totalRetein` / `totalRent`. |
| `withholding_amount_mismatch` | `totalRetein` or `totalRent` differ from what the customer's rules require (includes a mandatory withholding that was not declared). |

If a `tax_ids` value is not a UUID, the response is `validation_error` (not `tax_invalid`).

> [!NOTE]
>
> Validation scope: the server **does not compare** the declared amount of fixed-per-unit taxes (for example FOVIAL and COTRANS) against quantity × catalog value; it validates percentage-based taxes (VAT), withholdings, and income tax. Also, a sale with no value in `tax_ids`, `totals.taxes`, or `totals.withholdings` is treated as legacy and is not validated. In foreign currency, fixed-tax amounts are sent in the document's currency.

## Currency errors

They are validated before issuing; no error saves or transmits the document.

| `code` | HTTP | Cause |
| - | - | - |
| `currency_not_enabled` | `422` | Multi-currency not enabled, or the currency does not exist or is not active for the business. |
| `exchange_rate_not_found` | `422` | No `exchange_rate` and no rate in effect on the document date or earlier. |
| `exchange_rate_invalid` | `422` | `exchange_rate` is non-numeric or less than or equal to zero. |
| `base_currency_not_legal` | `422` | Electronic document in foreign currency and the business's base currency is not the country's legal currency (SV and US: USD; GT: GTQ). |
| `validation_error` | `422` | `products.*.tax_ids` is missing or another field is invalid. |
| `tax_invalid` | `422` | See [above](#tax_invalid-error). |

```json theme={null}
{
  "status": false,
  "code": "validation_error",
  "message": "Validation errors",
  "errors": { "products.0.tax_ids": ["The products.0.tax_ids field must be present."] }
}
```

## Get sale

```http theme={null}
GET /sales/{sale_id}
```

Returns the same as [v1](https://docs.filexpress.app/en/docs/api/facturacion/ventas.md#get-sale) plus the currency reference. The amounts in `totals` and `products` are **in base currency**; the total in the document's currency is in `foreign_total`.

```json theme={null}
{
  "status": true,
  "data": {
    "sale_id": "6c0e9f0a-7d41-4d58-b0a6-2b1f5e7d9c33",
    "generation_code": "6F1E2D3C-8A4B-4C5D-9E6F-0A1B2C3D4E5F",
    "control_number": "DTE-03-M001P001-000000000000045",
    "document_type": "ccf",
    "status": "PROCESADO",
    "issue_time": "2026-10-21 10:00:00",
    "stamp": "2026A1B2C3D4E5F6",
    "payment_status": "payed",
    "is_credit": false,
    "customer": { "id": "3f8a7b21-5c4d-4e6f-8a9b-0c1d2e3f4a5b", "legal_name": "Grupo Alfa, S.A. de C.V.", "customer_name": "Grupo Alfa" },
    "totals": { "sale_total": 110.48, "sale_to_payment": 110.48 },
    "products": [
      { "name": "Servicio de transporte", "quantity": 5, "individual_price": 12.80, "subtotal": 64.00 }
    ],
    "currency_code": "GTQ",
    "exchange_rate": "0.12800000",
    "foreign_total": "863.13",
    "base_currency_code": "USD"
  }
}
```

A sale from another business or one that does not exist responds `404` with `{ "status": false, "message": "..." }`.

> [!TIP]
>
> `GET /receivables` (v1) also exposes the currency reference of each receivable document. See [Accounts receivable](https://docs.filexpress.app/en/docs/api/cuentas-por-cobrar.md#pending-documents).

## Payment methods: use the `id`

Each `payment_details` line identifies its payment method with **`payment_method_id`**, the `id` returned by [`GET /countries/{code}`](https://docs.filexpress.app/en/docs/api/v2/paises) in `payment_methods[]`. The code (`"01"`) is **not unique**: another country may use the same code for a different method, and within a country there can be repeated methods.

| Field | Description |
| - | - |
| `payment_method_id` | Recommended. Must exist and be valid for the branch's country. |
| `code` | Deprecated, fallback only if you do not send the `id`. It is resolved within the branch's country and always gives the same method. |
| `amount` | Amount in the document's currency. |

If the payment method does not exist, is not from the country, or both the `id` and the `code` are missing, the sale is rejected with `422` and `code: "payment_method_invalid"` (in `errors`, the field `payment_details.N.payment_method_id`). Previously an unknown code left the sale without that payment, with no warning.

## Let FileXpress calculate the totals

Instead of declaring every amount, you can send only the lines and FileXpress calculates each line's amounts, the taxes, the withholdings, and the total to pay. It is the recommended mode for new integrations: there can be no difference of cents between your system and ours.

**When it activates:** when `totals` does not include `saleTotalToPayment`. If you send it, the mode above is used (you declare and we validate).

**What you send per line**

| Field | Description |
| - | - |
| `quantity` | Quantity (greater than zero). |
| `price_before_taxes` | Unit price before taxes. |
| `discount` | Line discount as an **amount** (optional). |
| `discount_percent` | Discount as a **fraction** between 0 and 1 (`0.10` = 10 %), optional. Do not send it together with `discount`. |
| `taxability` | `taxed` (default), `exempt`, or `not_subject`. |
| `tax_ids` | Line taxes (required; can be empty). |

`totals` can include only `freight` and `insurance`. If the customer qualifies (for example, a large taxpayer or a public institution), the 1 % withholding is applied automatically.

```json theme={null}
{
  "branch_id": "9d1c5a3e-4b7f-4e2a-9c1d-2f6b8a0e7c11",
  "customer_id": "3f8a7b21-5c4d-4e6f-8a9b-0c1d2e3f4a5b",
  "document_type": "ccf",
  "date": "2026-10-21T10:00:00",
  "products": [
    { "id": "b1c2d3e4-1111-4a2b-9c3d-4e5f6a7b8c9d", "name": "Servicio", "quantity": 4, "price_before_taxes": 50.00, "tax_ids": ["<id-del-IVA>"] }
  ]
}
```

The response includes what was calculated in `data.calculated_totals`:

```json theme={null}
{
  "status": true,
  "data": {
    "sale_id": "…",
    "total": 224.00,
    "calculated_totals": {
      "netTotals": 200, "saleTotalTax": 200, "saleTotalExent": 0, "saleTotalNotSuject": 0,
      "totalTax": 26, "totalRetein": 2, "totalRent": 0, "freight": 0, "insurance": 0,
      "saleTotalToPayment": 224,
      "taxes": [ { "tax_id": "<id-del-IVA>", "amount": 26 } ],
      "withholdings": [ { "tax_id": "<id-de-la-retención>", "amount": 2 } ]
    }
  }
}
```

**Calculation rules** (the same ones the New sale screen uses):

* Everything is calculated with exact decimals and rounded **only once** per total, rounding half up (`0.115` → `0.12`).
* The taxed amount is the sum of `quantity × price × (1 − discount)` **without rounding per line**, rounded at the end; exempt and not-subject amounts are rounded per line.
* Each percentage-based tax is the sum of each line's taxed amount times its rate, rounded at the end. Fixed-per-unit taxes (FOVIAL, COTRANS) are `quantity × catalog value`.
* In **foreign currency** the amounts are in the document's currency; the catalog's fixed taxes (in base currency) are expressed in it by dividing them by the rate.
* The amounts we return never have more decimals than the currency.

> [!WARNING]
>
> Automatic calculation is **not available for final consumer (`fc`)**: VAT is included in the price and its composition depends on the branch's configuration. For `fc`, send the full `totals`. It responds `422` with `code: "totals_required"`.

| `code` | When |
| - | - |
| `totals_required` | `fc` document without `totals.saleTotalToPayment`. |
| `validation_error` | Quantity ≤ 0, invalid price, unknown `taxability`, `discount_percent` outside 0–1, or `discount` and `discount_percent` together. |
| `tax_invalid` | `tax_ids` that do not exist, belong to another country, or have the wrong effect. |


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