Skip to Content
Products

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

MethodPathScopeSuccess
GET/v1/productsproducts:read200
GET/v1/products/typesproducts:read200
GET/v1/products/categoriesproducts:read200
POST/v1/products/categoriesproducts:write201
PATCH/v1/products/categories/:idproducts:write200
DELETE/v1/products/categories/:idproducts:write200
GET/v1/products/unitsproducts:read200
POST/v1/products/unitsproducts:write201
PATCH/v1/products/units/:idproducts:write200
DELETE/v1/products/units/:idproducts:write200
GET/v1/products/:idproducts:read200
POST/v1/productsproducts:write201
PATCH/v1/products/:idproducts:write200
DELETE/v1/products/:idproducts:write200

Send Idempotency-Key on product, category, and unit creation.

Product types

GET /v1/products/types
ValueCompositeUse
simpleNoA regular product with one or more variants.
comboYesFixed-price bundle made from child product variants.
boxYesBox or hamper made from child product variants.
platterYesPlatter made from child product variants.

The 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
Body fieldTypeRequiredDescription
namestringYesDisplay name, 1 to 255 characters.
slugstringNoUnique business-scoped slug. Generated from name when omitted.
parent_iduuid | nullNoParent category.
sort_ordernumberNoAscending display order.
{ "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/:id

PATCH 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/units

The 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
Body fieldTypeRequiredDescription
labelstringYesHuman-readable label.
codestringNoStable code, up to 40 characters. Derived from label when omitted.
sort_ordernumberNoAscending display order.
{ "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/:id

PATCH 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/products

Products are returned newest first with categories, variants, variant inventories, assigned stores, components, and modifier groups.

QueryTypeRequiredDescription
searchstringNoSearches product name, description, variant name, and variant SKU.
category_iduuidNoOne category.
category_idsuuid listNoComma-separated category IDs.
store_iduuidNoProducts assigned to one store.
store_idsuuid listNoProducts assigned to any listed store.
typeenum listNoComma-separated simple, combo, box, or platter.
statusenum listNoComma-separated in_stock or low_stock.
is_extra_onlybooleanNoFilter modifier-only products.
pageintegerNoOne-based page; default 1.
limitintegerNoPage size; default 50.
{ "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/:id

Returns 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-65ef5f119fdc

Core fields

Body fieldTypeRequiredDescription
namestringYesProduct name, 2 to 255 characters.
pricenumberYesBase selling price in the business currency.
descriptionstringNoDescription, up to 2,000 characters.
typeenumNosimple, combo, box, or platter; default simple.
skustringNoSKU used when a separate variant list is not supplied.
barcodestringNoBarcode.
category_iduuidNoExisting business category.
cost_pricenumberNoUnit cost for margin reporting.
unitstringNoUnit code from GET /v1/products/units.
is_activebooleanNoWhether the product may be sold.
catalog_sync_enabledbooleanNoWhether catalog changes trigger WhatsApp sync.
is_extra_onlybooleanNoMakes this SKU available only as a modifier or extra.
reorder_levelnumberNoLow-stock threshold.
image_urlsstring arrayNoPublic HTTPS image URLs.
store_idsuuid arrayNoStores that carry the product.
quantitynumberNoInitial quantity for the default variant/store.
store_inventoryobject arrayNoInitial product-level stock by store.

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.

Option fieldTypeRequiredDescription
namestringYesAxis name, such as Color or Size.
positionintegerNoAxis order from 1 to 3.
valuesstring arrayNoAllowed values for the axis. Values are also merged from variants.
Variant fieldTypeRequiredDescription
iduuidUpdate onlyExisting variant ID. Omit it to create a variant.
namestring | nullNoDisplay label. Generated from option values when omitted.
skustring | nullNoVariant SKU.
pricenumberNoVariant price; defaults to product price.
stock_quantitynumberNoInitial stock in the default store.
store_inventoryobject arrayNoStock per store.
attributesobject | nullNoAdditional variant attributes.
option_valuesobject arrayNo{ name, value } selections.
{ "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.

Component fieldTypeRequiredDescription
child_product_iduuidYesChild product.
child_variant_iduuidYesChild variant consumed when sold.
quantitynumberYesUnits consumed per composite sale.
sort_orderintegerNoDisplay order.
notesstringNoInternal component note.
{ "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.

Group fieldTypeRequiredDescription
namestringYesGroup label.
is_requiredbooleanNoWhether a choice is required.
max_selectintegerNoMaximum selected options.
sort_orderintegerNoDisplay order.
optionsobject arrayYesOne or more modifier options.
Modifier option fieldTypeRequiredDescription
child_product_iduuidYesExtra product.
child_variant_iduuidYesExtra variant.
quantitynumberNoUnits consumed; default 1.
price_overridenumberNoPrice charged instead of the child variant price.
namestringNoDisplay label override.
is_activebooleanNoWhether the option may be selected.
{ "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/:id

All 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 id to update an existing variant.
  • Omit id to create a variant.
  • Existing variants omitted from the array are soft-deleted.
  • Omit the entire variants field 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/:id

Deletion 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.

Last updated on