Consulta y actualización de stock

Este artículo cubre los endpoints núcleo de lectura y escritura de existencias: listado general, consulta por SKU o por ubicación, actualización individual, actualización masiva, estadísticas y reportes.

Listar inventario

Lista los registros de inventario del tenant, con filtros opcionales

pagenumber

Página, 1-indexada. Default: 1.

limitnumber

Elementos por página. Default: 20.

skustring

Filtro por SKU (regex, sin distinguir mayúsculas/minúsculas).

productSkustring

Filtro por SKU de producto padre (regex).

locationIdstring

Filtro por ubicación.

statusstring

Filtro por estado del registro.

lowStockboolean

Si es true, filtra solo registros con stock bajo (umbral fijo: stock < 10).

200
{
  "items": [
    {
      "tenantId": "69db07c8bce4d49b18c42a49",
      "sku": "CAM-ROJO-M",
      "locationId": "loc_abc123",
      "stock": 42,
      "compromised": 0,
      "handlerType": "manual"
    }
  ],
  "pagination": { "page": 1, "limit": 20, "total": 318, "pages": 16 }
}

Permiso requerido: inventory:read

lowStock usa un umbral fijo

lowStock=true filtra registros con stock < 10. Este umbral está hardcodeado en el servicio — no es configurable por tenant ni por SKU desde este endpoint.

Consultar por SKU

Consulta el registro de inventario de un SKU

skustringrequired

SKU a consultar (parte de la ruta).

200
{
  "tenantId": "69db07c8bce4d49b18c42a49",
  "sku": "CAM-ROJO-M",
  "locationId": "loc_abc123",
  "stock": 42
}
400
{ "code": "bad-request/missing-sku" }
404
{ "code": "not-found" }

Permiso requerido: inventory:read

Solo devuelve UNA ubicación, no todas

Este endpoint hace un findOne({tenantId, sku}) — devuelve el primer registro que Mongo encuentre para ese SKU, sin orden determinístico. Como un SKU puede tener un registro de inventario por cada ubicación (índice único {tenantId, sku, locationId}), este endpoint no consolida ni suma el stock de todas las ubicaciones de un SKU multi-ubicación. Si necesitas el stock de un SKU en una ubicación específica, usa GET /inventory/location/:id/product/:sku (más abajo en esta misma página).

Consultar por ubicación

Lista el inventario de una ubicación específica

locationIdstringrequired

ID de la ubicación (parte de la ruta).

pagenumber

Página, 1-indexada. Default: 1.

limitnumber

Elementos por página. Default: 20.

200
{
  "items": [
    { "sku": "CAM-ROJO-M", "locationId": "loc_abc123", "quantity": 42 }
  ],
  "pagination": { "page": 1, "limit": 20, "total": 54, "pages": 3 }
}

Permiso requerido: inventory:read

Consultar por ubicación y SKU

Consulta el registro exacto de un SKU en una ubicación

locationIdstringrequired

ID de la ubicación.

skustringrequired

SKU a consultar.

200
{ "tenantId": "69db07c8bce4d49b18c42a49", "sku": "CAM-ROJO-M", "locationId": "loc_abc123", "stock": 42 }
404
{ "code": "not-found" }

Permiso requerido: inventory:read

Este es el endpoint correcto para leer el stock exacto de un SKU en una ubicación puntual, sin la ambigüedad de GET /inventory/items/:sku.

Actualizar stock (individual)

Establece el stock de un SKU en una ubicación (reemplaza el valor, no aplica un delta)

locationIdstringrequired

ID de la ubicación.

skustringrequired

SKU a actualizar.

stocknumberrequired

Nuevo valor absoluto de stock (no es un incremento/decremento).

{ "stock": 50 }
200
{ "tenantId": "69db07c8bce4d49b18c42a49", "sku": "CAM-ROJO-M", "locationId": "loc_abc123", "stock": 50 }
400
{ "code": "bad-request/invalid-stock" }
404
{ "code": "not-found" }

Permiso requerido: inventory:update

Stock negativo devuelve 500, no 400

Si envías un stock que resultaría negativo, la API responde 500 con {"code":"internal-error","message":"Stock cannot be negative"} — no un 400 estructurado. Este es el comportamiento actual en producción: la validación de negativos ocurre en una capa que no distingue error de negocio de error interno. Trátalo como una respuesta de error de negocio (rechaza el intento), pero no esperes el status HTTP habitual para validaciones.

Un registro cuyo stock es controlado por una integración o canal (handlerType: 'integration' | 'channel') puede rechazar la escritura manual con 403 forbidden/inventory-handler.

Actualizar stock (masivo)

Actualiza el stock de varios SKUs en una sola petición

itemsarrayrequired

Lista de { sku, stock, locationId? } a actualizar.

