Core concepts
Orders
Create, validate, read and cancel orders; courier updates for your own couriers.
8.1 POST /orders
Headers: Authorization and Content-Type: application/json.
{
"schemaVersion": "1",
"externalId": "A-1001",
"currency": "EUR",
"type": "delivery",
"createdAt": "2026-10-15T10:00:00Z",
"fulfillment": { "mode": "scheduled", "requestedAt": "2026-10-15T12:30:00+02:00", "courier": "store" },
"customer": {
"name": "Kim Meyer",
"phone": "+49 40 123456",
"email": "kim@example.com",
"address": {
"street": "Hauptstraße", "streetNumber": "1", "zipCode": "20095", "city": "Hamburg", "countryCode": "DE",
"extraAddressInfo": "2nd floor", "lat": 53.55, "lng": 10.0
}
},
"items": [
{
"lineId": "l1", "productId": "p1", "name": "Margherita", "quantity": 2, "unitPrice": 1200, "taxRate": 7,
"note": "well done",
"modifiers": [
{ "modifierGroupId": "g1", "modifierOptionId": "o1", "name": "Cheese", "quantity": 1, "unitPrice": 150 }
]
}
],
"amounts": { "itemsSubtotal": 2700, "discount": 200, "deliveryFee": 300, "serviceFee": 0, "tip": 0, "total": 2800 },
"payment": { "method": "online", "status": "paid", "amountPaid": 2800, "reference": "pay_8d2f" },
"note": "Please ring twice",
"deliveryInstructions": "Back building"
}8.2 Field reference
| Field | Required | Rule |
|---|---|---|
schemaVersion | yes | "1". |
externalId | yes | Your order id, unique per connection. 1–64 characters of [A-Za-z0-9._~-]. |
currency | yes | The restaurant's currency (the menu file's currency). |
type | yes | pickup or delivery. |
createdAt | yes | When the customer placed the order. |
fulfillment.mode | yes | asap or scheduled. |
fulfillment.requestedAt | for scheduled | Required for scheduled, not allowed for asap. The pickup time for a pickup, the arrival time for a delivery. |
fulfillment.courier | no | Delivery only: store (default; the restaurant delivers) or partner (your courier). Not allowed for a pickup. |
customer.name, customer.phone | yes | Up to 200 and 50 characters. |
customer.email | no | A valid e-mail address. |
customer.address | for delivery | street, streetNumber, zipCode, city, countryCode (ISO 3166-1 alpha-2) required; extraAddressInfo, lat, lng optional. Without coordinates the till shows no map or distance. |
items | yes | 1–200 lines. |
items[].lineId | yes | Unique within the order. |
items[].productId | yes | The menu file's products[].id. |
items[].name | yes | 1–200 characters, as the customer saw it. The till prints it. |
items[].quantity | yes | 1–999. |
items[].unitPrice | yes | Gross price per piece in cents, without modifiers. |
items[].taxRate | yes | The line's tax rate in percent (the menu file's taxRate). It applies to the line's modifiers too. |
items[].note | no | Up to 500 characters. |
items[].modifiers | yes | 0–100 entries; send [] for none. |
modifiers[].modifierGroupId, modifierOptionId | yes | The menu file's modifierGroups[].id and options[].id. |
modifiers[].name | yes | 1–200 characters, as the customer saw it. |
modifiers[].quantity | yes | Per piece of the line. |
modifiers[].unitPrice | yes | Gross price per piece in cents. |
amounts.* | yes | itemsSubtotal, discount, deliveryFee, serviceFee, tip, total; see the sums below. |
payment.method, payment.status, payment.amountPaid | yes | See the payment rule below. |
payment.reference | for online | Your payment reference, up to 200 characters. |
note, deliveryInstructions | no | Up to 1000 characters each. |
8.3 Validation
We check in this order and answer the first problem we find:
- Key (
401/403), rate limit (429). - JSON (
400 MALFORMED_REQUEST), then the schema (422 INVALID_FIELDwithpath). - A known
externalId: the same payload answers200with the first receipt andduplicate: true; a different payload answers409 ORDER_CONFLICT. - Store:
409 STORE_OFFLINEwhile the restaurant is offline,409 WEBHOOK_NOT_CONFIGUREDwithout a webhook URL. - Currency: not the restaurant's →
422 CURRENCY_MISMATCH. Before the restaurant's first menu sync it is not checked. - Sums, using your prices (
422 INVALID_AMOUNTS):- line total =
quantity × (unitPrice + Σ modifier.unitPrice × modifier.quantity) itemsSubtotal= sum of the line totals0 ≤ discount ≤ itemsSubtotaltotal = itemsSubtotal − discount + deliveryFee + serviceFee + tipdeliveryFee = 0for a pickup
- line total =
- Payment, one of two combinations:
cash/unpaid/amountPaid: 0: the restaurant collects the money;online/paid/amountPaid = total, with areference: you collected it.
Any other combination is
422 INVALID_FIELD; a wrongamountPaidis422 INVALID_AMOUNTS.
Example: 2 × (1200 + 150) = 2700, minus 200 discount, plus 300 delivery fee = 2800.
Your prices are not compared with the menu's, and availability is not checked. There is no menu version to send either: an order built from an older copy of the menu goes through (§8.5).
8.4 Answers
{ "orderId": "oo_5c1e…", "externalId": "A-1001", "status": "inProgress",
"receivedAt": "2026-10-15T10:00:01.214Z", "duplicate": false }201: the order is received and rings in the till.200withduplicate: true: you sent the same order again (sameexternalId, same payload). The receipt always saysinProgress; the current status is inGET /orders/{externalId}.409 REQUEST_IN_PROGRESSwithRetry-After: 2: the same order is still being received from an earlier request. Retry with the same body.
orderId is our id for the order (oo_ plus 64 hex characters). Two connections may use the same externalId; their orders stay separate.
What the restaurant sees: a new online order under the connection's name, each line with your name and price.
cash/unpaidarrives unpaid, so the restaurant collects.online/paidarrives paid.courier: storeshows as the restaurant's own delivery,courier: partneras an external delivery.deliveryFeeis shown either way.- A scheduled order shows its requested time and is marked as a pre-order.
8.5 Matching, and orders from an older menu
We match each line's productId to the restaurant's product and each modifierOptionId to its option, so that the till applies the restaurant's tax settings and sends the line to the right kitchen printer.
- Name and price are always yours, whether the line matches or not.
- A line that cannot be matched is still accepted. This happens when the restaurant deleted the product after your last menu download. The line reaches the till with your name, price and
taxRate, and prints on the restaurant's default printer. - A switched-off product or option is matched and accepted like any other. It can only reach us when your copy of the menu is behind (every switch comes to you as
menu.updated), which is not the customer's fault. The restaurant decides in the till whether to accept the order. - For a restaurant whose till prices exclude tax, the till converts your gross price with the line's
taxRate.
Always send the correct taxRate: it is what the till uses for lines it cannot match.
8.6 GET /orders/{externalId}
{ "orderId": "oo_5c1e…", "externalId": "A-1001", "status": "cancelled", "version": 3,
"updatedAt": "2026-10-15T10:20:00.000Z",
"cancellation": { "status": "approved", "reason": "Customer cancelled" } }status is one of inProgress, accepted, ready, completed, rejected, cancelled. cancellation is present after your own cancel. 404 ORDER_NOT_FOUND for an unknown order, another connection's order, or one that is still being received.
8.7 POST /orders/{externalId}/cancel
{ "reason": "Customer cancelled" }reason: 1–500 characters. It is kept with the order but not shown in the till.- Answer:
200 {"externalId": "A-1001", "status": "cancelled", "version": 3}. - A cancel is approved at once in v1, and the till shows the order as cancelled.
- You also receive
order.cancelledwithreason.code: "PARTNER_REQUESTED", so your event stream stays complete. - An order that is already cancelled answers
200again, so retries are safe. 409 INVALID_TRANSITIONfor an order that iscompletedorrejected;404 ORDER_NOT_FOUNDfor an unknown one.
8.8 POST /orders/{externalId}/delivery
Only for orders with fulfillment.courier: "partner"; any other order answers 409 INVALID_TRANSITION.
{ "status": "picked_up", "etaPickupAt": "2026-10-15T12:10:00Z", "etaDropOffAt": "2026-10-15T12:30:00Z",
"courier": { "name": "Ali", "phone": "+49 151 000000" }, "trackingUrl": "https://track.example/x" }| Field | Effect in the till |
|---|---|
status: assigned, arriving | No change of the delivery status; the other fields still apply. |
status: picked_up | Delivery status "on the way". |
status: delivered | Delivery status "delivered"; the restaurant can complete the order. |
status: failed | Nothing changes in the till; our support is alerted. Cancel the order separately if needed. |
etaPickupAt, etaDropOffAt | Pickup and drop-off times. |
courier | Driver name and phone; the latest one counts. |
trackingUrl | Tracking link. |
Answer: 200 {"orderId": "oo_5c1e…", "externalId": "A-1001", "changed": true}. changed is false when the update changed nothing, so retries are safe. A courier update never changes the order's status.