Products
Products represent the items available for sale in your application. On this page, we'll dive into the different product endpoints you can use to manage products programmatically. We'll look at how to query, create, update, and delete products.
The product model
The product model includes detailed information about the product, including its variants, prices, and inventory details.
Properties
- Name
id- Type
- string
- Description
Unique identifier for the product.
- Name
name- Type
- string
- Description
The name of the product.
- Name
description- Type
- string
- Description
The description of the product.
- Name
imageUrl- Type
- string
- Description
URL of the product image.
- Name
price- Type
- number
- Description
The base price of the product.
- Name
discountPrice- Type
- number
- Description
The lowest price of the product.
- Name
currency- Type
- string
- Description
The currency code of the product price.
- Name
variants- Type
- array
- Description
List of product variants.
- Name
id- Type
- string
- Description
Unique identifier for the product variant.
- Name
name- Type
- string
- Description
The name of the variant.
- Name
inventoryQuantity- Type
- number
- Description
The inventory quantity of the variant.
- Name
inventoryTotal- Type
- number
- Description
The total inventory quantity of the variant in grams
- Name
price- Type
- number
- Description
The base price of the variant.
- Name
discountPrice- Type
- number
- Description
The discounted price of the variant.
- Name
discountPercentage- Type
- number
- Description
The discount percentage of the variant.
- Name
currencyCode- Type
- string
- Description
The currency code of the variant price.
- Name
priceListId- Type
- string
- Description
The price list identifier of the variant.
- Name
priceListName- Type
- string
- Description
The name of the price list.
- Name
weight- Type
- number
- Description
The weight of the variant.
- Name
prices- Type
- array
- Description
List of prices for the variant.
- Name
id- Type
- string
- Description
Unique identifier for the price.
- Name
price- Type
- number
- Description
The price amount.
- Name
priceList- Type
- object
- Description
The price list details.
- Name
id- Type
- string
- Description
Unique identifier for the price list.
- Name
name- Type
- string
- Description
The name of the price list.
- Name
currency- Type
- string
- Description
The currency code of the price.
- Name
createdAt- Type
- string
- Description
The timestamp when the price was created.
- Name
quantity- Type
- number
- Description
The quantity of the variant (grams in the fraction).
- Name
strain- Type
- object
- Description
The strain information of the product if it is flower
- Name
cannabinoids- Type
- object
- Description
The cannabinoids content of the strain.
- Name
thc- Type
- number
- Description
The THC content percentage of the strain.
- Name
cbg- Type
- number
- Description
The CBG content percentage of the strain.
- Name
cbd- Type
- number
- Description
The CBD content percentage of the strain.
- Name
terpenes- Type
- array
- Description
List of terpenes present in the strain.
- Name
flavors- Type
- array
- Description
List of flavors associated with the strain.
- Name
genetics- Type
- object
- Description
The genetics of the strain.
- Name
parents- Type
- array
- Description
List of parent strains.
- Name
children- Type
- array
- Description
List of child strains.
- Name
category- Type
- string
- Description
The category of the product.
- Name
isFlower- Type
- boolean
- Description
Whether the product is a flower.
- Name
inventoryQuantity- Type
- number
- Description
The total inventory quantity of the product.
Complete product example
{
"id": "prod_01HQ8BLUE_DREAM",
"name": "Blue Dream Indoor",
"description": "Blue Dream es una cepa híbrida sativa-dominante que cruza Blueberry con Haze. Conocida por su equilibrio entre relajación cerebral y estimulación corporal, es ideal para aliviar dolor, depresión y náuseas mientras mantiene claridad mental.",
"imageUrl": "https://storage.cannahub.tech/products/blue-dream-main.jpg",
"price": 330000,
"discountPrice": 320000,
"currency": "ARS",
"variants": [
{
"id": "variant_01HQ_BD_1G",
"name": "Blue Dream 1g",
"inventoryQuantity": 20,
"inventoryTotal": 20,
"weight": 1,
"price": 350000,
"discountPrice": null,
"discountPercentage": 0,
"currencyCode": "ARS",
"priceListId": "plist_basic",
"priceListName": "Básica",
"quantity": 1,
"prices": [
{
"id": "price_01HQ_BD_1G_BASIC",
"price": 350000,
"priceList": {
"id": "plist_basic",
"name": "Básica"
},
"currency": "ARS",
"createdAt": "2024-03-15T10:00:00Z"
},
{
"id": "price_01HQ_BD_1G_PREMIUM",
"price": 332500,
"priceList": {
"id": "plist_premium",
"name": "Premium"
},
"currency": "ARS",
"createdAt": "2024-03-15T10:00:00Z"
}
]
},
{
"id": "variant_01HQ_BD_5G",
"name": "Blue Dream 5g",
"inventoryQuantity": 8,
"inventoryTotal": 40,
"weight": 5,
"price": 1650000,
"discountPrice": 1650000,
"discountPercentage": 6,
"currencyCode": "ARS",
"priceListId": "plist_basic",
"priceListName": "Básica",
"quantity": 5,
"prices": [
{
"id": "price_01HQ_BD_5G_BASIC",
"price": 1650000,
"priceList": {
"id": "plist_basic",
"name": "Básica"
},
"currency": "ARS",
"createdAt": "2024-03-15T10:00:00Z"
}
]
}
],
"strain": {
"id": "strain_blue_dream",
"name": "Blue Dream",
"type": "hybrid",
"cannabinoids": {
"thc": 20,
"cbg": 0.8,
"cbd": 1.5
},
"terpenes": ["Mirceno", "Pineno", "Caryofileno", "Limoneno"],
"flavors": ["Arándano", "Dulce", "Terroso"],
"genetics": {
"parents": ["Blueberry", "Haze"],
"children": []
},
"effects": {
"helpsWith": ["Dolor", "Depresión", "Náuseas"],
"feelings": ["Eufórico", "Creativo", "Relajado"],
"negatives": ["Boca seca", "Ojos rojos"]
}
},
"category": "flores",
"isFlower": true,
"manageInventory": true,
"inventoryQuantity": 60
}
Field Mapping Reference
Cannahub uses Medusa.js for product management. Here's how frontend types map to Medusa API:
Product ↔ Medusa Product Mapping
| Cannahub Frontend | Medusa API | Type | Notes |
|---|---|---|---|
id | product.id | string | Medusa product ID |
name | product.title | string | - |
description | product.description | string | Full description |
imageUrl | product.thumbnail | string (URL) | Main product image |
images | product.images[] | array | All product images |
price | Calculated from variants | number | Lowest variant price |
discountPrice | Calculated from variants | number | Lowest discount price |
currency | region.currency_code | string | From region (ARS, USD, etc.) |
variants | product.variants[] | array | Product fractions |
strain | product.metadata.strain | object | Complete strain info in metadata |
category | product.metadata.category | string | flores, extracciones, etc. |
isFlower | product.metadata.isFlower | boolean | Whether product is flower |
manageInventory | variant.manage_inventory | boolean | Track stock or not |
inventoryQuantity | Sum of variant quantities | number | Total units across all variants |
Variant ↔ Medusa Variant Mapping
| Cannahub Frontend | Medusa API | Type | Notes |
|---|---|---|---|
id | variant.id | string | - |
name | variant.title | string | e.g. "Blue Dream 5g" |
inventoryQuantity | variant.inventory_quantity | number | Units available |
inventoryTotal | variant.metadata.inventoryTotal | number | Total grams (quantity × weight) |
weight | variant.metadata.weight | number | Grams per unit |
price | variant.prices[].amount | number | Price for current price list |
discountPrice | Calculated | number | If discount applied |
discountPercentage | variant.metadata.discount_percentage | number | Discount % for this fraction |
quantity | variant.metadata.quantity | number | Same as weight (for clarity) |
prices[] | variant.prices[] | array | All prices across price lists |
Complete Product Metadata Structure
{
"metadata": {
"strainId": "strain_blue_dream",
"isFlower": true,
"requireReprocann": true,
"category": "flores",
"subcategory": "hibridas",
"thc": 20,
"cbd": 1.5,
"cbg": 0.8,
"terpenes": ["mirceno", "pineno", "caryofileno", "limoneno"],
"effects": ["energetic", "creative", "euphoric"],
"cultivationType": "indoor",
"harvestDate": "2024-03-01",
"batchNumber": "BATCH-2024-03-001",
"strain": {
"id": "strain_blue_dream",
"name": "Blue Dream",
"altNames": "Azure Haze, BD",
"type": "hybrid",
"description": "Cepa híbrida equilibrada...",
"cannabinoids": {
"thc": 20,
"cbd": 1.5,
"cbg": 0.8
},
"terpenes": ["Mirceno", "Pineno", "Caryofileno"],
"flavors": ["Arándano", "Dulce", "Terroso"],
"genetics": {
"parents": ["Blueberry", "Haze"],
"children": []
},
"effects": {
"helpsWith": ["Dolor", "Depresión", "Náuseas"],
"feelings": ["Eufórico", "Creativo", "Relajado"],
"negatives": ["Boca seca", "Ojos rojos"]
},
"growInformation": {
"type": "indoor",
"difficulty": "moderate",
"height": "medium",
"yield": "high",
"floweringWeeks": "9-10",
"notes": "Requiere podas regulares"
}
}
}
}
Variant Metadata Structure
{
"metadata": {
"weight": 5,
"unit": "grams",
"inventoryTotal": 40,
"quantity": 5,
"packaging": "jar",
"discount_percentage": 6
}
}
List all products
This endpoint allows you to retrieve a paginated list of all your products. By default, a maximum of ten products are shown per page.
Optional attributes
- Name
category- Type
- string
- Description
Filter products by category.
- Name
limit- Type
- integer
- Description
Limit the number of products returned.
- Name
page- Type
- integer
- Description
Start offset of products returned.
- Name
sort- Type
- string
- Description
Sort the list of products by specified attribute.
- Name
direction- Type
- string
- Description
Sort direction, can be 'asc' or 'desc'.
- Name
search- Type
- string
- Description
Search products by name or description.
Request
curl -G https://api.yourapp.com/store/products \
-H "Authorization: Bearer {token}" \
-d limit=10
Response
[
{
"id": "l7cGNIBKZiNJ6wqF",
"name": "Product 1",
"description": "This is product 1",
"imageUrl": "https://assets.yourapp.com/products/product1.jpg",
"price": 100,
"discountPrice": 80,
"currency": "USD",
"variants": [
{
"id": "variant1",
"name": "Variant 1",
"inventoryQuantity": 50,
"price": 100,
"currencyCode": "USD",
// ...
}
],
"strain": null,
"category": "Category 1",
"isFlower": true,
"inventoryQuantity": 50
}
]
Retrieve a product
This endpoint allows you to retrieve a product by providing the product ID.
Request
curl https://yourapp.com/api/products/:id \
-H "Authorization: Bearer {token}"
Response
{
"id": "l7cGNIBKZiNJ6wqF",
"name": "Product 1",
"description": "This is product 1",
"imageUrl": "https://assets.yourapp.com/products/product1.jpg",
"price": 100,
"discountPrice": 80,
"currency": "USD",
"variants": [
{
"id": "variant1",
"name": "Variant 1",
"inventoryQuantity": 50,
"price": 100,
"currencyCode": "USD"
}
],
"strain": null,
"category": "Category 1",
"isFlower": true,
"inventoryQuantity": 50
}
Create a product
This endpoint allows you to create a new product in your application.
Medusa Reference
Required attributes
- Name
name- Type
- string
- Description
The name of the product.
- Name
description- Type
- string
- Description
The description of the product.
- Name
imageUrl- Type
- string
- Description
URL of the product image.
- Name
category- Type
- string
- Description
The category of the product.
Optional attributes
- Name
strain- Type
- object
- Description
The strain information of the product.
Request
curl -X POST https://yourapp.com/api/products \
-H "Authorization: Bearer {token}" \
-d '{
"name": "Product 1",
"description": "This is product 1",
"imageUrl": "https://assets.yourapp.com/products/product1.jpg",
"currency": "USD"
}'
Response
{
"id": "l7cGNIBKZiNJ6wqF",
"name": "Product 1",
"description": "This is product 1",
"imageUrl": "https://assets.yourapp.com/products/product1.jpg",
"currency": "USD",
"variants": [],
"strain": null,
"category": "Category 1",
"isFlower": true,
}
Update a product
This endpoint allows you to update an existing product in your application.
Medusa Reference
Optional attributes
- Name
name- Type
- string
- Description
The name of the product.
- Name
description- Type
- string
- Description
The description of the product.
- Name
imageUrl- Type
- string
- Description
URL of the product image.
- Name
category- Type
- string
- Description
The category of the product.
- Name
strain- Type
- object
- Description
The strain information of the product.
Request
curl -X PUT https://yourapp.com/api/products/:id \
-H "Authorization: Bearer {token}" \
-d '{
"name": "Updated Product",
"description": "This is an updated product",
"imageUrl": "https://assets.yourapp.com/products/updatedproduct.jpg",
}'
Response
{
"id": "l7cGNIBKZiNJ6wqF",
"name": "Updated Product",
"description": "This is an updated product",
"imageUrl": "https://assets.yourapp.com/products/updatedproduct.jpg",
"currency": "USD",
"variants": [],
"strain": null,
"category": "Category 1",
"isFlower": true,
}
Create a product variant
This endpoint allows you to create a new product variant in your application.
Medusa Reference
Required attributes
- Name
name- Type
- string
- Description
The name of the variant.
- Name
inventoryQuantity- Type
- number
- Description
The inventory quantity of the variant.
- Name
currencyCode- Type
- string
- Description
The currency code of the variant price.
Optional attributes
- Name
weight- Type
- number
- Description
The weight of the variant.
Request
curl -X POST https://yourapp.com/api/products/:productId/variants \
-H "Authorization: Bearer {token}" \
-d '{
"name": "New Variant",
"inventoryQuantity": 50,
"currencyCode": "USD"
}'
Response
{
"id": "variantId",
"name": "New Variant",
"inventoryQuantity": 50,
"currencyCode": "USD",
"weight": 10
}
BFF Route: Product with Inventory
Esta ruta BFF resuelve el problema N+1 al obtener un producto con todos sus datos de inventario. En lugar de hacer múltiples llamadas (1 para el producto + N para cada variante), esta ruta agrega toda la información en una sola llamada.
Problema que resuelve
Flujo actual (N+1 calls):
1. GET /admin/products/:id → Producto base
2. GET /admin/stock-locations → Ubicaciones disponibles
3. GET /admin/inventory-items?variant_id=v1 → Inventario variante 1
4. GET /admin/inventory-items?variant_id=v2 → Inventario variante 2
5. GET /admin/inventory-items?variant_id=v3 → Inventario variante 3
...N más por cada variante
Ejemplo: Producto con 5 variantes = 7+ llamadas API
Request
- Name
id- Type
- string
- Description
El ID del producto.
- Name
locationId- Type
- string
- Description
Filtrar inventario por ubicación específica. Si no se proporciona, retorna inventario de todas las ubicaciones.
Response
- Name
product- Type
- Product
- Description
El producto completo con strain, categoría y metadatos.
- Name
inventory- Type
- object
- Description
Mapa de inventario por variante.
- Name
[variantId]- Type
- VariantInventory
- Description
Datos de inventario para cada variante.
- Name
quantity- Type
- number
- Description
Cantidad total de unidades.
- Name
available- Type
- number
- Description
Cantidad disponible para venta.
- Name
reserved- Type
- number
- Description
Cantidad reservada en carritos/pedidos.
- Name
totalGrams- Type
- number
- Description
Total en gramos (quantity × weight).
- Name
byLocation- Type
- array
- Description
Desglose por ubicación.
- Name
locationId- Type
- string
- Description
ID de la ubicación.
- Name
locationName- Type
- string
- Description
Nombre de la ubicación.
- Name
quantity- Type
- number
- Description
Cantidad en esta ubicación.
- Name
available- Type
- number
- Description
Disponible en esta ubicación.
- Name
totalInventoryGrams- Type
- number
- Description
Total de gramos en inventario sumando todas las variantes.
- Name
totalInventoryUnits- Type
- number
- Description
Total de unidades en inventario.
- Name
lowStockVariants- Type
- array
- Description
Lista de variantes con stock bajo (menos de 5 unidades).
Request
curl https://api.cannahub.tech/api/products/prod_01HQ8BLUE_DREAM/with-inventory \
-H "Authorization: Bearer {token}"
Response
{
"product": {
"id": "prod_01HQ8BLUE_DREAM",
"name": "Blue Dream Indoor",
"description": "Cepa híbrida sativa-dominante...",
"imageUrl": "https://storage.cannahub.tech/products/blue-dream.jpg",
"price": 330000,
"currency": "ARS",
"variants": [
{
"id": "variant_01HQ_BD_1G",
"name": "Blue Dream 1g",
"weight": 1,
"price": 350000
},
{
"id": "variant_01HQ_BD_5G",
"name": "Blue Dream 5g",
"weight": 5,
"price": 1650000
}
],
"strain": {
"name": "Blue Dream",
"type": "hybrid",
"cannabinoids": { "thc": 20, "cbd": 1.5 }
},
"isFlower": true
},
"inventory": {
"variant_01HQ_BD_1G": {
"quantity": 20,
"available": 18,
"reserved": 2,
"totalGrams": 20,
"byLocation": [
{
"locationId": "sloc_dispensary",
"locationName": "Dispensario Principal",
"quantity": 15,
"available": 13
},
{
"locationId": "sloc_warehouse",
"locationName": "Depósito",
"quantity": 5,
"available": 5
}
]
},
"variant_01HQ_BD_5G": {
"quantity": 8,
"available": 8,
"reserved": 0,
"totalGrams": 40,
"byLocation": [
{
"locationId": "sloc_dispensary",
"locationName": "Dispensario Principal",
"quantity": 8,
"available": 8
}
]
}
},
"totalInventoryGrams": 60,
"totalInventoryUnits": 28,
"lowStockVariants": []
}
Flujo interno del BFF
Comparación de rendimiento
| Métrica | Sin BFF | Con BFF | Mejora |
|---|---|---|---|
| Llamadas API (5 variantes) | 7+ | 1 | -86% |
| Latencia típica | ~700ms | ~200ms | -71% |
| Cálculo de gramos | Cliente | Servidor | Centralizado |
| Detección stock bajo | Manual | Automático | Incluido |
BFF Route: Products with Inventory (List)
Versión paginada que lista productos con su inventario agregado. Ideal para dashboards de administración y vistas de catálogo con stock.
Query Parameters
- Name
locationId- Type
- string
- Description
Filtrar por ubicación específica.
- Name
limit- Type
- number
- Description
Número de productos por página (default: 20).
- Name
offset- Type
- number
- Description
Offset para paginación.
- Name
category- Type
- string
- Description
Filtrar por categoría.
- Name
lowStock- Type
- boolean
- Description
Si es
true, solo retorna productos con stock bajo.
- Name
outOfStock- Type
- boolean
- Description
Si es
true, solo retorna productos sin stock.
Request
curl "https://api.cannahub.tech/api/products/with-inventory?limit=20&lowStock=true" \
-H "Authorization: Bearer {token}"
Response
{
"products": [
{
"id": "prod_01HQ8BLUE_DREAM",
"name": "Blue Dream Indoor",
"imageUrl": "https://storage.cannahub.tech/...",
"totalInventoryGrams": 60,
"totalInventoryUnits": 28,
"variants": [
{
"id": "variant_01HQ_BD_1G",
"name": "1g",
"quantity": 20,
"available": 18
}
],
"isLowStock": false,
"isOutOfStock": false
}
],
"count": 45,
"limit": 20,
"offset": 0,
"summary": {
"totalProducts": 45,
"lowStockCount": 5,
"outOfStockCount": 2,
"totalInventoryGrams": 15000
}
}
BFF Route: Product Inventory por Variante
Esta ruta BFF obtiene un producto con datos de inventario embebidos directamente en cada variante. A diferencia de /api/products/:id/with-inventory (que retorna el inventario como un mapa separado), este endpoint enriquece cada variante con su array locationLevels, conteniendo cantidades stocked, reserved y available por ubicación.
Diferencia con /with-inventory
| Aspecto | /with-inventory | /inventory |
|---|---|---|
| Estructura | Inventario en mapa separado (inventory[variantId]) | Inventario embebido en cada variante (variant.locationLevels[]) |
| Campos | quantity, available, reserved, totalGrams, byLocation[] | stockedQuantity, reservedQuantity, availableQuantity por ubicación |
| Uso principal | Dashboards de inventario, reportes | Formularios de edición, popovers de stock |
| Stock bajo | Incluye lowStockVariants | No incluido |
Request
- Name
id- Type
- string
- Description
El ID del producto.
Response
Si el producto no tiene manageInventory habilitado, retorna solo el producto sin datos de inventario.
- Name
product- Type
- Product
- Description
El producto completo con variantes enriquecidas.
- Name
variants- Type
- array
- Description
Cada variante incluye un campo adicional
locationLevels.- Name
locationLevels- Type
- VariantLocationLevel[]
- Description
Niveles de inventario por ubicación.
- Name
locationId- Type
- string
- Description
ID de la ubicación.
- Name
locationName- Type
- string
- Description
Nombre de la ubicación.
- Name
stockedQuantity- Type
- number
- Description
Cantidad total almacenada.
- Name
reservedQuantity- Type
- number
- Description
Cantidad reservada en pedidos pendientes.
- Name
availableQuantity- Type
- number
- Description
Cantidad disponible para venta (stocked - reserved).
- Name
inventoryQuantity- Type
- number
- Description
Total stocked recalculado desde los location levels.
- Name
inventoryQuantity- Type
- number
- Description
Total en gramos recalculado (suma de
inventoryQuantity × weightpor variante).
Request
curl https://api.cannahub.tech/api/products/prod_01HQ8BLUE_DREAM/inventory \
-H "Authorization: Bearer {token}"
Response
{
"product": {
"id": "prod_01HQ8BLUE_DREAM",
"name": "Blue Dream Indoor",
"description": "Cepa híbrida sativa-dominante...",
"imageUrl": "https://storage.cannahub.tech/products/blue-dream.jpg",
"price": 330000,
"currency": "ARS",
"manageInventory": true,
"inventoryQuantity": 60,
"variants": [
{
"id": "variant_01HQ_BD_1G",
"name": "Blue Dream 1g",
"weight": 1,
"price": 350000,
"inventoryQuantity": 20,
"locationLevels": [
{
"locationId": "sloc_dispensary",
"locationName": "Dispensario Principal",
"stockedQuantity": 15,
"reservedQuantity": 2,
"availableQuantity": 13
},
{
"locationId": "sloc_warehouse",
"locationName": "Depósito",
"stockedQuantity": 5,
"reservedQuantity": 0,
"availableQuantity": 5
}
]
},
{
"id": "variant_01HQ_BD_5G",
"name": "Blue Dream 5g",
"weight": 5,
"price": 1650000,
"inventoryQuantity": 8,
"locationLevels": [
{
"locationId": "sloc_dispensary",
"locationName": "Dispensario Principal",
"stockedQuantity": 8,
"reservedQuantity": 0,
"availableQuantity": 8
}
]
}
],
"strain": {
"name": "Blue Dream",
"type": "hybrid",
"cannabinoids": { "thc": 20, "cbd": 1.5 }
},
"isFlower": true
}
}
Response (sin manageInventory)
{
"product": {
"id": "prod_01HQ8_ACCESSORY",
"name": "Grinder Metálico",
"manageInventory": false,
"variants": [
{
"id": "variant_grinder_1",
"name": "Grinder 50mm"
}
]
}
}
Update a product variant
This endpoint allows you to update an existing product variant in your application.
Medusa Reference
Optional attributes
- Name
name- Type
- string
- Description
The name of the variant.
- Name
inventoryQuantity- Type
- number
- Description
The inventory quantity of the variant.
- Name
weight- Type
- number
- Description
The weight of the variant.
Request
curl -X PUT https://yourapp.com/api/products/:productId/variants/:variantId \
-H "Authorization: Bearer {token}" \
-d '{
"name": "Updated Variant",
"inventoryQuantity": 100,
"weight": 10
}'
Response
{
"id": "variantId",
"name": "Updated Variant",
"inventoryQuantity": 100,
"weight": 10,
"price": 100,
"discountPrice": 80,
"currencyCode": "USD",
"priceListId": "priceListId",
"priceListName": "Price List",
"prices": [
{
"id": "priceId",
"price": 100,
"priceList": {
"id": "priceListId",
"name": "Price List"
},
"currency": "USD",
"createdAt": "2023-05-13T12:00:00Z"
},
{
"id": "priceId",
"price": 80,
"priceList": {
"id": "priceListId",
"name": "Price List"
},
"currency": "USD",
"createdAt": "2023-05-14T12:00:00Z"
}
]
}