Catálogo de Errores
No hay una convención única de códigos de error en este dominio
A diferencia de otras APIs de Fenicia con un namespace consistente (dominio:snake_case), el dominio de Productos mezcla al menos cuatro convenciones distintas de code, según qué capa del código generó el error: los errores tipados de la librería (kebab-case plano, sin namespace), códigos inline por endpoint (namespace/kebab-case), códigos en SCREAMING_SNAKE_CASE en el flujo de publicación a canal, y un tercer estilo UPPERCASE en la lambda de categorías. Esta página los agrupa por origen real, no finge que exista un catálogo unificado.
Trata el status HTTP como la señal primaria y confiable en este dominio; el code exacto puede variar de forma en distintos rincones de la superficie. Cuando dependas de un code específico, verifícalo contra el endpoint puntual — esta página documenta los que están confirmados por el código fuente, agrupados por de dónde vienen.
1. Errores tipados de la librería (@fenicia/products-service)
Estos son los errores que produce la capa de validación/repositorio de la librería subyacente, consumidos por lambda-products en las rutas de creación, actualización, borrado, variantes, medios, materiales y BOM.
| Clase | code | Status | Notas |
|---|---|---|---|
ValidationError | validation-error | 400 | Clase base. Trae field?, details?, fieldErrors?: FieldError[]. |
DuplicateSkuError extends ValidationError | duplicate-sku | 409 | Mensaje típico: "Product with SKU '<sku>' already exists". |
NotFoundError extends ValidationError | not-found | 404 | Ver advertencia abajo. |
InvalidStatusError extends ValidationError | invalid-status | 400 | Ver advertencia abajo. |
NotFoundError e InvalidStatusError: declaradas, sin uso interno confirmado
Ambas clases están declaradas y exportadas por la librería, pero no se encontró ningún throw interno de ellas dentro del código fuente de la librería auditado. Si aparecen en una respuesta real, el origen sería el lambda consumidor (no la librería) — no las trates como una respuesta garantizada de ningún endpoint específico hasta confirmarlo contra el comportamiento real.
DUPLICATE_SKU puede ser engañoso
El traductor de errores de MongoDB (translateMongoDuplicateKeyError) etiqueta como DUPLICATE_SKU cualquier colisión de índice único, no solo la de sku. Si tu documento tiene otro índice único (por ejemplo a nivel de variante) y choca, el código que recibes puede seguir diciendo duplicate-sku aunque el campo real en conflicto sea otro. Revisa siempre field/details del error, no solo el code.
ValidationError.fieldErrors trae un arreglo de errores por campo cuando la validación falla en más de un lugar a la vez (por ejemplo, un PUT con varios campos inválidos). La forma exacta de cada entrada de FieldError no fue verificada campo a campo en esta auditoría — asume al menos un campo field, pero confírmalo contra la respuesta real antes de parsearlo de forma estricta.
2. Errores de IA (AIServiceError)
Usados por Operaciones masivas en POST /products/bulk/ai/transform.
code (AI_ERROR_CODES) | Notas |
|---|---|
RATE_LIMITED | Límite de tasa del proveedor de IA alcanzado. |
BUDGET_EXCEEDED | Presupuesto diario de tokens de IA del tenant agotado — responde 429. Consulta el presupuesto restante con GET /products/bulk/ai/budget. |
INVALID_PROMPT | El prompt enviado no es válido. |
MODEL_ERROR | Falla del lado del proveedor del modelo. |
PARSE_ERROR | La respuesta del modelo no se pudo interpretar. |
TIMEOUT | La operación de IA excedió el tiempo límite. |
INVALID_RESPONSE | La respuesta del modelo no tiene la forma esperada. |
Cada AIServiceError trae un flag retryable: boolean — respétalo antes de reintentar automáticamente.
3. Catálogo de códigos declarados (no todos con clase propia)
La librería también declara estas constantes, aunque no todas tienen una clase de error dedicada como las de la sección 1:
ERROR_CODES: VALIDATION_ERROR, NOT_FOUND, UNAUTHORIZED, FORBIDDEN, INTERNAL_ERROR,
DUPLICATE_SKU, INVALID_STATUS, INVALID_BINDING, QUOTA_EXCEEDED, SYNC_FAILED
HTTP_STATUS: OK, CREATED, BAD_REQUEST, UNAUTHORIZED, FORBIDDEN, NOT_FOUND,
CONFLICT, UNPROCESSABLE_ENTITY, INTERNAL_ERROR4. Códigos inline por endpoint (namespace bad-request/…, forbidden/…, etc.)
Varias rutas de lambda-products arman su propio código como <categoría>/<detalle> directamente en el handler, sin pasar por la librería. Confirmados por área:
Consultas e inventario
code | Status | Endpoint |
|---|---|---|
bad-request/missing-param | 400 | GET /products/inventory-view |
bad-request/invalid-param | 400 | GET /products/inventory-view |
internal-error/missing-count | 500 | POST /products/query |
bad-request/ambiguous-param | 400 | POST /products/query (envías filters y query a la vez, cuando son excluyentes) |
bad-request/missing-param | 400 | GET /products/inventory — ⚠️ este endpoint responde {"error": "Missing required query parameter: locationId"} (string directo, sin objeto code), inconsistente con el resto de la tabla. |
Medios
code | Status | Endpoint |
|---|---|---|
internal-error/media-upload | 500 | POST /products/media/upload |
CSV asíncrono (import/export)
code | Status | Notas |
|---|---|---|
bad-request/missing-s3-key | 400 | Falta s3Key en el body. |
forbidden/invalid-s3-key | 403 | La s3Key no empieza con el prefijo del tenant (uploads/{tenantId}/) — protección anti-IDOR. |
not-found/s3-object | 404 | El objeto referenciado no existe en el bucket. |
bad-request/file-too-large | 400 | El archivo excede el tamaño máximo permitido. |
bad-request/invalid-config | 400 | La configuración de import/export enviada es inválida. |
bad-request/invalid-import-file | 400 | El archivo no tiene una estructura válida para importarse. |
rate-limited | 429 | Límite de tasa de subida/operaciones alcanzado. |
Ver CSV masivo para el flujo completo de cada endpoint.
Operaciones masivas
code | Status | Endpoint |
|---|---|---|
rollback-failed | 400 | POST /products/bulk/operations/{id}/rollback |
insufficient-stock | 400 | POST /products/{sku}/bom/consume |
consumption-failed (+ errors[]) | 400 | POST /products/materials/consumption/consume |
5. Publicación a canal — convención SCREAMING_SNAKE_CASE
POST /products/export y rutas relacionadas de Publicación a canal usan una tercera convención, literales sin namespace de dos puntos ni de slash:
code | Status | Cuándo ocurre |
|---|---|---|
EXPORT_IN_PROGRESS | 409 | Ya hay una exportación en curso para ese canal/selección. |
CHANNEL_NOT_FOUND | 404 | El channelId indicado no existe en el tenant. |
CHANNEL_INACTIVE | 400 | El canal existe pero está inactivo. |
NO_PRODUCTS | 400 | La selección/filtro no resolvió ningún producto para exportar. |
PRODUCT_EXPORT_FAILED | 400 | Falló la exportación de al menos un producto (revisa el detalle por ítem en la respuesta). |
6. Categorización de marketplace — kebab-case sin namespace
POST /products/marketplace-categories valida exhaustivamente y devuelve uno de estos códigos (siempre 400):
invalid-skus · too-many-skus · invalid-sku-values · invalid-channel-id
invalid-category · invalid-category-path · category-id-too-long
category-name-too-long · category-path-too-deep7. Lambda de categorías (/categories/...) — convención propia, distinta a todo lo anterior
Esta es una lambda separada (lambda-categories) y usa su propia convención, ya vista en la práctica como un único literal en mayúsculas sin namespace:
code | Status | Cuándo ocurre |
|---|---|---|
MISSING_PARAMETER | 400 | Falta un query param requerido (q en búsquedas, title en clasificación, etc.). |
No se confirmó un catálogo más amplio de códigos en esta lambda
El resto de rutas de /categories/... que devuelven 404/500 lo hacen mayormente con mensajes genéricos del handler, sin un code estructurado confirmado más allá de MISSING_PARAMETER. Detalle de endpoints en Categorías.
8. Errores de autenticación y autorización (genéricos, sin code de dominio confirmado)
Todas las rutas de este dominio devuelven 401 cuando falta un tenant resuelto y 403 cuando la API key no tiene el permiso RBAC requerido. A diferencia del catálogo anterior, esta auditoría no capturó un string de code específico y consistente para estos dos casos en Productos (el resto del dominio no comparte necesariamente el namespace auth:* que usan otros dominios de esta API) — trátalos por status HTTP, no por code, hasta que se confirme uno.
Cómo manejar esto en tu cliente
Dado que no hay un namespace único, la estrategia más segura es: ramifica primero por status HTTP, y usa el code solo dentro del endpoint puntual que estás llamando (donde ya sabes qué convención aplica).
const response = await fetch("https://api.fenicia.io/products/export", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.FENICIA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ channelId: "canal-shopify-mx" }),
});
const body = await response.json();
if (!response.ok) {
// En este endpoint puntual, el code es SCREAMING_SNAKE_CASE (sección 5).
if (body.code === "EXPORT_IN_PROGRESS") {
// ya hay una exportación corriendo para este canal
} else {
console.error(response.status, body.code ?? body.error);
}
}Tip
Si tu integración solo necesita reintentar con seguridad, el status HTTP es suficiente en el 100% de los casos de este dominio: 400/403/404/409 no se reintentan sin cambiar la petición; 429 se reintenta con backoff; 5xx se reintenta con backoff exponencial.