↑↓select↵openescclose

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.

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

FieldRequiredRule
schemaVersionyes"1".
externalIdyesYour order id, unique per connection. 1–64 characters of [A-Za-z0-9._~-].
currencyyesThe restaurant's currency (the menu file's currency).
typeyespickup or delivery.
createdAtyesWhen the customer placed the order.
fulfillment.modeyesasap or scheduled.
fulfillment.requestedAtfor scheduledRequired for scheduled, not allowed for asap. The pickup time for a pickup, the arrival time for a delivery.
fulfillment.couriernoDelivery only: store (default; the restaurant delivers) or partner (your courier). Not allowed for a pickup.
customer.name, customer.phoneyesUp to 200 and 50 characters.
customer.emailnoA valid e-mail address.
customer.addressfor deliverystreet, streetNumber, zipCode, city, countryCode (ISO 3166-1 alpha-2) required; extraAddressInfo, lat, lng optional. Without coordinates the till shows no map or distance.
itemsyes1–200 lines.
items[].lineIdyesUnique within the order.
items[].productIdyesThe menu file's products[].id.
items[].nameyes1–200 characters, as the customer saw it. The till prints it.
items[].quantityyes1–999.
items[].unitPriceyesGross price per piece in cents, without modifiers.
items[].taxRateyesThe line's tax rate in percent (the menu file's taxRate). It applies to the line's modifiers too.
items[].notenoUp to 500 characters.
items[].modifiersyes0–100 entries; send [] for none.
modifiers[].modifierGroupId, modifierOptionIdyesThe menu file's modifierGroups[].id and options[].id.
modifiers[].nameyes1–200 characters, as the customer saw it.
modifiers[].quantityyesPer piece of the line.
modifiers[].unitPriceyesGross price per piece in cents.
amounts.*yesitemsSubtotal, discount, deliveryFee, serviceFee, tip, total; see the sums below.
payment.method, payment.status, payment.amountPaidyesSee the payment rule below.
payment.referencefor onlineYour payment reference, up to 200 characters.
note, deliveryInstructionsnoUp to 1000 characters each.

8.3 Validation

We check in this order and answer the first problem we find:

  1. Key (401 / 403), rate limit (429).
  2. JSON (400 MALFORMED_REQUEST), then the schema (422 INVALID_FIELD with path).
  3. A known externalId: the same payload answers 200 with the first receipt and duplicate: true; a different payload answers 409 ORDER_CONFLICT.
  4. Store: 409 STORE_OFFLINE while the restaurant is offline, 409 WEBHOOK_NOT_CONFIGURED without a webhook URL.
  5. Currency: not the restaurant's → 422 CURRENCY_MISMATCH. Before the restaurant's first menu sync it is not checked.
  6. Sums, using your prices (422 INVALID_AMOUNTS):
    • line total = quantity × (unitPrice + Σ modifier.unitPrice × modifier.quantity)
    • itemsSubtotal = sum of the line totals
    • 0 ≤ discount ≤ itemsSubtotal
    • total = itemsSubtotal − discount + deliveryFee + serviceFee + tip
    • deliveryFee = 0 for a pickup
  7. Payment, one of two combinations:
    • cash / unpaid / amountPaid: 0: the restaurant collects the money;
    • online / paid / amountPaid = total, with a reference: you collected it.

    Any other combination is 422 INVALID_FIELD; a wrong amountPaid is 422 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

JSON
{ "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.
  • 200 with duplicate: true: you sent the same order again (same externalId, same payload). The receipt always says inProgress; the current status is in GET /orders/{externalId}.
  • 409 REQUEST_IN_PROGRESS with Retry-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 / unpaid arrives unpaid, so the restaurant collects. online / paid arrives paid.
  • courier: store shows as the restaurant's own delivery, courier: partner as an external delivery. deliveryFee is 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}

JSON
{ "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

JSON
{ "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.cancelled with reason.code: "PARTNER_REQUESTED", so your event stream stays complete.
  • An order that is already cancelled answers 200 again, so retries are safe.
  • 409 INVALID_TRANSITION for an order that is completed or rejected; 404 ORDER_NOT_FOUND for an unknown one.

8.8 POST /orders/{externalId}/delivery

Only for orders with fulfillment.courier: "partner"; any other order answers 409 INVALID_TRANSITION.

JSON
{ "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" }
FieldEffect in the till
status: assigned, arrivingNo change of the delivery status; the other fields still apply.
status: picked_upDelivery status "on the way".
status: deliveredDelivery status "delivered"; the restaurant can complete the order.
status: failedNothing changes in the till; our support is alerted. Cancel the order separately if needed.
etaPickupAt, etaDropOffAtPickup and drop-off times.
courierDriver name and phone; the latest one counts.
trackingUrlTracking 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.