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

# Ventas (v2)

> Emitir ventas en moneda extranjera con impuestos por id y consultar su referencia de moneda

**Permiso requerido:** `sales`

La venta v2 es la misma de [v1](/docs/api/facturacion/ventas.md) (mismos campos, punto de venta, centros de costo y tipos de documento) con tres diferencias: acepta **moneda** (`currency_code`, `exchange_rate`), exige **`tax_ids` por línea** y valida los impuestos en **modo estricto**. Sus errores llevan `code`; ver [API v2: qué cambia](/docs/api/v2/introduccion.md#errores-y-códigos).

> [!NOTE]
>
> Los campos que no se describen aquí (`observation`, `is_credit`, `credit_days`, `point_of_sale_id`, `cost_center_id`, `project_id`, etc.) son los de [Crear venta en v1](/docs/api/facturacion/ventas.md#crear-venta). El listado `GET /sales` sigue únicamente en v1.

## Crear venta

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

Ejemplo: venta (CCF) de un negocio con base USD, **en quetzales (GTQ)**, con IVA 13 % y FOVIAL. La tasa vigente de GTQ es `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": [
    { "code": "01", "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]
>
> **Todos los montos del documento van en la moneda de `currency_code`**: precios, subtotales, impuestos, pagos y totales. No los conviertas tú. El servidor aplica la tasa, convierte cada monto a la moneda base y emite el DTE en la moneda legal del país.

En el ejemplo, 750.00 GTQ gravados equivalen a 96.00 USD; el IVA de 97.50 GTQ, a 12.48 USD; y el FOVIAL de 15.63 GTQ, a 2.00 USD. El total a pagar es 863.13 GTQ = 110.48 USD.

### Campos nuevos o distintos de v1

| Campo | Tipo | Requerido | Descripción |
| - | - | - | - |
| `currency_code` | string | No | Código de 3 letras de la moneda del documento (por ejemplo `GTQ`). Debe estar activa para el negocio; ver [Monedas y tasas](/docs/api/v2/monedas-y-tasas.md). Si se omite o es la moneda base, el documento es en moneda base. |
| `exchange_rate` | number | No | Unidades de moneda **base** por 1 unidad de `currency_code`. Debe ser mayor que cero. Si se omite, se usa la tasa vigente a la `date` del documento (la del día o la última anterior). Solo aplica con moneda extranjera. |
| `products[].tax_ids` | array de uuid | **Sí** | IDs de los impuestos que aplican a la línea. La clave debe estar presente en cada línea; puede ser `[]` (por ejemplo, líneas exentas). Los IDs se obtienen con [`GET /taxes/branch/{branch_id}`](/docs/api/facturacion/catalogos) (v1). |
| `totals.taxes[]` | array | No | Impuestos aplicados al documento: `{ "tax_id", "amount" }` por impuesto, en la moneda del documento. |
| `totals.withholdings[]` | array | No | Retenciones y renta aplicadas: `{ "tax_id", "amount" }`, en la moneda del documento. |

### Respuesta `200`

```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"
  }
}
```

| Campo | Descripción |
| - | - |
| `total` | Total a pagar **en moneda base** (aquí 110.48 USD = 863.13 GTQ × 0.128). Es un número. |
| `currency_code` | Moneda del documento. Si el documento es en moneda base, es la moneda base. |
| `exchange_rate` | Tasa aplicada, como texto con 8 decimales. `"1.00000000"` en documentos en moneda base. |
| `foreign_total` | Total a pagar en la moneda del documento, como texto con 2 decimales. `null` si el documento es en moneda base. |
| `base_currency_code` | Moneda base del negocio. |

El resto de campos de la respuesta (`sale_id`, `generation_code`, `control_number`, etc.) son los de [v1](/docs/api/facturacion/ventas.md#respuesta-200).

Un documento en moneda base responde igual, con la moneda base como `currency_code`, tasa `"1.00000000"` y `foreign_total` `null`:

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

## Impuestos por id

En v2 cada línea declara los impuestos que le aplican con `tax_ids`, y el documento los totaliza en `totals.taxes` y `totals.withholdings`. Antes de transmitir el DTE, el servidor valida y **recalcula** los impuestos:

* Cada id debe existir, estar activo, **corresponder al país de la sucursal** y tener el efecto correcto (los de `tax_ids` y `taxes` suman; los de `withholdings` retienen).
* La suma de los montos declarados debe coincidir con `totalTax`, `totalRetein` y `totalRent`.
* En **modo estricto** (siempre en v2), si `totalTax`, `totalRetein` o `totalRent` difieren de lo que calcula el servidor en más de **0.02**, la venta se rechaza y no se transmite nada. Esto incluye la retención de IVA 1 % y la renta cuando las reglas del cliente las exigen (por ejemplo, un cliente grande con gravado de 100 o más).

Ejemplo de retención del 1 % para un cliente grande en USD (gravado 200.00, IVA 26.00, retención 2.00, total a pagar 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 } ]
  }
}
```

