Modules
Restaurant Ordering
Pickup, delivery and dine-in. The fulfillment step, the live kitchen board and printed tickets.
Restaurant Ordering is Online Ordering plus the question every restaurant has to ask before payment: how is the customer getting this. That answer travels with the order to a live board in the kitchen and onto a printed ticket.
Build the cart on Online Ordering, and read it first. The quote, the amount, the five states an address can be in, whether the kitchen is open and every refusal live on that page. This one is the configuration underneath: what a venue offers, what a dish can be ordered with, and how its delivery area is drawn.
Read the settings, do not hardcode them
/api/public/{organizationId}/restaurantPublicWhich fulfillment types are on, the indicative timings, the delivery and pickup terms, and the tax configuration.
{
"enabled": true,
"fulfillment_types": ["pickup", "delivery"],
"default_prep_minutes": 20,
"delivery_prep_minutes": 40,
"delivery_area": { "from_fee": 2.5, "min_order_from": 15, "zone_count": 3 },
"pickup": {
"min_order_amount": 0,
"lead_minutes": 20,
"instructions": "Ring the bell at the side entrance.",
"address": { "street": "Dam", "house_number": "1",
"postal_code": "1012 JS", "city": "Amsterdam", "country": "NLD" }
},
"tax": {
"enabled": true,
"label": "BTW",
"prices_include_tax": true,
"rates": [
{ "id": "8f1c...", "name": "Laag (eten)", "rate": 9, "is_default": true },
{ "id": "b2d7...", "name": "Hoog", "rate": 21, "is_default": false }
]
}
}pickup.lead_minutes is the minimum notice your customer asks for a collection, in minutes. It is a request, not a rule: the checkout does not enforce it, so offer no collection time sooner than that yourself.
Three rules follow from this response, and all three matter more than they look:
- A type that is not in
fulfillment_typesmust not appear anywhere. Not greyed out, not disabled. Absent. - Exactly one type means there is nothing to choose. Select it silently and show no picker: a radio group with one option is a question with one answer.
enabled: falsemeans hide restaurant ordering altogether.
Your customer changes these whenever they like. A kitchen that stops delivering on a quiet Monday expects that to reach their website the same minute. A hardcoded list means someone picks delivery, types a full address, and is told at the payment step that it is unavailable. The checkout does refuse it, with 400 FULFILLMENT_TYPE_UNAVAILABLE, so nothing invalid gets through. But by then you have wasted their time.
The three types
| Type | Ask for | Rejected without |
|---|---|---|
pickup | An optional collection time. No address — anyone willing to drive to the restaurant is welcome, wherever they live. | Nothing, unless the basket is under the pickup minimum. |
delivery | A full address, ideally through autocomplete, checked against the delivery area. | address, with 400 ADDRESS_REQUIRED. |
dine_in | Not available yet — table ordering is being designed around a QR code per table. The setting cannot be switched on, so this type never appears in fulfillment_types. | — |
Options and extras
Dishes can carry option groups: a required "pick your pasta", extras with a surcharge, "without onion" — sometimes with a discount. Your customer configures them once per group and attaches them to categories or individual dishes; you render them and send the picks.
/api/public/{organizationId}/restaurant/modifiers?content_item_id={id}PublicThe groups that apply to one dish, include/exclude precedence already resolved. Omit the parameter to fetch every group at once for a menu prefetch.
{
"groups": [
{ "id": "…", "name": "Soort pasta", "selection_type": "single",
"min_select": 1, "max_select": 1, "allow_quantity": false,
"options": [
{ "id": "…", "name": "Penne", "kind": "add", "price_delta": 0,
"is_available": true, "is_default": true },
{ "id": "…", "name": "Tagliatelle", "kind": "add", "price_delta": 0.5,
"is_available": false, "is_default": false }
] }
]
}min_select >= 1 means the group is required — the checkout refuses an order that skips it. is_available: false means sold out: grey the option out rather than hiding it. price_delta can be negative; the server does all arithmetic, including a €0 floor per line. Send the picks on the cart line as ids and quantities, never prices:
{ "content_item_id": "…", "quantity": 2,
"options": [ { "option_id": "…", "quantity": 2 }, { "option_id": "…" } ] }| Code | What to do |
|---|---|
MODIFIER_REQUIRED / MODIFIER_TOO_FEW / MODIFIER_TOO_MANY / MODIFIER_SINGLE_CHOICE | A picking rule was broken. The message is customer-ready and names the group — show it next to that group. |
MODIFIER_QUANTITY_NOT_ALLOWED / MODIFIER_QUANTITY_OUT_OF_RANGE | The amount is wrong for that group. Show the message at the option. |
MODIFIER_OPTION_UNAVAILABLE | Sold out while the basket was open. Refetch the modifiers and re-render the group. |
MODIFIER_GROUP_NOT_APPLICABLE | Your cached menu is stale — the merchant moved or detached the group. Refetch. |
MODIFIER_DUPLICATE_OPTION / MODIFIER_OPTION_NOT_FOUND | An integration bug: the same option twice (use quantity), or a bad id. |
The delivery area
A restaurant delivers in its own region. Your customer draws that region as zones — built from postcodes, from an area on a map, or both — and each zone carries its own delivery fee, minimum order value and extra delivery time. Zones can also exclude an area, and an exclusion always wins.
/api/client/v1/{organizationId}/restaurant/deliveryPublicWhether an address can be delivered to, and what it costs on this basket.
// the body
{ "postal_code": "1012 JS", "subtotal": 24.50 }// the answer
{
"deliverable": true,
"unrestricted": false,
"state": "resolved",
"shortfall": 0,
"zone": {
"name": "Centrum",
"fee": 2.5,
"min_order_amount": 15,
"free_delivery_from": 30,
"extra_minutes": 0
}
}A cart does not call this. The quote takes the basket and the address together and answers with the fee, the minimum, the shortfall and any refusal in one call, priced by the same function as the checkout — see Online Ordering. This endpoint answers the one question the quote cannot: whether an address is deliverable when there is no basket yet, because a quote needs at least one line. That makes it an address-field check, and nothing more.
unrestricted: true means no zones are set and this customer delivers everywhere; show nothing in that case. A shortfall above zero means the address is fine but the basket is too small, which the customer fixes by ordering more, not by moving house.
Branch on state, the same word the quote uses: resolved, outside, unrestricted or needs_address. The last one comes with deliverable: null and a needs list — a postcode alone could not place the address in a zone drawn on the map, so ask for the street and house number and check again. It is not a no, and the customer should not hear one: the full address may well be inside.
The fulfillment block
"fulfillment": {
"type": "delivery",
"requested_at": "2026-07-26T18:30:00.000Z",
"address": {
"street": "Eerste Helmersstraat",
"house_number": "56",
"postal_code": "1054 DJ",
"city": "Amsterdam",
"country": "NLD",
"note": "Ring the bell twice"
},
"table_number": "12",
"customer_note": "No onions please"
}| Field | Required | Notes |
|---|---|---|
type | yes, within the block | pickup, delivery or dine_in. Must be one your customer enabled. |
requested_at | no | ISO 8601 UTC. The time the buyer asked for. Omit for as soon as possible. |
address | for delivery | street, house_number, postal_code, city, plus optional country and note. |
table_number | no | Only meaningful for dine-in. Up to 20 characters. |
customer_note | no | Up to 1000 characters. Printed on the kitchen ticket. |
The block itself is optional, for backwards compatibility with storefronts built before it existed. Omit it and the order still goes through, but it reaches the kitchen with no type, no time and no address, which is not usable for a restaurant. Always send it.
The customer_note is printed, so it is for allergies and doorbell instructions. Not marketing copy, and not anything long.
Timing
default_prep_minutes is how long the kitchen takes, for pickup and delivery alike. delivery_prep_minutes is the ride, and a delivery takes both: with 20 and 30, pickup is "ready in about 20 minutes" and delivery "with you in about 50", not 30. An order the venue accepts automatically also gets its delivery zone's extra minutes. They are what your customer configured, and indicative only.
The kitchen confirms the real time when it accepts the order, and the buyer receives that by email. Do not present your own estimate as a guarantee, and do not state a delivery time on your confirmation page. Two sources naming different times is worse than one saying nothing.
The live board
Inside the dashboard, orders land on a live board. Staff accepts an order, sets how long it will take, marks it ready, and hands it over or sends it out. The board updates in real time across every device that has it open, so two people in a kitchen cannot both accept the same order.
Each organization defines its own statuses and the emails attached to them, which is why your site should not model an order lifecycle of its own. There is nothing for you to build here. It is worth knowing it exists, because it is the reason your storefront never has to tell anyone how long their food will be.
Tickets and receipts
Orders can print automatically to a kitchen printer as they arrive. The ticket layout, what appears on it and which printer takes which orders are configured under Settings then Restaurant Ordering. Receipts for the buyer are configured in the same place.
Grouping is worth matching. If your customer mapped a category field, the ticket groups by it. Rendering your menu in the same groups means staff and buyer are looking at the same menu when something needs discussing.
Errors specific to this module
These are the checkout's, at the moment an order is refused. The quote reports the same conditions earlier and in a different shape, so read the table as "what the customer meets at the end" and the note under it as "what your cart sees all along".
| Status | Code | Cause |
|---|---|---|
400 | FULFILLMENT_TYPE_UNAVAILABLE | That type is switched off. Read the settings and only offer what is there. |
400 | ADDRESS_REQUIRED | Delivery without an address. |
400 | DELIVERY_OUT_OF_AREA | The address falls outside every delivery zone. Check it at the address step so nobody hears this at the payment screen. |
400 | DELIVERY_AREA_EXCLUDED | The address sits in an area your customer explicitly excluded. |
400 | DELIVERY_ADDRESS_INCOMPLETE | The zone can only be decided from a full address and the street or house number is empty. Not a refusal of the address: complete it and send again. |
400 | BELOW_MINIMUM_ORDER | The basket is under the minimum for that zone, or under the pickup minimum. The message names the shortfall. |
400 | VALIDATION_ERROR | A misspelled key inside fulfillment. The message names it. |
403 | ORDERING_DISABLED | Online ordering is switched off for this organization. |
403 | PERMISSION_DENIED | A pay-in-person checkout with a key that lacks create_orders. |
404 | CUSTOM_PAYMENT_METHOD_NOT_FOUND | A pay-in-person checkout naming a method that is not active for this organization. Nothing is created. |
503 | — | Payments are briefly unavailable. Show "Ordering is briefly unavailable" rather than a raw error: it is temporary, and a customer shown a stack trace does not come back an hour later to try again. |
Quoting the basket first does not replace this branch. The quote is a courtesy so the news arrives early; the checkout is the authority. The two can legitimately disagree — a zone gets redrawn while a basket is open, or the customer edits the address after quoting — and only one of them decides whether an order exists.