Skip to content

Using Catalog Data

The catalog is a simple per-project store of products and categories. Use it as a reference list for the dashboard and your own tooling. Promotion evaluation does not read the catalog. The engine sees only the fields you put on each cart item, so a promotion that targets “Outerwear” matches only items sent with category: "Outerwear" (or categories, tags, …).

POST /projects/{projectId}/products

{
"name": "Wool Jacket",
"description": "Medium, navy",
"price": 8900,
"categoryIds": ["cat_k3j2h1"],
"metadata": { "externalId": "shopify-81234", "sku": "JKT-M-NV" },
"status": "active"
}
FieldNotes
nameRequired, non-empty
priceRequired, integer ≥ 0. Use cents, as everywhere else in Storelayer
categoryIdsCategory ids (cat_…). Not checked for existence
metadataFree-form object. Replaced as a whole on update
statusactive (default) or inactive
  • Ids are generated (prod_…) and cannot be chosen. Store your own system’s id in metadata to keep a stable mapping.
  • PUT /projects/{projectId}/products/{productId} updates only the fields you send. A missing product returns 404 PRODUCT_NOT_FOUND.
  • GET /projects/{projectId}/products accepts limit (1–100, default 50), offset, status and categoryId. The categoryId filter is applied after paging, so a filtered page can hold fewer than limit results even when more exist. Keep paging until a page comes back empty.

POST /projects/{projectId}/products/bulk with { "products": [ ... ] } takes 1–1000 products per request, each with the create shape above. The response is { "success": [products...], "errors": [{ "index", "reason" }] }. Rows are inserted one at a time, so a bad row does not block the others. Check errors and resend only those indexes. Bulk import always creates new products: re-importing the same list creates duplicates.

POST /projects/{projectId}/categories with { "name": "Outerwear", "description": null }. Ids are generated (cat_…). Update with PUT /projects/{projectId}/categories/{categoryId}. Deleting a category fails while any product still lists it in categoryIds.

Since evaluation never looks up products, your backend builds each cart line from its own source of truth:

Cart item fieldFill fromWhy
idYour basket line idDiscounts are reported per line
productIdYour product id (or the catalog prod_… id; just be consistent)targetRules on $('item').productId compare exactly
unitPriceThe price you will charge, in centsNot taken from catalog price
category / categoriesThe names promotions use, e.g. "Outerwear"Compared case-insensitively. Catalog categoryIds are opaque ids, so map them to names, or write rules on the ids
tags, brand, collections, attributesProduct dataItem metadata is not visible to rules, so copy matchable values here

Pick one vocabulary per field. If rules say "Outerwear", every cart must send "Outerwear" (any case), never cat_k3j2h1.

  • Preview a cart line for a product that should match, one that should not, and one with modifiers. Check notApplied[].reason for the excluded one (computation_no_discount means the conditions passed but no item matched targetRules).
  • Before renaming a category, search promotions for rules that compare against the old name.