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.

12 min de lecturaActualizado el 30 de setiembre de 2026

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. polo encuentra «Polo algodón»; P-001 encuentra 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, como destacados o para-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_price y max_price: comparan el precio normal (price), no el de oferta ni el de las variantes. min_price: 0 equivale 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 ignora type_order.
  • sort_by solo ordena si también llega sort_direction. Con sort_by solo, el listado sale sin orden: ni el de sort_by ni el de type_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:

  1. Busca el primer tramo en el que cae la cantidad y usa su offer_price si está en oferta, o su price si no.
  2. Si la cantidad supera el max_quantity del último tramo, usa el último tramo.
  3. 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

  • stock del 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[].stock es 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_stock en los ajustes), el listado solo trae productos con stock mayor 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