{
  "items": [
    { "sku": "CAM-ROJO-M", "stock": 50, "locationId": "loc_abc123" },
    { "sku": "PAN-AZUL-32", "stock": -5 }
  ]
}
200
[
  { "success": true, "sku": "CAM-ROJO-M", "result": { "stock": 50 } },
  { "success": false, "sku": "PAN-AZUL-32", "error": "Stock cannot be negative" }
]
400
{ "code": "bad-request/invalid-items" }

Permiso requerido: inventory:update

200 no significa que todos los items se actualizaron

Esta respuesta es siempre 200 si el body es válido, incluso si algunos (o todos) los items individuales fallan. No hay una bandera de éxito a nivel raíz. Debes iterar el arreglo de resultados y revisar success en cada elemento — un 200 global nunca garantiza que tu lote completo se aplicó.

Estadísticas

Totales agregados de productos por estado de stock

locationIdstring

Limita las estadísticas a una ubicación.

200
{
  "totalProducts": 318,
  "productsInStock": 290,
  "productsOutOfStock": 12,
  "productsLowStock": 16
}

Permiso requerido: inventory:read

productsLowStock usa un umbral fijo de 5 unidades (DEFAULT_MIN_ALERT_STOCK), distinto del umbral < 10 que usa el filtro lowStock de GET /inventory. No confundas ambos — son dos umbrales hardcodeados independientes.

Reportes

Genera un reporte según el tipo solicitado

reportTypestringrequired

inventory | transfers | adjustments | counts | valuation

startDatestring

Fecha ISO 8601 de inicio del rango (no aplica a reportType=inventory).

endDatestring

Fecha ISO 8601 de fin del rango.

locationIdstring

Filtra el reporte a una ubicación.

formatstring

Aceptado (json | csv) pero sin efecto — ver advertencia abajo.

200
{
  "data": [
    { "sku": "CAM-ROJO-M", "quantity": 42 }
  ],
  "summary": { "totalItems": 318, "locationId": "loc_abc123" }
}
400
{ "code": "bad-request/missing-type" }

Permiso requerido: inventory:read

reportTypedatasummary
inventoryInventory[] (hasta 10,000, filtrado por locationId){ totalItems, locationId }
transfersInventoryTransfer[] filtrado por fecha{ total, byStatus }
adjustmentsInventoryAdjustment[] filtrado por fecha/ubicación{ total, totalQuantityAdjusted, byReason }
countsInventoryCount[] filtrado por fecha/ubicación{ total, byStatus }
valuationArray<{sku, productName, stock, unitCost, totalValue, locationId}>{ totalItems, totalUnits, totalValue, averageUnitCost }

El reporte de valuación (reportType=valuation) está roto

El reporte de valuación lee unitCost directamente del documento de Inventory, pero ese campo no existe en el esquema de inventario — siempre es undefined. Como resultado, totalValue y averageUnitCost de este reporte son efectivamente 0 para cualquier tenant. No lo uses como fuente de valuación de inventario.

format=csv no hace nada

El parámetro format se acepta pero se ignora: la respuesta siempre es JSON, sin importar el valor enviado.

POST /inventory no está implementado

Ruta raíz de creación — no implementada

No se valida ni se lee ningún cuerpo — la ruta responde 501 sin importar el body enviado.

{}
501
{ "code": "not-implemented" }

GET /inventory (ruta raíz) es equivalente al listado documentado arriba. POST /inventory sobre esa misma ruta responde 501 not-implemented — no existe una operación de "crear inventario" genérica; los registros de inventario se crean implícitamente al escribir stock (PUT/bulk-update) o mediante los flujos internos de producto.

Ejemplo — actualizar stock

curl -X PUT 'https://api.fenicia.io/inventory/location/loc_abc123/product/CAM-ROJO-M' \
  -H 'Authorization: Bearer fkapi_tu_api_key' \
  -H 'Content-Type: application/json' \
  -d '{"stock": 50}'

Errores

CódigoStatusDescripción
bad-request/missing-tenant401No se pudo resolver el tenant de la API key.
bad-request/missing-sku400Falta el SKU en la ruta o el body.
bad-request/missing-body400El body de la petición está vacío.
bad-request/invalid-json400El body no es JSON válido.
bad-request/invalid-stock400stock falta o no es de tipo number.
bad-request/invalid-items400items falta, está vacío o mal formado (bulk-update).
not-found404No existe un registro de inventario para ese SKU/ubicación.
forbidden/inventory-handler403El registro está controlado por una integración/canal; la escritura manual fue rechazada.
internal-error500Error interno — incluye el caso de stock negativo (ver advertencia arriba).
not-implemented501POST /inventory (ruta raíz) — no existe esta operación.

Siguientes pasos