Catálogo de Errores — Inventario y Ubicaciones

/inventory/* y /locations/* son dos servicios distintos detrás de un mismo API Gateway. No comparten un envelope de respuesta ni una convención de código de error — cada uno devuelve, literalmente, lo que su capa de servicio produjo. Esta página documenta ambos tal como se comportan hoy en producción, incluyendo sus inconsistencias.

No hay envelope {data, meta}

Cada respuesta es el cuerpo devuelto por la capa de servicio, serializado tal cual. Las formas varían por recurso: documento plano, { items, pagination }, { transfers, pagination }, { data, total, page, limit, totalPages, hasMore } (lotes), { locations, totalCount, page, limit } (ubicaciones — nota totalCount, no total). No asumas un esquema de respuesta compartido entre endpoints de este dominio.

Los dos códigos genéricos de error 500

Base pathCódigo en el 500 genérico
/inventory/*internal-error
/locations/*internal-server-error

Son literalmente distintos. No es un typo de esta guía — son dos lambdas separadas con implementaciones independientes del catch-all genérico.

Tenant faltante (401)

Ambos servicios devuelven 401 con el mismo código, a pesar de que el código sugiere un 400:

{ "code": "bad-request/missing-tenant", "message": "..." }

Permisos y tenant suspendido (403)

Un 403 puede deberse a que tu API key no tiene el permiso requerido por el endpoint, o a que el tenant está suspendido (por ejemplo, por facturación). La fuente de esta auditoría no confirmó un código de error literal único para estos casos — verifica el message de la respuesta para distinguir la causa exacta.


Errores del dominio Inventario (/inventory/*)

Todos requieren el permiso indicado en cada endpoint (inventory:read, inventory:update, inventory:adjust, inventory:transfer, inventory:count o inventory:manage según el recurso — ver API de Inventario).

Stock (/inventory, /inventory/items, /inventory/location, /inventory/bulk-update, /inventory/stats)

CódigoHTTPCausaCómo resolver
bad-request/missing-sku400Falta el SKU en la ruta o en el body.Incluye sku.
bad-request/missing-body400El body de la petición está vacío.Envía un body JSON válido.
bad-request/invalid-json400El body no es JSON válido.Verifica el Content-Type y el JSON enviado.
bad-request/invalid-stock400stock no es de tipo number.Envía stock como número, no como string.
bad-request/invalid-items400POST /inventory/bulk-updateitems inválido o vacío.Envía un arreglo items no vacío con {sku, stock}.
not-found404GET /inventory/items/{sku} o GET /inventory/location/{id}/product/{sku} — no existe inventario para ese SKU/ubicación.Verifica el SKU y el locationId.
not-found/location404PUT /inventory/location/{id}/product/{sku} — la ubicación no existe.Verifica el locationId.
forbidden/inventory-handler403PUT /inventory/location/{id}/product/{sku} — el SKU/ubicación está bajo manejo de integración/canal y no permite edición manual.Usa el flujo de sincronización del handler correspondiente, no la edición manual.

Stock negativo devuelve 500, no 400

Enviar un stock negativo en PUT /inventory/location/{id}/product/{sku} o dentro de POST /inventory/bulk-update produce hoy un 500 sin estructura de error de negocio:

{ "code": "internal-error", "message": "Stock cannot be negative" }

No es un 400 de validación — es una excepción genérica de JavaScript (Error('Stock cannot be negative')) que cae al catch-all del lambda. Valida tú mismo que stock sea ≥ 0 antes de enviarlo; no confíes en que la API te devuelva un 400 estructurado para este caso hoy.

Reportes (/inventory/reports)

CódigoHTTPCausa
bad-request/missing-type400Falta reportType.
bad-request/invalid-report400reportType no es uno de los cinco valores válidos.

Transferencias (/inventory/transfers)

Requieren inventory:transfer.

CódigoHTTPCausa
bad-request/missing-origin400Falta originId al crear.
bad-request/missing-destination400Falta destinationId al crear.
bad-request/missing-products400Falta o está vacío productsList al crear.
not-found404La transferencia no existe, en cualquier acción (approve, receive, cancel).

Sin máquina de estados — y sin idempotencia

approve, receive y cancel no validan el estado actual de la transferencia antes de aplicar el cambio: una transferencia puede recibirse dos veces (aplicando el movimiento de stock dos veces) o cancelarse después de recibida. Además, receive no es idempotente — reenviar la misma petición vuelve a aplicar el movimiento de stock. No reintentes un POST .../receive a ciegas ante un timeout; verifica primero el estado de la transferencia.

Ajustes (/inventory/adjustments)

Requieren inventory:adjust.

CódigoHTTPCausa
bad-request/missing-location400Falta locationId al crear.
bad-request/missing-sku400Falta sku al crear.
bad-request/missing-quantity400Falta newQuantity al crear.
bad-request/missing-reason400Falta reason al crear, o falta el motivo al rechazar.
bad-request/invalid-reason400reason no está entre los 11 valores aceptados por este endpoint (ver abajo).
not-found404El ajuste no existe.

'sale' no es un motivo válido aquí, aunque el modelo lo soporte

POST /inventory/adjustments acepta exactamente estos 11 valores de reason: count_correction, damage, theft, expired, found, return, supplier_error, restock, shrinkage, sample, other. El schema canónico de InventoryAdjustment define un doceavo valor, sale ("venta — decremento automático por orden"), pero este endpoint público lo rechaza con bad-request/invalid-reason. sale está reservado para el decremento automático interno que dispara una orden — no lo uses al crear un ajuste manual.

Conteos (/inventory/counts)

Requieren inventory:count.

CódigoHTTPCausa
bad-request/missing-name400Falta name al crear.
bad-request/missing-location400Falta locationId al crear.
bad-request/missing-type400Falta countType al crear.
bad-request/invalid-items400PUT /inventory/counts/{id}/itemscountItems inválido.
not-found404El conteo no existe.

Lotes (/inventory/lots)

Requieren inventory:read. Superficie de solo lectura.

CódigoHTTPCausa
not-found404El lote no existe.
400Falta locationId en GET /inventory/lots/sku/{sku}/valuation (obligatorio; el código literal no fue confirmado en esta auditoría).

Handlers de inventario externo (/inventory/handlers)

Requieren inventory:manage.

Brecha de aislamiento multi-tenant en producción

GET /inventory/handlers/{id} y POST /inventory/handlers/{id}/sync no filtran por tenantId en la versión actualmente desplegada en producción. Si el handlerId de otro tenant es adivinado o se filtra, es posible leer o escribir su registro de handler/sincronización. La corrección (ambos métodos ganan un parámetro tenantId obligatorio) ya existe integrada en la rama de desarrollo, pero todavía no está desplegada a producción. Si expones handlerId en un cliente, trátalo como un secreto hasta que la corrección esté en vivo.


Errores del dominio Ubicaciones (/locations/*)

Requieren el permiso indicado en cada endpoint (locations:read, locations:create, locations:update o locations:delete — ver Ubicaciones y Zonas).

CódigoHTTPCausa
not-found404GET /locations/{id} — no existe la ubicación. Único endpoint del dominio con este código literal en un 404.
bad-request/not-found404El resto de los lookups por ID (places, set-default, delete) cuando el recurso no existe.
bad-request/not-found400GET /locations/count cuando el servicio no resuelve el conteo.
bad-request/missing-name400Falta name al crear una ubicación.
bad-request/missing-type400Falta type al crear una ubicación.
bad-request/invalid-type400type no está en el enum válido (ubicación o zona).
bad-request/can't-create400El servicio no pudo crear la ubicación.
bad-request/can't-update400El servicio no pudo actualizar la ubicación.
bad-request/can't-delete400El servicio no pudo eliminar la ubicación.
bad-request/system-location400Se intentó eliminar una ubicación de sistema, o quitarle el flag isDefault.
bad-request/missing-id400PUT /locations/{id} invocado sin el parámetro de ruta id.
bad-request/missing-code, bad-request/missing-name, bad-request/missing-type400Faltan campos obligatorios al crear una zona (code, name, type).
bad-request/duplicate-code400Ya existe una zona con ese code en la ubicación (verificado en memoria, no por índice único de base de datos).
internal-server-error500Error genérico del lambda de ubicaciones.

'not-found' inconsistente dentro del propio servicio

bad-request/not-found combina un código con prefijo bad-request/ con un status 404 — es contradictorio a nivel de nombre, pero así se comporta hoy. Rama tu lógica por el par (code, status) completo, no asumas que bad-request/* siempre implica 400.


Cuándo reintentar y cuándo no

Status¿Reintentar?Notas
400 validaciónNoCorrige la petición según el code.
401 tenant/auth faltanteNoRevisa el header Authorization y tu API key.
403 permiso/tenant suspendidoNoSolicita el scope correcto o resuelve el estado de facturación del tenant.
404 no encontradoNoEl recurso no existe en tu tenant.
500 interno (internal-error / internal-server-error)Sí, con backoffFrecuentemente transitorio — pero recuerda que un stock negativo también produce un 500 (ver arriba), así que un 500 no siempre es transitorio en este dominio.

No reintentes ciegamente escrituras sin máquina de estados

POST /inventory/transfers/{id}/receive y las transiciones de conteos/transferencias/ajustes no tienen un guardia de estado ni son idempotentes. Antes de reintentar una escritura tras un timeout o un 500, consulta primero el estado actual del recurso (GET) para confirmar si la operación ya se aplicó — reintentar a ciegas puede duplicar el movimiento de stock.

Loguea el code, no el message

Los mensajes pueden cambiar; los códigos son la superficie estable para ramificar tu lógica de integración.

// Correcto
if (error.code === "forbidden/inventory-handler") {
  await notifyMerchant("Este SKU se gestiona por integración, no manualmente");
}
 
// Frágil
if (error.message.includes("handler")) { ... }

Siguientes pasos