> ## Documentation Index
> Fetch the complete documentation index at: https://secdocs.asikoexpress.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Catalogue & Checkout

> Retail vs trade browsing, pack sizes as variants, deleted products, and how checkout re-validates the cart

## Browsing: retail or trade

`GET /products` requires a `priceType` query parameter — exactly one of `retail` or `trade`.

| `priceType` | Lists | Price filters, price sorts and the `priceRange` facet use |
| - | - | - |
| `retail` | products with a retail price | `retailPrice` |
| `trade` | products with a trade price | `tradePrice` |

`trade` is only available to trade accounts and staff; anyone else gets `403`. Omitting
`priceType` is a `400`.

```bash theme={null}
GET /v1/products?priceType=retail&categoryId=…&sortBy=PRICE_ASC
```

## 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 of `justForYou`) need a signed-in caller; for guests
  `recentlyViewed` is `null`.
* A rail that fails comes back `null` with its message under `errors`; the others still load.

```json theme={null}
{
  "justForYou": [ … ],
  "bestSellers": [ … ],
  "flashSales": null,
  "recentlyViewed": null,
  "errors": { "flashSales": "…" }
}
```

## 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:

```json theme={null}
{
  "category": { "id": "9f8b…", "name": "Rice & Grains" },
  "brand": { "id": "a1b2…", "name": "Mama Africa" }
}
```

**Variants and sizes.** A product has one or more variants (e.g. "Original", "Wholewheat"), and
each variant has one or more sizes. The size is what's sold: it has its own id, SKU, prices,
sale window and stock. Products without real choices have a single "Default" variant with one
size.

```json theme={null}
{
  "priceRange": { "min": 5.99, "max": 24.5 },
  "tradePriceRange": null,
  "defaultSizeId": "size-uuid-1",
  "available": true,
  "variants": [
    {
      "id": "var-uuid", "name": "Original", "images": [],
      "sizes": [
        { "id": "size-uuid-1", "label": "1kg", "sku": "GP-SEM-1KG", "weight": "1kg",
          "retailPrice": 5.99, "tradePrice": null, "salePrice": null, "isOnFlashSale": false,
          "available": true, "stockStatus": "in_stock" }
      ]
    }
  ]
}
```

* `priceRange` is the lowest–highest retail price across the sizes ("from £5.99");
  `tradePriceRange` is the same for trade prices and is `null` unless 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 `available` and `stockStatus`
  (`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=5kg` matches size labels.
* For one release the product still carries `retailPrice`, `tradePrice`, `sku`, `weight` and the
  sale fields from its default size, and `stockLevel` as the total across its sizes.

**Cart and orders.** `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 — and `GET /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:

```bash theme={null}
GET /v1/products/availability?ids=…&ids=…   # up to 100 ids
```

```json theme={null}
{ "data": [{ "id": "…", "available": true }, { "id": "…", "available": false }] }
```

`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 an `available` 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:

| Status | Meaning | What to do |
| - | - | - |
| `400` | One or more items are no longer available; the message names them | Remove them and check out again |
| `409` | A price changed since the item was added. The cart has already been updated to current prices; the message lists each change | Reload the cart, show the customer the new total, and let them check out again |
| `502` | Product service unavailable or timed out — checkout never guesses a price | Retry |

A `409` is safe to retry immediately — the next attempt goes through at the updated prices.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.