Productos y existencias
Con estos endpoints tu tienda en línea lee el catálogo del comercio, lo mantiene al día, consulta cuántas unidades hay en cada bodega y sabe desde qué sucursal vende.
| Endpoint | Permiso |
|---|---|
GET /products | catalog:read |
GET /products/{id} | catalog:read |
GET /categories | catalog:read |
GET /brands | catalog:read |
GET /stock | stock:read |
GET /branches | orders:write o stock:read |
GET /warehouses | orders:write o stock:read |
Cómo está organizado el catálogo
- Un producto es la ficha: nombre, categoría, marca, impuesto e imágenes.
- Cada producto tiene una o más variantes, que son lo que se vende: cada talla o color, con su SKU, código de barras y precio. Un producto sin opciones tiene una sola variante.
- Para vender, tu tienda envía el SKU de la variante. Una variante sin SKU no se puede vender por la API.
Listar productos
GET /products| Parámetro | Tipo | Descripción |
|---|---|---|
page | entero | Página, desde 1. Por defecto 1 |
limit | entero | Resultados por página, de 1 a 100. Por defecto 50 |
updatedSince | fecha ISO 8601 | Solo los productos que cambiaron en ese momento o después. Debe llevar zona horaria: 2026-10-06T00:00:00Z o 2026-10-06T00:00:00-05:00 |
cursor | texto | El nextCursor de la respuesta anterior. Pide la página siguiente sin saltarse productos; con cursor, page se ignora |
categoryId | texto | Solo los productos de esa categoría. Es el id de category de cualquiera de sus productos |
search | texto, hasta 120 caracteres | Busca por nombre del producto, SKU o código de barras |
Los productos salen ordenados por updatedAt, del más antiguo al más reciente.
curl "https://api.zenci.app/public/v1/products?limit=2" \
-H "Authorization: Bearer $ZENCI_API_KEY"{
"data": [
{
"id": "p4k8m2x9q7w1e5r3t6y0u2i8",
"name": "Sandalia Crucero",
"type": "product",
"category": { "id": "c2v6b8n4m1q9w3e7r5t0y2u4", "name": "Calzado" },
"brand": { "id": "d9f3g7h1j5k2l8z4x6c0v2b6", "name": "Marea" },
"taxRate": "7.00",
"images": [
{
"id": "i1o5p9a3s7d2f6g0h4j8k2l6",
"url": "https://imagenes.mareaboutique.test/sandalia-crucero-cruda.jpg"
}
],
"variants": [
{
"id": "v3q7w1e5r9t2y6u0i4o8p2a6",
"sku": "MB-029-CRU-38",
"barcode": "7451234560381",
"name": "Sandalia Crucero - Cruda / 38",
"price": "36.00",
"priceSource": "catalog",
"attributes": [
{ "option": "Color", "value": "Cruda" },
{ "option": "Talla", "value": "38" }
]
},
{
"id": "v8s2d6f0g4h7j1k5l9z3x7c1",
"sku": "MB-029-CRU-39",
"barcode": "7451234560398",
"name": "Sandalia Crucero - Cruda / 39",
"price": "36.00",
"priceSource": "catalog",
"attributes": [
{ "option": "Color", "value": "Cruda" },
{ "option": "Talla", "value": "39" }
]
}
],
"priceList": null,
"deleted": false,
"createdAt": "2026-08-14T14:02:11.000Z",
"updatedAt": "2026-10-02T21:15:40.000Z"
},
{
"id": "p6n0b4v8c2x6z1l5k9j3h7g1",
"name": "Arreglo de ruedo",
"type": "service",
"category": null,
"brand": null,
"taxRate": null,
"images": [],
"variants": [
{
"id": "v5t9y3u7i1o4p8a2s6d0f4g8",
"sku": "MB-SRV-RUEDO",
"barcode": null,
"name": "Arreglo de ruedo",
"price": "5.00",
"priceSource": "catalog",
"attributes": []
}
],
"priceList": null,
"deleted": false,
"createdAt": "2026-09-01T16:40:00.000Z",
"updatedAt": "2026-10-03T13:22:05.000Z"
}
],
"pagination": { "page": 1, "limit": 2, "total": 128, "totalPages": 64 },
"nextCursor": "WyIyMDI2LTEwLTAzVDEzOjIyOjA1LjAwMFoiLCJwNm4wYjR2OGMyeDZ6MWw1azlqM2g3ZzEiXQ"
}nextCursor es la llave para pedir la página siguiente. Vale null cuando la página vino incompleta, es decir, cuando ya no hay más productos. Trátalo como un texto opaco: no lo armes a mano.
Campos del producto
| Campo | Descripción |
|---|---|
id | Identificador del producto |
name | Nombre del producto |
type | product (mercancía con existencias) o service (servicio, no mueve inventario) |
category, brand | { id, name }, o null si no tiene |
taxRate | Porcentaje de impuesto, por ejemplo "7.00", o null si no lleva impuesto |
images | Imágenes del producto, con id y url |
variants | Variantes que se venden (ver abajo) |
priceList | { id, name } de la lista de precios de la clave, o null si la clave usa los precios del catálogo |
deleted | true si el producto se eliminó. Solo aparece en true cuando consultas con updatedSince |
createdAt, updatedAt | Fechas de creación y del último cambio |
Campos de la variante
| Campo | Descripción |
|---|---|
id | Identificador de la variante |
sku | SKU con el que se vende por la API, o null |
barcode | Código de barras, o null |
name | Nombre del producto seguido de los valores de sus opciones: Sandalia Crucero - Cruda / 38 |
price | Precio de venta por unidad, sin impuesto |
priceSource | catalog si es el precio del catálogo, price_list si sale de la lista de precios de la clave |
attributes | Opciones de la variante, como { "option": "Talla", "value": "38" } |
Precios y lista de precios
price es el mismo precio que POST /orders cobra por esa variante con la misma clave. No incluye el impuesto: para mostrar el precio final, súmale taxRate. Por ejemplo, 36.00 con taxRate 7.00 se muestra como 38.52. El cálculo exacto está en .
Si la clave tiene una lista de precios, cada variante trae el precio de esa lista. Una variante que la lista no cubre sale con el precio del catálogo y priceSource: "catalog"; por eso priceSource va en cada variante.
Si la lista de la clave se elimina o se desactiva, el catálogo sigue respondiendo con los precios del catálogo y priceList pasa a null. No da error: revisa priceList si tu tienda depende de esa lista.
Leer un producto
GET /products/{id}Devuelve un producto con la misma forma que cada elemento de la lista. Un producto eliminado, o de otro comercio, responde 404 not_found con el mensaje Product not found.
curl https://api.zenci.app/public/v1/products/p4k8m2x9q7w1e5r3t6y0u2i8 \
-H "Authorization: Bearer $ZENCI_API_KEY"Sincronizar el catálogo
Para mantener una copia del catálogo en tu tienda, sin descargarlo completo cada vez:
- Primera carga. Llama a
GET /products?limit=100y sigue pidiendo concursor=<nextCursor>hasta quenextCursorvenga ennull. Así recibes todos los productos vigentes. - Guarda una marca. Anota el
updatedAtdel último producto que procesaste. - Siguientes cargas. Llama a
GET /products?updatedSince=<marca>&limit=100y sigue losnextCursorigual que en la primera carga. Recibes solo lo que cambió desde la marca, incluidos los productos eliminados condeleted: true: quítalos de tu tienda. - Actualiza la marca con el
updatedAtdel último producto de esa pasada.
Ten en cuenta:
- Recorre siempre con
cursor, no conpage. Conpage, un producto que cambia mientras recorres salta al final de la lista y corre a los demás una posición, y el siguiente quedaría sin leer. El cursor no tiene ese problema. updatedSinceincluye el momento exacto de la marca, así que el último producto de la pasada anterior vuelve a llegar. Actualiza poridy no lo dupliques.- Cambian el
updatedAtde un producto: editar su ficha o sus variantes, eliminarlo, y cambiar su precio en una lista de precios. - Las existencias no son parte del producto y no cambian su
updatedAt. Consúltalas enGET /stock. - Sin
updatedSince, los productos eliminados no aparecen. - Los
product.updatedyproduct.deletedte dicen cuándo vale la pena sincronizar, sin consultar a cada rato.
curl "https://api.zenci.app/public/v1/products?updatedSince=2026-10-02T21:15:40.000Z&limit=100" \
-H "Authorization: Bearer $ZENCI_API_KEY"
curl "https://api.zenci.app/public/v1/products?updatedSince=2026-10-02T21:15:40.000Z&limit=100&cursor=WyIyMDI2LTEw…" \
-H "Authorization: Bearer $ZENCI_API_KEY"Categorías y marcas
GET /categories
GET /brandsSirven para armar los filtros de tu tienda. No se paginan: traen todas las del comercio. Con el id de una categoría puedes filtrar GET /products?categoryId=….
{
"data": [
{ "id": "c2v6b8n4m1q9w3e7r5t0y2u4", "name": "Calzado", "slug": "calzado", "parentId": null },
{ "id": "c8x2z6l0k4j7h1g5f9d3s7a1", "name": "Sandalias", "slug": "sandalias", "parentId": "c2v6b8n4m1q9w3e7r5t0y2u4" }
]
}parentId indica la categoría de la que depende, o null si es de primer nivel. Las marcas traen solo id y name.
Consultar existencias
GET /stock| Parámetro | Tipo | Descripción |
|---|---|---|
page | entero | Página, desde 1. Por defecto 1 |
limit | entero | Variantes por página, de 1 a 100. Por defecto 50 |
sku | texto | Solo la variante con ese SKU exacto |
warehouseId | texto | Solo esa bodega |
Devuelve una fila por variante y bodega. Solo aparecen las variantes que tienen existencias registradas en alguna bodega; los servicios no tienen.
curl "https://api.zenci.app/public/v1/stock?sku=MB-029-CRU-38" \
-H "Authorization: Bearer $ZENCI_API_KEY"{
"data": [
{
"variantId": "v3q7w1e5r9t2y6u0i4o8p2a6",
"sku": "MB-029-CRU-38",
"warehouseId": "w2e6r0t4y8u1i5o9p3a7s1d5",
"warehouseName": "Bodega Obarrio",
"availableQty": "4",
"stockedQty": "5"
},
{
"variantId": "v3q7w1e5r9t2y6u0i4o8p2a6",
"sku": "MB-029-CRU-38",
"warehouseId": "w9f3g7h1j4k8l2z6x0c4v8b2",
"warehouseName": "Bodega Costa del Este",
"availableQty": "2",
"stockedQty": "2"
}
],
"pagination": { "page": 1, "limit": 50, "total": 1, "totalPages": 1 }
}| Campo | Descripción |
|---|---|
variantId, sku | La variante |
warehouseId, warehouseName | La bodega |
stockedQty | Unidades físicas en la bodega |
availableQty | Unidades que se pueden vender: las físicas menos las reservadas (por ejemplo, por apartados). Es la que debes mostrar en tu tienda. Puede ser negativa si el comercio vende sin existencias |
La paginación cuenta variantes, no filas
limit, total y totalPages cuentan variantes. Cada variante trae una fila por cada bodega donde tiene existencias, así que una página puede traer más filas que limit. En el ejemplo, total es 1 y data trae dos filas.
Presentaciones enlazadas
Una presentación enlazada es una variante que no tiene inventario propio y se descuenta de otra: por ejemplo, una caja de 12 que sale de las unidades sueltas. Su fila muestra existencias calculadas: las unidades de la base divididas entre las que trae la presentación, redondeadas hacia abajo.
Si hay 30 unidades sueltas disponibles, la caja de 12 muestra availableQty: "2". Al vender una caja por la API se descuentan 12 unidades de la base, y las dos filas cambian.
Qué bodega mirar
Una venta por la API descuenta de la bodega de la sucursal donde se registra (la de la clave, o la que envíes en branchId). Busca esa sucursal en GET /branches, toma su warehouseId y filtra GET /stock por él para mostrar lo que de verdad puedes vender.
Sucursales y bodegas
GET /branches
GET /warehousesLas pide quien vende (orders:write) o quien consulta existencias (stock:read). No se paginan.
{
"data": [
{
"id": "b7n2q8w4e1r9t3y6u0i5o2p7",
"name": "Obarrio",
"code": "001",
"address": "Calle 50, edificio Marea",
"phone": "+507 6000-0000",
"warehouseId": "w2e6r0t4y8u1i5o9p3a7s1d5"
}
]
}| Campo | Descripción |
|---|---|
id | El branchId que acepta POST /orders |
code | Código de la sucursal |
warehouseId | La bodega de la que descuenta una venta en esa sucursal |
GET /warehouses trae id, name e isBranch, que vale true si la bodega es la de una sucursal.
Siguientes pasos
- — Vende por SKU.
- — Registra a quién le vendes y a qué compañía se factura.
- — Entérate de los cambios de producto, existencias y precio sin consultar a cada rato.