Build an ordering flow
This is the sequence we recommend for a customer-facing checkout. Each step is one endpoint; the notes say what to check before moving on.
-
Show stores that can serve the customer —
GET /stores?lat=&lng=Pass the customer’s coordinates and only stores that deliver there come back, nearest first, each with
status(open,busy,closed) andaccepting_orders.- Offer ordering only when
accepting_ordersistrue. busymeans the restaurant paused new orders temporarily — show it as such rather than as closed.- Re-fetch every few minutes; opening hours and pauses change.
- Offer ordering only when
-
Show the menu —
GET /stores/{store_id}/menuOne call returns the whole menu grouped by category, with prices, availability and modifier groups. Cache it for a few minutes. See Menus & modifiers for how to render required choices.
-
Quote the address —
GET /stores/{store_id}/delivery-quote?lat=&lng=Returns
deliverable, thefee, the store’smin_orderand its currentstatus. Show the fee before the customer commits. Ifdeliverableisfalse, do not let them continue. -
(Optional) Apply a promo code —
POST /promo-codes/validateReturns
validand thediscount_amountit would give. Validating does not redeem the code — you redeem it by sendingpromo_codewhen you create the order. -
Create the order —
POST /ordersAlways send an
Idempotency-Key(use your own order id). Send items byitem_id, not prices — GoOrder calculates the total and returns it inpricing.async function placeOrder(cart, customer, address) {const res = await fetch(`${BASE}/orders`, {method: 'POST',headers: {Authorization: `Bearer ${KEY}`,'Idempotency-Key': cart.id, // your own unique id'Content-Type': 'application/json',},body: JSON.stringify({store_id: cart.storeId,fulfilment: 'delivery',items: cart.lines.map((l) => ({item_id: l.itemId, quantity: l.qty, modifier_ids: l.modifierIds, notes: l.notes,})),customer, // { name, phone }delivery_address: address,payment_method: cart.pay, // 'cash' | 'card'reference: cart.id,}),});const body = await res.json();if (!res.ok) throw Object.assign(new Error(body.error.message), body.error);return body.data; // 201 new, 200 if replayed}def place_order(cart, customer, address):res = requests.post(f"{BASE}/orders",headers={"Authorization": f"Bearer {KEY}", "Idempotency-Key": cart["id"]},json={"store_id": cart["store_id"],"fulfilment": "delivery","items": [{"item_id": l["item_id"], "quantity": l["qty"], "modifier_ids": l["modifier_ids"]}for l in cart["lines"]],"customer": customer,"delivery_address": address,"payment_method": cart["pay"],"reference": cart["id"],},timeout=15,)body = res.json()if not res.ok:raise ApiError(**body["error"])return body["data"]Handle the business-rule errors by message to the customer, not as crashes — they are normal:
store_closed,out_of_zone,below_minimum_order,item_unavailable,address_incomplete. See Errors. -
Collect payment
payment_method: "cash"— nothing to do; the customer pays at the door.payment_method: "card"— callPOST /orders/{order_id}/paymentand send the customer to the returnedpayment_url. See Payments.
-
Follow the order — webhooks first, polling as a fallback
Subscribe to
order.status_changedandorder.payment_updated(Webhooks). Where you must poll,GET /orders/{order_id}/trackingno more often than every 10–15 seconds. -
Handle cancellation —
POST /orders/{order_id}/cancelWorks while the order is
placedorconfirmed. Afterwards it returns409 order_not_cancellable.
A minimal state machine for your side
Section titled “A minimal state machine for your side”| You see | You show the customer |
|---|---|
placed | “Waiting for the restaurant to confirm” |
confirmed | “Confirmed” |
preparing | “Being prepared” |
ready | “Ready — waiting for the rider” (delivery) / “Ready to collect” (pickup) |
out_for_delivery | “On the way” — show the rider from /tracking |
delivered / completed | “Delivered” / “Collected” |
cancelled | “Cancelled” and the cancel_reason |
partially_refunded / refunded | “Refunded” |