Productos
El listado de productos de la tienda, con búsqueda, filtros por categoría, atributo y precio, orden y paginación, y qué trae cada producto.
Listar productos
POST /api/v1/ecommerce/shop/products
No pide token. Es POST porque los filtros van en el cuerpo, pero no cambia nada: es una búsqueda.
curl -X POST https://<dominio-de-la-empresa>/api/v1/ecommerce/shop/products \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{ "categories": ["ropa"], "type_order": "ALFABETICO", "limit": 12, "page": 1 }'
Sin cuerpo devuelve la primera página de todo el catálogo, de 12 en 12.
Cuerpo
Todos los campos son opcionales y se combinan entre sí: un producto tiene que cumplir todos los filtros enviados.
| Campo | Tipo | Obligatorio | Reglas |
|---|---|---|---|
limit |
número | No | Productos por página. Entero entre 1 y 100. Sin él, 12 |
page |
número | No | Página a devolver. Entero, 1 o más. Sin él, la 1 |
description |
texto | No | Busca el texto dentro del nombre o del código interno (SKU). Máximo 255 caracteres |
barcode |
texto | No | Código de barras exacto. Máximo 100 caracteres |
categories |
lista | No | Ids o slugs de categoría, mezclados si hace falta. Hasta 50 |
categories[] |
texto o número | Con categories |
Solo letras, números, - y _. Máximo 255 caracteres |
attributes |
lista | No | Pares atributo-valor. Hasta 50 |
attributes[].attribute_id |
número | Con attributes |
Id de un atributo. Entero, 1 o más |
attributes[].value_id |
número | Con attributes |
Id de un valor de ese atributo (values[].id). Entero, 1 o más |
type_order |
texto | No | Orden del listado. Ver «Orden» más abajo |
sort_by |
texto | No | created_at, sale_unit_price o description. Solo funciona junto con sort_direction |
sort_direction |
texto | No | asc o desc |
min_price |
número | No | Precio mínimo. 0 o más |
max_price |
número | No | Precio máximo. 0 o más, y no menor que min_price |
page también se puede enviar en la dirección (?page=2); las dos formas valen.
Cómo filtra cada campo
description: coincidencia parcial, sin distinguir mayúsculas.poloencuentra «Polo algodón»;P-001encuentra el producto cuyo código interno lo contenga.barcode: solo el código exacto.categories: un producto está en una categoría si la tiene como categoría principal en su ficha del ERP o si se le asoció desde las categorías de la tienda. Basta con que esté en una de las enviadas. Una categoría que no existe no da error: la respuesta sale vacía. Admite también las categorías ocultas de la tienda, comodestacadosopara-ti, que se usan como secciones de la portada.attributes: el producto tiene que tener todos los pares enviados. Dos valores del mismo atributo (talla S y talla M) solo encuentran productos que tengan los dos. Se buscan los atributos asignados al producto en el ERP (Tienda virtual → Productos → Atributos), no los valores de sus variantes: un producto con variantes que no tenga ahí sus atributos no sale con este filtro.min_priceymax_price: comparan el precio normal (price), no el de oferta ni el de las variantes.min_price: 0equivale a no enviarlo.
Orden
type_order acepta estos valores:
| Valor | Ordena por |
|---|---|
ALFABETICO |
Nombre, de la A a la Z |
DEFAULTDESC |
Fecha de creación, del más reciente al más antiguo |
STOCKASC |
Stock, de menos a más |
STOCKDESC |
Stock, de más a menos |
SKUALFABETICO |
Código interno (SKU), de la A a la Z |
ALEATORIO |
Se acepta, pero hoy no cambia el orden en este listado |
Reglas:
- Si se envía
sort_by, se ignoratype_order. sort_bysolo ordena si también llegasort_direction. Consort_bysolo, el listado sale sin orden: ni el desort_byni el detype_order.- Sin ningún orden, los productos salen en el orden de la base de datos, que en la práctica es el de alta, pero no está garantizado.
El orden por defecto que la empresa elige en el ERP llega en ecommerce_order_type de los ajustes, pero esta ruta no lo aplica: si quieres respetarlo, envíalo tú en type_order. Su valor inicial, DEFAULT («de más antiguo a más reciente»), no es válido en type_order y da 422. Para ese caso, no envíes type_order.
Respuesta
{
"data": [
{
"id": 45,
"description": "Polo algodón premium",
"slug": "p-001-polo-algodon-premium",
"brand": { "id": 1, "marca": "Textiles Andinos" },
"internal_id": "P-001",
"barcode": "7750000000011",
"has_igv": null,
"price": 49.9,
"in_offer": true,
"offer_price": 39.9,
"prices": [],
"stock": 60,
"has_variants": true,
"files": [
{
"id": 1828,
"description": null,
"is_cover": true,
"path": "https://miempresa.ejemplo.com/attach-files/1828/preview",
"preview": "https://miempresa.ejemplo.com/attach-files/1828/preview/small"
}
],
"variants": [
{
"id": 9,
"sku": "P-001-S-NEG",
"price": 49.9,
"in_offer": true,
"offer_price": 39.9,
"stock": 20,
"prices": [],
"values": [
{ "id": 17, "attribute": "Talla", "value": "S", "color": null, "is_main": true },
{ "id": 18, "attribute": "Color", "value": "Negro", "color": "#111111", "is_main": false }
],
"files": []
}
],
"currency": { "id": "PEN", "description": "Soles", "symbol": "S/" },
"unit_type": { "id": "NIU", "description": "Unidades" },
"custom_measurement": null,
"min_order": 0,
"weight": null
}
],
"links": {
"first": "https://<dominio-de-la-empresa>/api/v1/ecommerce/shop/products?page=1",
"last": "https://<dominio-de-la-empresa>/api/v1/ecommerce/shop/products?page=3",
"prev": null,
"next": "https://<dominio-de-la-empresa>/api/v1/ecommerce/shop/products?page=2"
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 3,
"path": "https://<dominio-de-la-empresa>/api/v1/ecommerce/shop/products",
"per_page": 12,
"to": 12,
"total": 30
},
"success": true,
"message": "Información procesada de forma correcta."
}
El producto
| Campo | Qué es |
|---|---|
id |
Identificador del producto. Es el que se usa en el carrito (item_id) |
description |
Nombre del producto |
slug |
Nombre para la dirección de la ficha. Sirve igual que id en la ficha |
brand |
Marca (marca es su nombre), o null |
internal_id |
Código interno (SKU) |
barcode |
Código de barras, o null |
has_igv |
En el listado llega siempre null. El valor real está en la ficha |
price |
Precio normal |
in_offer |
Si el producto está en oferta |
offer_price |
Precio de oferta. Solo vale cuando in_offer es true; si no, suele llegar 0 |
prices |
Precios por cantidad. Lista vacía si no tiene. Ver «Precios por cantidad» |
stock |
Stock del producto en el almacén de la tienda. Ver «Stock» |
has_variants |
Si el producto se vende en variantes (talla, color…) |
files |
Imágenes. Ver «Imágenes» |
variants |
Variantes del producto. Lista vacía si no tiene |
currency |
Moneda de los precios: id (código ISO, por ejemplo PEN o USD), description y symbol |
unit_type |
Unidad de venta: id (código SUNAT, por ejemplo NIU) y description |
custom_measurement |
En el listado llega siempre null. Ver la ficha |
min_order |
Cantidad mínima de compra que tiene configurada el producto. 0 si no tiene. El carrito no la comprueba: si hay que exigirla, lo hace la tienda |
weight |
En el listado llega siempre null. Ver la ficha |
El listado no trae categories, additional ni specifications: esos campos solo están en la ficha.
Precio
El precio que paga el cliente es offer_price si in_offer es true, y price si no. Si el producto tiene prices, manda el tramo que corresponda a la cantidad (ver abajo).
Precios por cantidad
prices son tramos de precio según la cantidad que se compra:
"prices": [
{ "price": 45, "in_offer": false, "offer_price": 0, "min_quantity": 1, "max_quantity": 11 },
{ "price": 40, "in_offer": true, "offer_price": 38, "min_quantity": 12, "max_quantity": 50 }
]
| Campo | Qué es |
|---|---|
min_quantity, max_quantity |
Cantidades entre las que vale el tramo, las dos incluidas |
price |
Precio unitario del tramo |
in_offer, offer_price |
Si el tramo está en oferta y su precio de oferta |
Cómo calcula el ERP el precio al añadirlo al carrito:
- Busca el primer tramo en el que cae la cantidad y usa su
offer_pricesi está en oferta, o supricesi no. - Si la cantidad supera el
max_quantitydel último tramo, usa el último tramo. - Si no cae en ningún tramo (por ejemplo, por debajo del primero), usa el precio del producto.
Todos los valores llegan como números; un tramo incompleto llega con 0 en lo que falte.
Variantes
Cuando has_variants es true, el cliente elige una variante (por ejemplo, talla M en color azul). Cada variante tiene:
| Campo | Qué es |
|---|---|
variants[].id |
Identificador de la variante. Es el que se envía en variants[].id al añadir al carrito |
variants[].sku |
Código de la variante |
variants[].price, in_offer, offer_price |
Precio propio de la variante, con la misma regla que el producto |
variants[].prices |
Precios por cantidad propios de la variante, con la misma forma que prices |
variants[].stock |
Stock propio de la variante. Ver «Stock» |
variants[].values |
La combinación que forma la variante: un elemento por atributo |
variants[].values[].attribute |
Nombre del atributo, por ejemplo Talla |
variants[].values[].value |
Valor, por ejemplo M |
variants[].values[].color |
Color en #RRGGBB si el atributo es de color; si no, null |
variants[].values[].is_main |
true en el atributo principal del producto. Sirve para mostrarlo primero (por ejemplo, elegir la talla antes que el color) |
variants[].files |
Imágenes propias de la variante: id, path (dirección completa), extension, size e is_cover. Lista vacía si no tiene |
values[].id es el identificador de ese valor dentro de la variante, no el del valor del atributo: no sirve para el filtro attributes. Para relacionar una variante con los atributos, compara por nombre (attribute y value).
Si la tienda vende como mayorista (ecommerce_business_models en los ajustes), el carrito cobra las variantes con el precio y los tramos del producto, sobre la suma de las cantidades de todas sus variantes. Si no, cada variante se cobra con su propio precio y sus propios tramos.
Stock
stockdel producto es la suma de sus movimientos de inventario (kárdex) en el almacén de la tienda: entradas menos salidas. Puede ser 0 o negativo.variants[].stockes un número aparte que la empresa lleva en la tienda para cada variante. No sale del kárdex, y no tiene por qué sumar el stock del producto.- Con «mostrar solo productos con stock» activado en la tienda (
ecommerce_show_only_stocken los ajustes), el listado solo trae productos constockmayor que 0. Se mira el stock del producto, no el de sus variantes.
Imágenes
files son las imágenes del producto, en el orden que eligió la empresa.
| Campo | Qué es |
|---|---|
files[].id |
Identificador de la imagen |
files[].description |
Texto de la imagen, o null |
files[].is_cover |
true en la imagen de portada |
files[].path |
Dirección de la imagen en tamaño completo |
files[].preview |
Dirección de la miniatura. Si no hay miniatura, la misma que path |
Si el producto no tiene imágenes en la tienda, files trae un único elemento con id en null e is_cover en true, armado con la imagen de la ficha del ERP (storage/uploads/items/…). Si el producto tampoco tiene esa imagen, es la genérica del ERP, imagen-no-disponible.jpg.
Paginación
| Campo | Qué es |
|---|---|
meta.total |
Productos que cumplen los filtros |
meta.per_page |
Productos por página (limit) |
meta.current_page, meta.last_page |
Página actual y última |
meta.from, meta.to |
Posición del primer y último producto de la página, o null si está vacía |
links |
Direcciones de la primera, última, anterior y siguiente página |
Las direcciones de links son de tipo GET y no llevan los filtros, así que no se pueden seguir tal cual: un GET a esta ruta da 405. Para pasar de página, repite el mismo POST con los mismos filtros y cambia page.
Si algo sale mal
Un filtro mal escrito da 422: no se ignora en silencio. message es el primer error y errors trae cada campo con su mensaje:
{
"success": false,
"code": "VALIDATION_ERROR",
"message": "El precio máximo no puede ser menor que el precio mínimo.",
"errors": { "max_price": ["El precio máximo no puede ser menor que el precio mínimo."] }
}
Los mensajes de cada campo:
| Campo | Mensaje |
|---|---|
limit |
«El límite de productos por página debe ser un número entero.», «… debe ser 1 o más.» o «El máximo de productos por página es 100.» |
page |
«La página debe ser un número entero.» o «La primera página es la 1.» |
description |
«La búsqueda por descripción debe ser un texto.» o «… no puede pasar de 255 caracteres.» |
barcode |
«El código de barras debe ser un texto.» o «… no puede pasar de 100 caracteres.» |
categories |
«El filtro de categorías debe ser una lista de ids o slugs.», «No se pueden pedir más de 50 categorías a la vez.» o, en categories.N, «Cada categoría debe ser un id o un slug.» |
attributes |
«El filtro de atributos debe ser una lista.», «No se pueden pedir más de 50 atributos a la vez.», y en cada elemento «Cada atributo debe ser un objeto con attribute_id y value_id.», «Cada atributo necesita un attribute_id numérico.» o «Cada atributo necesita un value_id numérico.» |
type_order |
«El orden solicitado no existe. Válidos: ALFABETICO, DEFAULTDESC, STOCKASC, STOCKDESC, SKUALFABETICO, ALEATORIO.» |
sort_by |
«Solo se puede ordenar por created_at, sale_unit_price o description.» |
sort_direction |
«La dirección del orden solo puede ser asc o desc.» |
min_price, max_price |
«El precio mínimo debe ser un número.», «El precio mínimo no puede ser negativo.» (y lo mismo para el máximo), o «El precio máximo no puede ser menor que el precio mínimo.» |
Los errores de un elemento de una lista llegan con su posición: categories.0, attributes.1.value_id.
| HTTP | code | Cuándo | Qué hacer |
|---|---|---|---|
| 404 | STORE_NOT_FOUND |
La tienda no está publicada | La empresa debe publicarla desde el ERP |
| 503 | STORE_PAUSED |
La tienda está pausada | Reintenta más tarde (Retry-After) |
| 503 | STORE_NOT_CONFIGURED |
El ERP no tiene ningún almacén | Avisa a la empresa |
| 422 | VALIDATION_ERROR |
Un filtro no es válido | Corrige el campo que indica errors |
| 405 | METHOD_NOT_ALLOWED |
Se llamó con GET |
Usa POST |
| 429 | RATE_LIMITED |
Se superó el límite | Espera lo que indica Retry-After |
| 500 | INTERNAL_ERROR |
Fallo del servidor | Reporta la reference |