Checkout
The server prices the cart; your site sends product_id, variant_id and qty, nothing more. Every amount is a whole-rupiah integer: 79000 is Rp 79.000.
1. Shipping
Read commerce.shipping.mode from GET /storefront/config first.
flat: skip the shipping calls. The buyer picks one ofcommerce.couriers; send itscodeascourier_codein the draft. A courier the platform knows carrieslogo_url, andoptions[].courier_logo_urldoes the same for live rates. Draw the logo when it is there and the name when it is not.live: quote the cart for the buyer's address.
| Call | Does | Returns |
|---|---|---|
POST /shipping/destinations | searches places (q, 3+ characters) | results[].ref |
POST /shipping/destinations/resolve | turns the picked ref into a quotable one | destination.ref |
POST /shipping/rates | quotes the cart for destination_ref | options[].quote_id, send it as shipping_quote_id |
Under flat, do not call these routes; they usually answer 404. A resolve can answer 409 destination_unserved with details.alternatives to offer instead. A quote lives six hours. A rate fetch for a new destination or cart weight costs the shop one of its monthly rate checks (Limits): fetch rates when the destination or the cart changes, not on every render.
2. Order
| Call | Does | Returns |
|---|---|---|
POST /checkout/draft | turns the cart into a priced draft | draft_id, total, voucher, otp_required |
POST /checkout/otp/send | sends a code to the buyer's contact | channel, masked, expires_in_seconds |
POST /checkout/otp/verify | checks the code and places the order | order, payment, track_token, track_url |
GET /orders/track/{token} | status and payment instructions | order.timeline, payment |
The API reference has the fields of every call. Codes go out by email today; read channel from the send answer rather than assuming one.
Rules the server enforces
- You receive
track_tokenonce. Keep it, in the browser too: a buyer returning from a payment page lands on/lacakwithout it (Limits). - From a browser on your own domain,
otp_requiredis alwaystrue: the cookie that skips the code cannot travel cross-origin. Send a code before every verify. - Send a code when the buyer asks for one, and show a refused code as refused; the server does not say why.
- A draft lives 30 minutes (
expires_at). After that, send and verify answer409 draft_expired: build the draft again. The draft answers409 quote_expiredwhen theshipping_quote_idis older than six hours or the cart weight changed: fetch rates again, then build the draft. - A voucher that did not apply still produces a draft. Read
voucher.appliedandvoucher.reason(for examplelimit_reached,buyer_limit_reached,requirements_not_met) and showvoucher.message. A voucher used up after the draft answers409 voucher_limiton send or verify: rebuild the draft without the code. A cross-origin site learns a per-buyer limit only at verify. - A shop on holiday stays browsable, and
/storefront/configdoes not say it is closed. The draft, send and verify answer409 store_closed;messagenames the reopening day when the merchant set one. Tracking and payment keep working for orders already placed. - Render
order.timelineandorder.status_labelas given; the merchant, a gateway or a worker can move the status at any time. - From version
2026-10-20, a product can be sold by weight, volume or length. It carriesunit(pcs,kg,literormeter),qty_stepandqty_min. Everyqtyandstockcounts the unit's small unit: grams forkg, millilitres forliter, centimetres formeter. The price stays per sale unit. A line of 1,25 kg tomatoes atprice: 12500sendsqty: 1250; the line costs15625. Send aqtythat is at leastqty_minand a multiple ofqty_step, at most 99 sale units; anything else answers422 qty_step. Show1250as "1,25 kg", never as a count. Lines (lines[].unit) say the unit they were sold by. A missingunitispcs, step 1. - A site pinned to an earlier version sees no unit fields, so its
qty: 1of a kg product would buy one gram. The draft refuses such a cart with422 unit_needs_update; pin2026-10-20to sell such products. - From version
2026-10-20, orders carry the shop's loyalty points:points_used, thepoints_discountthey gave, andpoints_earned. The storefront API cannot spend points; show them as given. - From version
2026-10-20(Versions),order.fulfilmentsays how the order reaches the buyer:ship(a courier),pickup(collected at the shop),dine_in(served at a table) ornow(handed over at the counter).pickupmeans only the flat courier whosecodeispickup; an "Ambil di toko" option under any other code readsship. Earlier versions omit the field. - Branch on
payment.kind, notpayment.method. The server picks the gateway behind a method, so the same method can come back as a different kind. See Payments. POST /orders/lookup/otp/sendand/verifylist a buyer's 20 most recent orders after a code. The list carries notrack_token, so it cannot open an order: the buyer needs the link from their confirmation email.
Every refusal has its own code; Errors lists what to do with each.