Build an ordering flow
This guide walks through a complete takeaway checkout on your own site. The examples call the API from your server with the API key. All endpoints in this guide also accept calls from the browser on a verified domain; see Authentication.
Build the cart
Each cart line is a product, an optional variant, optional add-ons and a quantity. Calculate the price you display like this; the server does the same calculation when the order is created.
- Unit price: the chosen variant’s
priceif the product has variants, otherwisebasePrice. - Add-ons: the groups that apply to a product are
product.addonGroups, the category’saddonGroupsand the sharedrestaurant.addonGroups. The customer must pick betweenminSelectandmaxSelectin each group. An add-on coststierPrices[variant.addonTierKey]if that exists, otherwiseprice. - Line total:
(unit price + add-ons) × quantity. - Combo deals (
isCombo: true): the price isbasePriceplus add-ons. The customer fills each ofcomboSlotswithminQty–maxQtyproducts fromallowedProductIds; send them ascomboSelections. - Pickup or delivery only: hide or disable products whose
allowedFulfillmentdoes not match the customer’s choice.nullmeans both.
Find the delivery address
For delivery, let the customer search for the address and send what you get back with the order:
Send lat/lng as deliveryLat/deliveryLng and the postal code as deliveryPostalCode. Radius-based delivery zones need the coordinates; postal-code zones need the postal code. The zone decides the delivery fee and minimum order; you find the zones in restaurant.deliveryZones in the menu.
Show discounts
An order gets either the coupon code the customer typed or the best automatic promotion, never both.
- No code: call
POST /coupons/preview-autowith the cart to show the automatic promotion. - Code typed: call
POST /coupons/validate, and send the code ascouponCodewhen you create the order.
Both are for display; the server calculates the discount again when the order is created. Details in Coupons and promotions.
Create the order
Send the cart to POST /orders. Three things make this step safe:
Idempotency-Key: create one key per checkout attempt and reuse it when you retry after a timeout or network error. A replay returns the same order instead of a second one.expectedTotal: the total you showed the customer. If the server’s total is higher, you get409 PRICE_CHANGEDand no order is created.successUrlwith{TOKEN}: LettBestilt replaces{TOKEN}with the order’spublicTokenwhen the customer returns from payment.
The response has totals (subtotal, deliveryFee, discount, total, currency). They are the amounts the customer pays.
Send the customer to payment
- Online (
paymentMethod: "STRIPE"): redirect the customer tostripeUrl, SagaPay’s hosted payment page. The customer chooses card, Vipps or another method there, and comes back to yoursuccessUrl. The namestripeUrlis historical. - Cash (
paymentMethod: "CASH"): there is nostripeUrl. The order is already confirmed and on its way to the kitchen, so show your confirmation page right away.
More in Payments.
Show the order and its status
Your confirmation page gets the publicToken from the URL. Fetch the order with it; no API key is needed:
Do not treat the redirect to successUrl as proof of payment. Check order.status instead: while an online order is AWAITING_PAYMENT, the server asks the payment provider for the latest status on every call, so poll every few seconds until it changes. Then poll every 15–30 seconds to show progress: PAID → CONFIRMED → PREPARING → READY_FOR_PICKUP or OUT_FOR_DELIVERY → COMPLETED. Stop polling at COMPLETED, CANCELLED or FAILED.
If pickupTimeRevisedAt is set, the restaurant has moved the time. Tell the customer instead of changing the time silently.
LettBestilt’s emails to the customer link to /order/{publicToken} on the restaurant’s website domain. If that domain is your site, serve the order page at exactly that path.
Listen for webhooks
For your own systems (a back office, a CRM, notifications), subscribe to ORDER_CREATED, ORDER_PAID, ORDER_STATUS_CHANGED and ORDER_CANCELLED instead of polling. See Webhooks.
Handle errors
The full list is in Errors, rate limits and retries.

