Shopping
Cart & checkout
The one-store cart, how offers set the price, and every reason checkout can refuse an order.
A shopper has one cart, and a cart holds products from one store. Checkout turns the whole cart into one order at that store.
One store per cart#
POST cart/add takes product_id, store_id and quantity.
Adding a product from a different store than the one already in the cart returns 409:
{
"status": "conflict",
"message": "Your cart has items from another store…",
"current_store": { "ar": "…", "en": "Al Noor Market" },
"new_store": { "ar": "…", "en": "Fresh Corner" }
}
Ask the user whether to start a new cart. If they agree, call POST cart/clear and add the product again.
Other things to know:
- Adding a product that is already in the cart adds to its quantity rather than replacing it. To set an exact quantity, use
PUT cart/update/{cartItem}. - Every cart call returns the whole cart, the same shape as
GET cart, so the app can redraw from the response. - Placing an order empties the cart.
Before signing in#
The cart works without an account. While nobody is signed in, the app sends no Authorization header and identifies the cart with a UUID it generated itself:
X-Cart-Token: 6f1c2a9e-3b7d-4c11-9a52-0d8e4f7b6a21
GET cartwith an unknown token, or none, returns an empty cart. The cart is created by the firstPOST cart/add, which answers422(errors.cart_token) when the header is missing or is not a UUID.- Editing a line that belongs to another cart answers
403. - Sending the header on register or login moves the cart onto the account. If the account already holds a cart for the same store, quantities are added together; if it holds one for another store, the cart the visitor just filled replaces it. An empty anonymous cart changes nothing.
- Once signed in, the header is ignored: the bearer token alone selects the cart.
See Shopping before signing in for the whole flow.
Offers and prices#
Each cart line stores a unit price that already includes any offer. The cart total is the sum of price × quantity.
A product gets at most one offer, from its store: either an offer on the product itself or an offer on its category. Three kinds exist (see Merchant offers):
| Offer | Effect on the unit price |
|---|---|
| Percentage | price − price × value / 100 |
| Fixed | price − value, never below 0 |
| Buy X get Y | Free units spread across the line, so price is the average unit price for that quantity |
Because "buy X get Y" depends on the quantity, the unit price of a line can change when its quantity changes. Always show the price and subtotal the server returns; never compute them on the device.
An offer stops applying when it expires, is switched off, or reaches its usage limit. Each order counts as one use of an offer.
Checkout#
POST orders/checkout places the order:
{ "address_id": 12, "notes": "Ring the bell", "delivery_time": "18:00-19:00" }
All fields are optional. address_id must be one of the customer's own addresses. Payment is always by card, so the response also opens the first payment attempt; see Payments.
Checkout needs a customer token (401 without one): a visitor must register or log in first, which carries their cart over.
When checkout says no#
Checkout re-checks the whole cart against the live store inside one transaction. When something no longer holds, it answers 422 with a machine-readable code:
code |
Meaning | What the app should do |
|---|---|---|
cart_empty |
Nothing to order | Go back to the store |
store_unavailable |
The store was deactivated | Clear the cart, pick another store |
product_unavailable |
items[] are no longer sold here |
Remove those lines |
prices_changed |
items[] have a new price (old_price, new_price). The cart is already updated |
Show the changes, let the user confirm, call checkout again |
below_min_order_price |
current_total is under the store's min_order_price |
Ask for more items |
insufficient_stock |
items[] with requested and available |
Lower the quantities |
{
"status": "error",
"code": "insufficient_stock",
"message": "…",
"items": [{ "product_id": 88, "name": { "ar": "…", "en": "Milk 1L" }, "requested": 4, "available": 2 }]
}
Switch on code, not on message: the message is translated.
Stock is only reserved when the order is placed, so two customers can hold the last unit in their carts; the second to check out gets insufficient_stock.