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 ruta | Cubre |
|---|---|
/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.ioNo 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_keyTip
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 parcial | 207 + resultado propio del endpoint, típicamente con un array results[]/errors[] por ítem |
{
"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 }
}
}[
{ "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, base0en todos los endpoints de listado.limit: el default más común enlambda-productses20; el validador de la librería subyacente (validatePagination) usa50como default genérico y aplica un techo de500en 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 extend | Qué agrega |
|---|---|
variants | Array completo de variantes |
options | Opciones de variación (talla, color, etc.) |
description | Descripción larga / HTML |
bindings | Vínculos con canales de venta |
metadata | Metafields |
inventory | Inventario por variante y ubicación (join en vivo contra la colección inventory) |
dimensions | Dimensiones de envío |
seo | metaTitle, metaDescription, slug |
all | Todo 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:
| Ruta | Permiso(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.UPDATE — no 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íaPUT /products/{id}. Ver Gestionar productos. - Sin endpoints para borrar o reordenar medios: el array
media[]completo se reescribe víaPUT /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).
bundleConfiges 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ículo | Contenido |
|---|---|
| Catálogo y búsqueda | GET /products, búsqueda, conteo, filtros disponibles. |
| Consultar un producto | Detalle por ID/SKU, ?extend=, relacionados, variantes de un producto. |
| Modelo de datos | Forma real (Mongoose) de Product y ProductVariant, divergencias con el tipo TS público. |
| Gestionar productos | Crear, actualizar, borrar, rename de SKU. |
| Variantes y medios | Actualizar campos de una variante existente; subir imágenes/medios. |
| Compatibilidad (fitment) | Catálogos y nodos de compatibilidad para el vertical automotriz. |
| Materiales y BOM | Materiales, insumos, lista de materiales (BOM) y modificadores tipo food-delivery. |
| Operaciones masivas | Rollback de operaciones bulk, editor de flujos visual, mutaciones con IA. |
| CSV masivo | Import/export por CSV, síncrono y asíncrono, plantillas. |
| Publicación a canal | Categorización de marketplace, exportación/publicación a canal, enriquecimiento T1. |
| Colecciones | Agrupaciones de productos (/collections/...). |
| Categorías | Árbol propio, clasificación SAT/T1/Walmart/TikTok, mapeos hacia marketplaces (/categories/...). |
| Catálogo de errores | Todos los códigos de error conocidos del dominio, agrupados por su origen real. |