Products
Product responses embed product_variants and product_pricing_tiers. The pricing-tier key is an alias over the pricing_tiers table — the JSON field name partners consume is and stays product_pricing_tiers.
/v1/productsList products with variants, pricing tiers and a nested `businesses` summary. Filters: `business_id`, `category_id`, `status`, `visibility` (internal scope only), `limit`, `offset`.
{
"data": [
{
"id": "7ab2…",
"name": "Ergonomic office chair",
"slug": "ergonomic-office-chair",
"status": "active",
"visibility": "public",
"base_price": 1450000,
"currency": "IDR",
"business_id": "c3a1…",
"businesses": { "id": "c3a1…", "name": "Bali Teak Works", "slug": "bali-teak-works" },
"product_variants": [],
"product_pricing_tiers": [
{ "id": "aa11…", "min_quantity": 50, "price": 1320000, "currency": "IDR" }
]
}
],
"total": 1842,
"limit": 20,
"offset": 0
}Without products.internal.read, results are filtered to status = active AND visibility = public, and ?visibility= / ?status= cannot widen that.
/v1/products/:idSingle product with variants, pricing tiers and business summary. 404 when it is not active + public and the key lacks `products.internal.read`.
/v1/productsCreate a product. Slug is generated from the name. Fires `product.created`.
{
"business_id": "c3a1…",
"name": "Ergonomic office chair",
"description": "Mesh back, adjustable lumbar.",
"category_id": "8f2b…",
"base_price": 1450000,
"currency": "IDR",
"unit_of_measure": "piece",
"status": "active",
"visibility": "public"
}Always send currency alongside base_price. If omitted the record is stored as IDR, which silently mis-prices non-IDR catalogs.
/v1/products/:idPartial update. Fires `product.updated` with the changed field names.
/v1/products/:id/price-historyHistorical price points for a product, newest first. Applies the same visibility guard as the product detail route: a non-public product returns 404 unless the key has `products.internal.read`.