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

# Inventario

> Existencias, movimientos, lotes, alertas, ajustes y transferencias entre sucursales

| Permiso | Endpoints |
| - | - |
| `inventory.read` | `GET /inventory/stock`, `/movements`, `/batches`, `/alerts`, `/transfers`, `/transfers/{id}` |
| `inventory.adjustments` | `POST /inventory/adjustments` |
| `inventory.transfers` | `POST /inventory/transfers`, `/transfers/{id}/complete`, `/transfers/{id}/cancel` |

<Note>
  Son permisos **explícitos**: una integración solo los tiene si el administrador los marca uno por uno en **Integraciones externas**. Las integraciones existentes no los reciben automáticamente.
</Note>

* Solo los productos **físicos** manejan inventario; los servicios no aparecen en existencias ni se pueden ajustar o transferir.
* Todo se limita al negocio de la integración: una sucursal, producto o transferencia de otro negocio responde `404`.
* `unit_cost` es siempre el **costo** del producto (no el precio de venta). Los ajustes y transferencias quedan listos para contabilizarse a costo desde [Procesar documentos](/docs/contabilidad/procesar-documentos#ajustes-de-inventario-y-transferencias).
* Las escrituras aceptan el encabezado [`Idempotency-Key`](/docs/api/introduccion#idempotencia). Envíalo siempre para no duplicar un ajuste o una transferencia al reintentar.

## Consultar existencias

```http theme={null}
GET /inventory/stock?branch_id={branch_id}&low_stock=true
```

| Parámetro | Tipo | Requerido | Descripción |
| - | - | - | - |
| `branch_id` | string | Sí | Sucursal. |
| `search` | string | No | Busca en nombre, SKU o código de barras (máx. 100 caracteres). |
| `low_stock` | boolean | No | `true`: solo productos con stock bajo (existencia menor o igual al mínimo, cuando el mínimo es mayor que 0). |
| `per_page` | integer | No | 1 a 200 (por defecto 50). |

```bash theme={null}
curl "https://api.filexpress.app/api/external/inventory/stock?branch_id=9d1c5a3e-...&low_stock=true" \
  -H "X-API-Key: fx_xxxxxxxx" -H "Accept: application/json"
```

```json theme={null}
{
  "status": true,
  "data": {
    "data": [
      {
        "product_id": "9d2a...",
        "branch_id": "9d1c5a3e-...",
        "sku": "CAF-500",
        "barcode": "7401234567890",
        "name": "Café molido 500 g",
        "availability": 4,
        "min_stock": 10,
        "is_low_stock": true,
        "unit_cost": 3.25
      }
    ],
    "pagination": { "current_page": 1, "last_page": 1, "per_page": 50, "total": 1 }
  }
}
```

| Campo | Descripción |
| - | - |
| `availability` | Existencia actual. |
| `min_stock` | Mínimo configurado en el producto (`0` = sin mínimo). |
| `is_low_stock` | `true` si `min_stock > 0` y `availability <= min_stock`. |
| `unit_cost` | Costo del producto. |

## Consultar movimientos

```http theme={null}
GET /inventory/movements?branch_id={branch_id}&start_date=2026-10-01&end_date=2026-10-31
```

| Parámetro | Tipo | Requerido | Descripción |
| - | - | - | - |
| `branch_id` | string | Sí | Sucursal. |
| `start_date`, `end_date` | date | Sí | Rango `YYYY-MM-DD` (máximo 93 días). |
| `product_id` | string | No | Filtra por producto. |
| `source` | string | No | `adjustment` (ajustes) o `transfer` (transferencias). |
| `per_page` | integer | No | 1 a 500 (por defecto 100). |

```json theme={null}
{
  "status": true,
  "data": {
    "data": [
      {
        "id": "9d3b...",
        "branch_id": "9d1c5a3e-...",
        "product": { "id": "9d2a...", "sku": "CAF-500", "name": "Café molido 500 g" },
        "type": "out",
        "quantity": 2,
        "unit_cost": 3.25,
        "source": "adjustment",
        "reason": "Merma por empaque dañado",
        "notes": null,
        "branch_transfer_id": null,
        "created_at": "2026-10-06T10:15:00-06:00"
      }
    ],
    "pagination": { "current_page": 1, "last_page": 1, "per_page": 100, "total": 1 }
  }
}
```

* `type`: `in` (entrada) u `out` (salida). `quantity` siempre es positiva.
* `source`: `adjustment`, `transfer`, `sale`, `purchase` o `null` (movimientos antiguos sin origen registrado).
* `unit_cost` puede ser `null` en movimientos anteriores a esta versión.

## Consultar lotes

```http theme={null}
GET /inventory/batches?branch_id={branch_id}&status=expiring&days=15
```

| Parámetro | Tipo | Requerido | Descripción |
| - | - | - | - |
| `branch_id` | string | Sí | Sucursal. |
| `product_id` | string | No | Filtra por producto. |
| `status` | string | No | `available` (con existencia), `expiring` (por vencer) o `expired` (vencidos). |
| `days` | integer | No | Ventana para `expiring`, 1 a 365 (por defecto 30). |
| `per_page` | integer | No | 1 a 200 (por defecto 50). |

Los lotes se ordenan por fecha de vencimiento. Cada elemento:

```json theme={null}
{
  "id": "9d4c...",
  "branch_id": "9d1c5a3e-...",
  "product": { "id": "9d2a...", "sku": "AMX-500", "name": "Amoxicilina 500 mg" },
  "batch_number": "L2026-031",
  "expiration_date": "2026-10-20",
  "days_to_expire": 14,
  "quantity": 100,
  "available_quantity": 37,
  "is_expired": false,
  "is_removed": false
}
```

`days_to_expire` es negativo si el lote ya venció.

## Alertas

```http theme={null}
GET /inventory/alerts?branch_id={branch_id}&days=30
```

Reúne en una sola llamada el stock bajo, los lotes por vencer y los vencidos con existencia (hasta 500 de cada tipo).

| Parámetro | Tipo | Requerido | Descripción |
| - | - | - | - |
| `branch_id` | string | Sí | Sucursal. |
| `days` | integer | No | Ventana de lotes por vencer, 1 a 365 (por defecto **30**). |

```json theme={null}
{
  "status": true,
  "data": {
    "days": 30,
    "low_stock": [ { "product_id": "9d2a...", "sku": "CAF-500", "availability": 4, "min_stock": 10, "is_low_stock": true, "...": "..." } ],
    "expiring_batches": [ { "batch_number": "L2026-031", "days_to_expire": 14, "...": "..." } ],
    "expired_batches": []
  }
}
```

`low_stock` usa el formato de [existencias](#consultar-existencias) y los lotes el de [lotes](#consultar-lotes).

## Registrar ajuste

```http theme={null}
POST /inventory/adjustments
```

Registra un **faltante/merma** (`out`) o un **sobrante** (`in`) y actualiza la existencia de inmediato. El movimiento guarda el costo unitario del producto en ese momento.

| Campo | Tipo | Requerido | Descripción |
| - | - | - | - |
| `branch_id` | string | Sí | Sucursal. |
| `product_id` | string | Sí | Producto de esa sucursal. |
| `type` | string | Sí | `in` (sobrante) u `out` (faltante/merma). |
| `quantity` | number | Sí | Mayor que 0. |
| `reason` | string | Sí | Motivo (máx. 255 caracteres). |
| `notes` | string | No | Notas (máx. 500 caracteres). |

```bash theme={null}
curl -X POST https://api.filexpress.app/api/external/inventory/adjustments \
  -H "X-API-Key: fx_xxxxxxxx" \
  -H "Idempotency-Key: conteo-2026-10-06-CAF-500" \
  -H "Content-Type: application/json" \
  -d '{
    "branch_id": "9d1c5a3e-...",
    "product_id": "9d2a...",
    "type": "out",
    "quantity": 2,
    "reason": "Merma por empaque dañado"
  }'
```

**Respuesta `201`**: el movimiento y la existencia actualizada.

```json theme={null}
{
  "status": true,
  "data": {
    "adjustment": {
      "id": "9d3b...",
      "branch_id": "9d1c5a3e-...",
      "product": { "id": "9d2a...", "sku": "CAF-500", "name": "Café molido 500 g" },
      "type": "out",
      "quantity": 2,
      "unit_cost": 3.25,
      "source": "adjustment",
      "reason": "Merma por empaque dañado",
      "notes": null,
      "branch_transfer_id": null,
      "created_at": "2026-10-06T10:15:00-06:00"
    },
    "stock": {
      "product_id": "9d2a...",
      "branch_id": "9d1c5a3e-...",
      "sku": "CAF-500",
      "barcode": "7401234567890",
      "name": "Café molido 500 g",
      "availability": 2,
      "min_stock": 10,
      "is_low_stock": true,
      "unit_cost": 3.25
    }
  }
}
```

<Note>
  El ajuste no valida la existencia disponible: un faltante mayor que la existencia deja el producto en negativo. Consulta [existencias](#consultar-existencias) antes si tu proceso lo requiere.
</Note>

**Errores**

| Código | Causa |
| - | - |
| `404` | `Branch not found` / `Product not found in this branch`. |
| `422` | Validación, o `Solo los productos físicos manejan inventario.` |
| `409` | Conflicto de `Idempotency-Key`. |

## Transferencias entre sucursales

Una transferencia tiene tres estados:

| Estado | Significado |
| - | - |
| `pending` | Creada; la existencia todavía no se ha movido. |
| `completed` | La existencia salió del origen y entró al destino. |
| `cancelled` | Anulada sin mover existencias. Solo se cancelan transferencias `pending`. |

### Crear transferencia

```http theme={null}
POST /inventory/transfers
```

Crea la transferencia en estado `pending`. Con `"complete": true` además la completa en la misma llamada.

| Campo | Tipo | Requerido | Descripción |
| - | - | - | - |
| `origin_branch_id` | string | Sí | Sucursal que envía. |
| `destination_branch_id` | string | Sí | Sucursal que recibe (distinta del origen y del mismo negocio). |
| `products` | array | Sí | 1 a 500 elementos. |
| `products[].product_id` | string | Sí | Producto de la sucursal **origen**. |
| `products[].quantity` | number | Sí | Mayor que 0 y no mayor que la existencia en origen. |
| `reference_number` | string | No | Referencia (máx. 100). Si se omite se genera `TRANS-<timestamp>`. |
| `notes` | string | No | Notas (máx. 1000). |
| `complete` | boolean | No | `true` para completarla de inmediato. |

```bash theme={null}
curl -X POST https://api.filexpress.app/api/external/inventory/transfers \
  -H "X-API-Key: fx_xxxxxxxx" \
  -H "Idempotency-Key: traslado-000457" \
  -H "Content-Type: application/json" \
  -d '{
    "origin_branch_id": "9d1c5a3e-...",
    "destination_branch_id": "9d1c5a40-...",
    "reference_number": "TR-000457",
    "products": [ { "product_id": "9d2a...", "quantity": 12 } ],
    "complete": true
  }'
```

**Respuesta `201`**

```json theme={null}
{
  "status": true,
  "data": {
    "id": "9d5d...",
    "reference_number": "TR-000457",
    "status": "completed",
    "origin_branch": { "id": "9d1c5a3e-...", "name": "Casa Matriz" },
    "destination_branch": { "id": "9d1c5a40-...", "name": "Sucursal Santa Ana" },
    "notes": null,
    "items": [
      { "product_id": "9d2a...", "sku": "CAF-500", "name": "Café molido 500 g", "quantity": 12, "unit_cost": 3.25 }
    ],
    "total_cost": 39.0,
    "created_at": "2026-10-06T11:00:00-06:00",
    "transferred_at": "2026-10-06T11:00:00-06:00"
  }
}
```

`items[].unit_cost` es el costo del producto al crear la transferencia y `total_cost` es la suma de cantidad × costo.

<Warning>
  Con `complete: true`, crear y completar es una sola operación: si la transferencia no puede completarse (por ejemplo, el stock cambió), la respuesta es un error y **no queda ninguna transferencia creada**. Puedes corregir y reintentar la misma llamada.
</Warning>

### Completar transferencia

```http theme={null}
POST /inventory/transfers/{id}/complete
```

Sin cuerpo. Mueve la existencia de forma atómica: descuenta en origen (solo si aún hay existencia suficiente) y suma en destino. Responde `200` con la transferencia en estado `completed`.

**Producto en la sucursal destino**: se busca por **SKU**; si no existe, por **código de barras**; si tampoco existe, se crea una copia del producto en el destino (mismo SKU, nombre, categoría, unidad, precio e impuestos) con existencia 0 antes de sumar la entrada.

### Cancelar transferencia

```http theme={null}
POST /inventory/transfers/{id}/cancel
```

Sin cuerpo. Solo para transferencias `pending`; no mueve existencias. Responde `200` con la transferencia en estado `cancelled`.

**Errores de transferencias**

| Código | Causa |
| - | - |
| `400` | `Las sucursales deben pertenecer a la misma empresa` (origen o destino de otro negocio o inexistente). |
| `400` | `La sucursal destino debe ser diferente a la de origen`. |
| `400` | `Solo se pueden transferir productos físicos: <producto>`. |
| `400` | `Cantidad insuficiente para <producto>. Disponible: <n>`. |
| `400` | `Esta transferencia ya fue completada` / `No se puede completar una transferencia cancelada` / `No se puede cancelar una transferencia completada` / `Esta transferencia ya está cancelada`. |
| `404` | `Transfer not found` (inexistente o de otro negocio) / `Producto no encontrado en la sucursal origen`. |
| `409` | `Cantidad insuficiente para <producto>. El stock cambió por otra operación concurrente, intenta de nuevo.` Ninguna parte de la transferencia se aplica. |
| `409` | Conflicto de `Idempotency-Key`. |
| `422` | Validación (`errors` indica el campo). |

### Consultar transferencias

```http theme={null}
GET /inventory/transfers?status=pending&branch_id={branch_id}
GET /inventory/transfers/{id}
```

| Parámetro | Tipo | Requerido | Descripción |
| - | - | - | - |
| `status` | string | No | `pending`, `completed` o `cancelled`. |
| `branch_id` | string | No | Transferencias donde la sucursal es origen **o** destino. |
| `start_date`, `end_date` | date | No | Rango de creación `YYYY-MM-DD`. |
| `per_page` | integer | No | 1 a 200 (por defecto 50). |

El listado es paginado (`data` + `pagination`) con el mismo formato de la [respuesta de creación](#crear-transferencia). El detalle devuelve un solo objeto o `404` si no existe o es de otro negocio.


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