API de Productos

La API de Productos cubre el catálogo completo de un tenant: búsqueda y listado, detalle de producto, alta/edición/borrado, variantes, medios, compatibilidad automotriz (fitment), materiales/insumos/BOM/modificadores (manufactura y POS), operaciones masivas (rollback, edición visual con flujos, mutaciones con IA), importación/exportación CSV, categorización y publicación hacia marketplaces, colecciones y categorías.

Léelo antes de cualquier otro artículo de esta sección. A diferencia de otros dominios de esta API, aquí no existe un contrato de respuesta único — es el hallazgo más importante de todo el dominio y condiciona cómo debes leer cada artículo siguiente.

Arquitectura del dominio

"Productos" en esta API en realidad son tres lambdas independientes detrás de tres bases de ruta distintas, cada una con su propio esquema de autorización:

Base de rutaCubre
/products/...Catálogo, detalle, CRUD, variantes, medios, fitment, materiales/BOM, bulk, CSV, publicación a canal
/collections/...Colecciones de productos (agrupaciones manuales/dinámicas)
/categories/...Árbol de categorías propio + clasificación asistida por IA para SAT, T1, Walmart, TikTok y mapeos hacia marketplaces

Todas requieren autenticación; el ruteo interno de cada lambda lo hace el propio código por path/método, no API Gateway.

URL base y autenticación

https://api.fenicia.io

No hay prefijo de versión (/v1) en la URL. Cada petición requiere un API key enviado como Bearer token:

Authorization: Bearer fkapi_tu_api_key

Tip

Guarda tu API key en una variable de entorno (FENICIA_API_KEY) y nunca la incluyas directamente en el código fuente.

⚠️ No hay un envelope de respuesta único

En el dominio de Órdenes toda respuesta sigue {data, meta} / {error}. En Productos eso no es cierto. Solo un endpoint de todo el dominio usa ese envelope; el resto responde shapes ad-hoc por endpoint — algunos con wrapper, otros sin ninguno, algunos como array plano.

Verifica el shape en cada artículo, no lo asumas

No existe una regla general que puedas aplicar a un endpoint nuevo de este dominio. El shape de la respuesta se documenta caso por caso en cada artículo de esta sección.

Ejemplos reales, para que calibres la variedad:

Endpoint (ejemplo)Shape de la respuesta 200/201
GET /products?availableAt=<locationId>{ "data": Product[], "meta": { "pagination": {...} } } — el único caso con este envelope
GET /products (sin availableAt)Product[] — array plano, sin wrapper (forma legada)
GET /products/count{ "count": number } o { "count": number, "searchStrategy": string } si hay term
GET /products/exist/{sku}{ "exist": boolean }
POST /products/query{ "products": [...], "total": number, "page": number, "limit": number }
GET /products/{id}Product — objeto directo, sin wrapper
POST /products / PUT /products/{id}Product — objeto directo, sin wrapper (201 / 200)
DELETE /products/{id}{ "success": true, "message": "<string>" }
Operaciones asíncronas (CSV, export a canal, bulk programado)202 + { "jobId": "..." } (o { "scheduleId": "..." })
Operaciones bulk con éxito parcial207 + resultado propio del endpoint, típicamente con un array results[]/errors[] por ítem
Único caso con {data, meta}: GET /products?availableAt=
{
  "data": [
    { "sku": "CAM-ROJO-M", "title": { "value": "Camisa Roja Talla M" }, "price": 499.0, "status": "active" }
  ],
  "meta": {
    "pagination": { "page": 0, "limit": 20, "total": 143, "totalPages": 8, "hasMore": true }
  }
}
El mismo listado SIN availableAt: array plano
[
  { "sku": "CAM-ROJO-M", "title": { "value": "Camisa Roja Talla M" }, "price": 499.0, "status": "active" }
]

Consulta Catálogo y búsqueda para el detalle completo de GET /products y sus dos formas.

Paginación

page y limit son los parámetros de paginación en toda la superficie de listado. La base es consistente (page arranca en 0), pero el default y el techo de limit varían por endpoint — no hay un único valor global:

  • page: entero, base 0 en todos los endpoints de listado.
  • limit: el default más común en lambda-products es 20; el validador de la librería subyacente (validatePagination) usa 50 como default genérico y aplica un techo de 500 en la inmensa mayoría de rutas.
  • Excepción de techo: la exportación CSV asíncrona usa EXPORT_MAX_LIMIT = 10000, muy por encima del resto del dominio.

Confirma el default y el máximo exactos en el artículo de cada endpoint antes de asumirlos — Catálogo y búsqueda documenta los de GET /products, y CSV masivo los de import/export.

Proyección con extend

GET /products/{id} (y, a nivel de librería, el resto de endpoints de listado) soporta ?extend= para pedir campos que no vienen por defecto en la proyección base (la respuesta por defecto pesa ~3 KB por producto; sin extend, varios campos pesados se omiten a propósito).

Valor de extendQué agrega
variantsArray completo de variantes
optionsOpciones de variación (talla, color, etc.)
descriptionDescripción larga / HTML
bindingsVínculos con canales de venta
metadataMetafields
inventoryInventario por variante y ubicación (join en vivo contra la colección inventory)
dimensionsDimensiones de envío
seometaTitle, metaDescription, slug
allTodo lo anterior

