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.

ClasecodeStatusNotas
ValidationErrorvalidation-error400Clase base. Trae field?, details?, fieldErrors?: FieldError[].
DuplicateSkuError extends ValidationErrorduplicate-sku409Mensaje típico: "Product with SKU '<sku>' already exists".
NotFoundError extends ValidationErrornot-found404Ver advertencia abajo.
InvalidStatusError extends ValidationErrorinvalid-status400Ver 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_LIMITEDLímite de tasa del proveedor de IA alcanzado.
BUDGET_EXCEEDEDPresupuesto diario de tokens de IA del tenant agotado — responde 429. Consulta el presupuesto restante con GET /products/bulk/ai/budget.
INVALID_PROMPTEl prompt enviado no es válido.
MODEL_ERRORFalla del lado del proveedor del modelo.
PARSE_ERRORLa respuesta del modelo no se pudo interpretar.
TIMEOUTLa operación de IA excedió el tiempo límite.
INVALID_RESPONSELa 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_ERROR

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

codeStatusEndpoint
bad-request/missing-param400GET /products/inventory-view
bad-request/invalid-param400GET /products/inventory-view
internal-error/missing-count500POST /products/query
bad-request/ambiguous-param400POST /products/query (envías filters y query a la vez, cuando son excluyentes)
bad-request/missing-param400GET /products/inventory — ⚠️ este endpoint responde {"error": "Missing required query parameter: locationId"} (string directo, sin objeto code), inconsistente con el resto de la tabla.

Medios

codeStatusEndpoint
internal-error/media-upload500POST /products/media/upload

CSV asíncrono (import/export)

codeStatusNotas
bad-request/missing-s3-key400Falta s3Key en el body.
forbidden/invalid-s3-key403La s3Key no empieza con el prefijo del tenant (uploads/{tenantId}/) — protección anti-IDOR.
not-found/s3-object404El objeto referenciado no existe en el bucket.
bad-request/file-too-large400El archivo excede el tamaño máximo permitido.
bad-request/invalid-config400La configuración de import/export enviada es inválida.
bad-request/invalid-import-file400El archivo no tiene una estructura válida para importarse.
rate-limited429Límite de tasa de subida/operaciones alcanzado.

Ver CSV masivo para el flujo completo de cada endpoint.

Operaciones masivas

codeStatusEndpoint
rollback-failed400POST /products/bulk/operations/{id}/rollback
insufficient-stock400POST /products/{sku}/bom/consume
consumption-failed (+ errors[])400POST /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:

codeStatusCuándo ocurre
EXPORT_IN_PROGRESS409Ya hay una exportación en curso para ese canal/selección.
CHANNEL_NOT_FOUND404El channelId indicado no existe en el tenant.
CHANNEL_INACTIVE400El canal existe pero está inactivo.
NO_PRODUCTS400La selección/filtro no resolvió ningún producto para exportar.
PRODUCT_EXPORT_FAILED400Falló 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-deep

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

codeStatusCuándo ocurre
MISSING_PARAMETER400Falta 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.

Siguientes pasos