Browsing: retail or trade
GET /products requires a priceType query parameter — exactly one of retail or trade.
trade is only available to trade accounts and staff; anyone else gets 403. Omitting
priceType is a 400.
Home page in one call
GET /recommendations returns every product rail on the storefront home page —
justForYou, everydayItems, lowPrices, bestSellers, newArrivals, flashSales and
recentlyViewed — instead of one call per rail. Each rail has exactly the shape its own
/recommendations/* endpoint returns, with the same pricing visibility for the caller.
limit— products per rail (1–100, default 10).include— comma-separated subset of rails, e.g.include=bestSellers,flashSales.recentlyViewed(and the personal part ofjustForYou) need a signed-in caller; for guestsrecentlyViewedisnull.- A rail that fails comes back
nullwith its message undererrors; the others still load.
Several products in one call
POST /products/lookup with { "ids": [ … ] } (up to 100) returns each product exactly as
GET /products/:id would for the same caller — trade pricing included only if the caller may see
it. Ids that don’t exist, are deleted, or are hidden from the caller (trade-only products for a
retail caller or guest) are simply left out rather than failing the call.
Product shape
Category and brand are embedded objects,null when unset:
priceRangeis the lowest–highest retail price across the sizes (“from £5.99”);tradePriceRangeis the same for trade prices and isnullunless the caller can see wholesale pricing.- Customers see only active sizes they can buy: trade-only sizes are hidden from retail
callers, and exact stock is replaced by
availableandstockStatus(in_stock,low_stock,out_of_stock). - Browse price filters and price sorts use each product’s lowest size price for the requested
priceType;?packSize=5kgmatches size labels. - For one release the product still carries
retailPrice,tradePrice,sku,weightand the sale fields from its default size, andstockLevelas the total across its sizes.
POST /cart/items takes { productId, sizeId, quantity }; sizeId may be
left out only when the product has exactly one size. Each size is its own cart line. Cart and
order lines include sizeId, variantName and sizeLabel (size holds the same label for
existing clients), and order lines also record the size’s sku. The wishlist saves the product,
not a size.
Customer-facing product responses don’t include source; admin responses do.
Deleted products
Deleting a product hides it everywhere a customer or admin browses — storefront, search, recommendations, the admin product list and admin product counts — andGET /products/:id
returns 404. Existing orders, carts and wishlists keep their own copy of the product’s name,
image and price, so order history is unaffected.
Check whether products can still be bought with:
available is false for a product that is deleted, deactivated, or unknown.
A deleted product keeps its SKU and slug reserved, so creating a product with either returns
409 with the deleted product’s productId. Bring it back instead: admins find deleted
products with GET /admin/products?status=deleted and restore them with
POST /admin/products/bulk-restore ({ "ids": [ … ] }). A restored product comes back
inactive; activate it to put it back on sale. Leave slug out when creating a product and a
unique one is generated from the name (rice-5kg-2 if rice-5kg is taken).
Cart and wishlist availability
Every cart and wishlist item has anavailable flag. Unavailable cart lines stay visible so
the customer can see what happened, but they’re excluded from the cart subtotal. Prompt the
customer to remove them — checkout refuses the cart until they do.
Checkout re-validation
The cart stores a snapshot of each product taken when it was added.POST /orders/checkout
re-checks every line against the live product, at the caller’s own price (trade or retail),
before creating the order:
A
409 is safe to retry immediately — the next attempt goes through at the updated prices.