Puedes combinar varios valores separados por coma: ?extend=variants,inventory,seo.

`embeddings` existe a nivel de librería, no confirmado en este endpoint

La librería subyacente define embeddings como un campo extendible adicional (junto con los de la tabla). No se confirmó en esta auditoría que GET /products/{id} lo acepte en su parseo de query — no lo des por hecho sin probarlo primero.

Detalle completo de la respuesta con cada valor en Consultar un producto.

Permisos

El catálogo de permisos vive en @fenicia/core. Cada base de ruta usa su propio namespace, con una excepción notable:

PRODUCTS.READ    products:read      PRODUCTS.CREATE   products:create
PRODUCTS.UPDATE  products:update    PRODUCTS.DELETE   products:delete
PRODUCTS.IMPORT  products:import    PRODUCTS.EXPORT   products:export
PRODUCTS.SYNC    products:sync      PRODUCTS.MANAGE   products:manage
PRODUCTS.ALL     products:*

COLLECTIONS.* sigue el mismo patrón (READ/CREATE/UPDATE/DELETE) para /collections/....

/categories/... NO tiene su propio namespace de permiso

Pese a ser una lambda y una base de ruta separadas, /categories/... reutiliza PERMISSIONS.PRODUCTS.* (mayormente READ y MANAGE) más SETTINGS.READ/SETTINGS.MANAGE para sus endpoints de estadísticas y administración de cache. No existe un PERMISSIONS.CATEGORIES.*.

RBAC apilado — algunas rutas exigen dos permisos, no uno

La mayoría de los endpoints valida un solo permiso. Un subconjunto exige dos, apilados, y hay una asimetría documentada entre dev y producción:

RutaPermiso(s) exigidos
/products/export*PRODUCTS.EXPORT + PRODUCTS.READ
GET /products/bulk/operations*PRODUCTS.READ + PRODUCTS.UPDATE
GET /products/bulk/schedule* (solo dev, aún no en master)únicamente PRODUCTS.UPDATEno exige READ, a diferencia del resto de rutas GET del dominio

No asumas "un permiso por ruta" al integrar: confirma en el artículo específico (Publicación a canal, Operaciones masivas) si la ruta que vas a usar exige más de uno.

Cuatro rutas de /categories/... sin requirePermission() explícito

walmart/unified-classify, cache/lookup, cache/stats y cache/invalidate solo validan que exista un tenantId en la sesión — no verifican un permiso RBAC explícito, a diferencia de casi todo el resto del archivo. Podría ser intencional (uso interno) o un descuido; trátalo como una particularidad conocida, no como el patrón general del dominio.

Operaciones irreversibles

El borrado de un producto es un hard delete

DELETE /products/{id} elimina el documento de la base de datos de forma permanente — no hay papelera ni soft-delete a nivel de este endpoint. status: 'disabled' es un estado lógico independiente y no equivale a borrarlo. Antes de invocar este endpoint desde tu integración, confirma que realmente quieres una eliminación irrecuperable. Detalle en Gestionar productos.

Límites conocidos de esta versión de la API

  • Sin endpoints granulares para atributos/custom fields, SEO ni price-schemas: todo eso se lee con ?extend= pero solo se escribe reenviando el payload completo del producto vía PUT /products/{id}. Ver Gestionar productos.
  • Sin endpoints para borrar o reordenar medios: el array media[] completo se reescribe vía PUT /products/{id}. Ver Variantes y medios.
  • No existe una entidad HTTP "bundle": el único mecanismo de composición producto-de-productos con endpoints propios es BOM (orientado a manufactura/consumo). bundleConfig es un sub-shape de datos que se gestiona dentro del payload del producto, sin rutas /products/{sku}/bundle. Ver Modelo de datos y Materiales y BOM.

Ejemplo rápido

curl "https://api.fenicia.io/products?limit=5" \
  -H "Authorization: Bearer $FENICIA_API_KEY"

Mapa de la sección

ArtículoContenido
Catálogo y búsquedaGET /products, búsqueda, conteo, filtros disponibles.
Consultar un productoDetalle por ID/SKU, ?extend=, relacionados, variantes de un producto.
Modelo de datosForma real (Mongoose) de Product y ProductVariant, divergencias con el tipo TS público.
Gestionar productosCrear, actualizar, borrar, rename de SKU.
Variantes y mediosActualizar campos de una variante existente; subir imágenes/medios.
Compatibilidad (fitment)Catálogos y nodos de compatibilidad para el vertical automotriz.
Materiales y BOMMateriales, insumos, lista de materiales (BOM) y modificadores tipo food-delivery.
Operaciones masivasRollback de operaciones bulk, editor de flujos visual, mutaciones con IA.
CSV masivoImport/export por CSV, síncrono y asíncrono, plantillas.
Publicación a canalCategorización de marketplace, exportación/publicación a canal, enriquecimiento T1.
ColeccionesAgrupaciones de productos (/collections/...).
CategoríasÁrbol propio, clasificación SAT/T1/Walmart/TikTok, mapeos hacia marketplaces (/categories/...).
Catálogo de erroresTodos los códigos de error conocidos del dominio, agrupados por su origen real.

Siguientes pasos