Products
The Products API manages the catalog used by orders, storefronts, inventory, WhatsApp, and voice agents. It supports simple SKUs, variants, custom option axes, composite products, modifier groups, categories, units, store assignment, and store-level stock.
All endpoints use /v1/products and an API key.
Endpoint summary
Send Idempotency-Key on product, category, and unit creation.
Product types
GET /v1/products/typesThe response is machine-readable:
{
"message": "Product types retrieved",
"data": [
{ "value": "simple", "isComposite": false },
{ "value": "combo", "isComposite": true },
{ "value": "box", "isComposite": true },
{ "value": "platter", "isComposite": true }
]
}Composite products use components instead of independent stock. Availability is derived
from the stock of each child variant.
Categories
Categories are business-scoped and may form a parent-child hierarchy. Assign a category
to a product with category_id during create or update.
List categories
GET /v1/products/categories{
"message": "Product categories retrieved",
"data": [
{
"id": "9666e1c2-7d11-430e-8254-0c542019cae5",
"name": "Meals",
"slug": "meals",
"parentId": null,
"sortOrder": 10,
"createdAt": "2026-07-14T08:00:00.000Z",
"updatedAt": "2026-07-14T08:00:00.000Z"
}
]
}Create a category
POST /v1/products/categories
Idempotency-Key: fdb2ce70-b399-4b16-99b0-f45c1015c613{
"name": "Rice dishes",
"parent_id": "9666e1c2-7d11-430e-8254-0c542019cae5",
"sort_order": 20
}Update or delete a category
PATCH /v1/products/categories/:id
DELETE /v1/products/categories/:idPATCH accepts any create field. Send parent_id: null to move a category to the root.
Category IDs are business-scoped.
Units
Products store a stable unit code, such as KG, PLATE, or BOTTLE. The unit catalog
maps each code to a display label. Assign a unit to a product by passing its code in the
product’s unit field.
List units
GET /v1/products/unitsThe first read seeds Cloove’s default units for the business.
{
"message": "Product units retrieved",
"data": [
{
"id": "a4245845-d858-4bd1-8622-699673c37410",
"code": "KG",
"label": "Kilogram (kg)",
"sortOrder": 10,
"isSystem": true,
"createdAt": "2026-07-14T08:00:00.000Z",
"updatedAt": "2026-07-14T08:00:00.000Z"
}
]
}Create a unit
POST /v1/products/units
Idempotency-Key: db286c27-6a47-48cb-afef-15b39b80e24d{ "code": "PLATE", "label": "Plate", "sort_order": 30 }Codes are normalized to uppercase with underscores. fl oz becomes FL_OZ.
Update or delete a unit
PATCH /v1/products/units/:id
DELETE /v1/products/units/:idPATCH accepts label, code, and sort_order. Products keep the code stored on them,
so deleting a unit does not remove the unit value from existing products.
List products
GET /v1/productsProducts are returned newest first with categories, variants, variant inventories, assigned stores, components, and modifier groups.
{
"message": "Products retrieved",
"data": [
{
"id": "21f0daa6-ec20-442f-9f68-82bfb6bb93fc",
"name": "Classic T-shirt",
"description": "Heavyweight cotton",
"basePrice": 12000,
"type": "simple",
"categoryId": "9666e1c2-7d11-430e-8254-0c542019cae5",
"unit": "PIECE",
"isActive": true,
"isExtraOnly": false,
"productOptions": [
{ "name": "Color", "position": 1, "values": ["Black", "White"] },
{ "name": "Size", "position": 2, "values": ["M", "L"] }
],
"variants": [
{
"id": "f6c66508-ae84-4522-bdba-8c268b786e91",
"name": "Black / M",
"sku": "TS-BLK-M",
"price": 12000,
"optionValues": [
{ "name": "Color", "value": "Black" },
{ "name": "Size", "value": "M" }
],
"inventories": [
{
"storeId": "dd757839-80d4-4f14-8ded-923a66df14c5",
"stockQuantity": 20
}
]
}
]
}
],
"meta": {
"total": 1,
"page": 1,
"perPage": 50,
"totalPages": 1,
"hasMore": false
}
}Get a product
GET /v1/products/:idReturns the full product graph, including active variants, product options, category,
stores, store-level inventories, components, and modifier groups. A missing or foreign
business product returns 404 not_found.
Create a simple product
POST /v1/products
Idempotency-Key: 1a72c58b-18ce-4825-9d89-65ef5f119fdcCore fields
Use store_inventory when the same initial quantity should not be applied everywhere:
[
{
"store_id": "dd757839-80d4-4f14-8ded-923a66df14c5",
"stock_quantity": 30
}
]Options and variants
product_options declares up to three variant axes. Every variant uses option_values to
select one value from each applicable axis.
{
"name": "Classic T-shirt",
"description": "Heavyweight cotton",
"price": 12000,
"category_id": "9666e1c2-7d11-430e-8254-0c542019cae5",
"unit": "PIECE",
"store_ids": ["dd757839-80d4-4f14-8ded-923a66df14c5"],
"product_options": [
{ "name": "Color", "position": 1, "values": ["Black", "White"] },
{ "name": "Size", "position": 2, "values": ["M", "L"] }
],
"variants": [
{
"name": "Black / M",
"sku": "TS-BLK-M",
"price": 12000,
"option_values": [
{ "name": "Color", "value": "Black" },
{ "name": "Size", "value": "M" }
],
"store_inventory": [
{
"store_id": "dd757839-80d4-4f14-8ded-923a66df14c5",
"stock_quantity": 20
}
]
},
{
"name": "White / L",
"sku": "TS-WHT-L",
"option_values": [
{ "name": "Color", "value": "White" },
{ "name": "Size", "value": "L" }
],
"stock_quantity": 12
}
]
}The response is 201 Created. Creating a product with an existing business-scoped name
updates that product and still returns 201; use product IDs for subsequent updates.
Create a composite product
Set type to combo, box, or platter and supply components. Each component points
to an existing child product and variant.
{
"name": "Lunch Combo",
"type": "combo",
"price": 7500,
"components": [
{
"child_product_id": "9739036f-9ffc-4906-95d3-f792f6f07a38",
"child_variant_id": "892b27ac-ebf2-499e-91ca-1cd823b3d1bb",
"quantity": 1,
"sort_order": 1
},
{
"child_product_id": "87de3036-d3d5-4ec8-a41e-b245d6bd7503",
"child_variant_id": "66129e74-fbf0-4afe-a2ed-cc9f7e8a6eae",
"quantity": 1,
"sort_order": 2
}
]
}Composite products keep one default variant. Their sellable quantity is derived from child inventory, and selling one decrements each component variant.
Modifier groups and extras
A simple product may define modifier_groups such as toppings, sides, or add-ons. Modifier
options reference child product variants. Create those child products with
is_extra_only: true when they should not be sold by themselves.
{
"name": "Burger",
"price": 5000,
"modifier_groups": [
{
"name": "Extras",
"is_required": false,
"max_select": 2,
"options": [
{
"child_product_id": "95dfa47e-3896-4971-94a1-af91db9a65d2",
"child_variant_id": "483a3ef1-13b2-4eec-a165-b209e5490dee",
"quantity": 1,
"price_override": 700,
"name": "Extra cheese",
"is_active": true
}
]
}
]
}Update a product
PATCH /v1/products/:idAll create fields are optional on update. price updates basePrice in the response.
Use category_id and unit to assign a category or unit. Send null to clear either
assignment.
When variants is present, it represents the desired final variant set:
- Include
idto update an existing variant. - Omit
idto create a variant. - Existing variants omitted from the array are soft-deleted.
- Omit the entire
variantsfield when variants should remain unchanged.
The same replacement rule applies to components and modifier_groups. Treat each array
as the resource’s intended configuration, not as an append-only operation.
{
"category_id": "9666e1c2-7d11-430e-8254-0c542019cae5",
"unit": "PIECE",
"product_options": [
{ "name": "Size", "position": 1, "values": ["M", "L", "XL"] }
],
"variants": [
{
"id": "f6c66508-ae84-4522-bdba-8c268b786e91",
"name": "XL",
"sku": "TS-XL",
"price": 13500,
"option_values": [{ "name": "Size", "value": "XL" }]
}
]
}Delete a product
DELETE /v1/products/:idDeletion soft-deletes the product and its variants. A product used as a component by a
combo, box, or platter returns 409 product_in_use; remove it from those composite products
before retrying.
Product and variant IDs are stable integration keys. Names, SKUs, category slugs, and unit labels may change. Store IDs from API responses and use them for updates and order lines.