Skip to main content

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 of commerce.couriers; send its code as courier_code in the draft. A courier the platform knows carries logo_url, and options[].courier_logo_url does 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.
CallDoesReturns
POST /shipping/destinationssearches places (q, 3+ characters)results[].ref
POST /shipping/destinations/resolveturns the picked ref into a quotable onedestination.ref
POST /shipping/ratesquotes the cart for destination_refoptions[].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​

CallDoesReturns
POST /checkout/draftturns the cart into a priced draftdraft_id, total, voucher, otp_required
POST /checkout/otp/sendsends a code to the buyer's contactchannel, masked, expires_in_seconds
POST /checkout/otp/verifychecks the code and places the orderorder, payment, track_token, track_url
GET /orders/track/{token}status and payment instructionsorder.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_token once. Keep it, in the browser too: a buyer returning from a payment page lands on /lacak without it (Limits).
  • From a browser on your own domain, otp_required is always true: 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 answer 409 draft_expired: build the draft again. The draft answers 409 quote_expired when the shipping_quote_id is 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.applied and voucher.reason (for example limit_reached, buyer_limit_reached, requirements_not_met) and show voucher.message. A voucher used up after the draft answers 409 voucher_limit on 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/config does not say it is closed. The draft, send and verify answer 409 store_closed; message names the reopening day when the merchant set one. Tracking and payment keep working for orders already placed.
  • Render order.timeline and order.status_label as 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 carries unit (pcs, kg, liter or meter), qty_step and qty_min. Every qty and stock counts the unit's small unit: grams for kg, millilitres for liter, centimetres for meter. The price stays per sale unit. A line of 1,25 kg tomatoes at price: 12500 sends qty: 1250; the line costs 15625. Send a qty that is at least qty_min and a multiple of qty_step, at most 99 sale units; anything else answers 422 qty_step. Show 1250 as "1,25 kg", never as a count. Lines (lines[].unit) say the unit they were sold by. A missing unit is pcs, step 1.
  • A site pinned to an earlier version sees no unit fields, so its qty: 1 of a kg product would buy one gram. The draft refuses such a cart with 422 unit_needs_update; pin 2026-10-20 to sell such products.
  • From version 2026-10-20, orders carry the shop's loyalty points: points_used, the points_discount they gave, and points_earned. The storefront API cannot spend points; show them as given.
  • From version 2026-10-20 (Versions), order.fulfilment says how the order reaches the buyer: ship (a courier), pickup (collected at the shop), dine_in (served at a table) or now (handed over at the counter). pickup means only the flat courier whose code is pickup; an "Ambil di toko" option under any other code reads ship. Earlier versions omit the field.
  • Branch on payment.kind, not payment.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/send and /verify list a buyer's 20 most recent orders after a code. The list carries no track_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.