La **renta retenida** se envía igual: `totalRent` con su monto y una fila en `withholdings` con el `tax_id` de la renta. Un gravado de 300.00 con IVA 39.00 y renta del 10 % (30.00) paga 309.00.

### Error `tax_invalid`

```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)."
    }
  ]
}
```

En `tax_invalid`, `errors` es una lista; cada elemento trae `field`, `message`, un `code` de detalle y, cuando aplica, `tax_id`. Los nombres de `field` son los internos del documento (`invoiceResumenDetail.*`, `invoiceProductsDetail.tax_ids`).

| `code` de detalle | Causa |
| - | - |
| `tax_not_found` | El impuesto no existe o está inactivo. |
| `tax_wrong_effect` | El impuesto no se puede usar en ese campo (por ejemplo, una retención en `tax_ids`). |
| `tax_country_mismatch` | El impuesto no corresponde al país de la sucursal. |
| `tax_not_in_lines` | Un impuesto de `totals.taxes` no está en ninguna línea. |
| `tax_totals_inconsistent` | La suma de `totals.taxes` no coincide con `totalTax`. |
| `tax_amount_mismatch` | `totalTax` difiere del monto recalculado por el servidor. |
| `withholding_totals_inconsistent` | La suma de `totals.withholdings` no coincide con `totalRetein` / `totalRent`. |
| `withholding_amount_mismatch` | `totalRetein` o `totalRent` difieren de lo que exigen las reglas del cliente (incluye una retención obligatoria que no se declaró). |

Si un valor de `tax_ids` no es un UUID, la respuesta es `validation_error` (no `tax_invalid`).

> [!NOTE]
>
> Alcance de la validación: el servidor **no compara** el monto declarado de los impuestos fijos por unidad (por ejemplo FOVIAL y COTRANS) con cantidad × valor del catálogo; valida los porcentuales (IVA), las retenciones y la renta. Además, una venta sin ningún valor en `tax_ids`, `totals.taxes` ni `totals.withholdings` se trata como legada y no se valida. En moneda extranjera, los montos de los impuestos fijos se envían en la moneda del documento.

## Errores de moneda

Se validan antes de emitir; ningún error guarda ni transmite el documento.

| `code` | HTTP | Causa |
| - | - | - |
| `currency_not_enabled` | `422` | Multi-moneda no habilitada, moneda inexistente o no activa para el negocio. |
| `exchange_rate_not_found` | `422` | Sin `exchange_rate` y sin tasa vigente a la fecha del documento ni anterior. |
| `exchange_rate_invalid` | `422` | `exchange_rate` no numérica o menor o igual que cero. |
| `base_currency_not_legal` | `422` | Documento electrónico en moneda extranjera y la moneda base del negocio no es la moneda legal del país (SV y US: USD; GT: GTQ). |
| `validation_error` | `422` | Falta `products.*.tax_ids` u otro campo inválido. |
| `tax_invalid` | `422` | Ver [arriba](#error-tax_invalid). |

```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."] }
}
```

## Consultar venta

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

Devuelve lo mismo que [v1](/docs/api/facturacion/ventas.md#consultar-venta) más la referencia de moneda. Los montos de `totals` y de `products` están **en moneda base**; el total en la moneda del documento está en `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"
  }
}
```

Una venta de otro negocio o inexistente responde `404` con `{ "status": false, "message": "..." }`.

> [!TIP]
>
> `GET /receivables` (v1) también expone la referencia de moneda de cada documento por cobrar. Ver [Cuentas por cobrar](/docs/api/cuentas-por-cobrar.md#documentos-pendientes).


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