For developers

Errors

Every refusal /v1 can return, its status, and what to do about it, rendered from the same vocabulary the API throws from.

/v1 · contract version 1.0.0 · OpenAPI reference (openapi.json)


Errors

Every refusal answers in one envelope. The human sentence is in error; the machine-readable reason is in details.code. Branch on the code, never on the sentence: the sentences are improved over time, the codes are not.

Refusal envelope
{
  "error": "The order would take stock below zero at that Location. …",
  "code": "CONFLICT",
  "requestId": "5f3c…",
  "details": { "code": "v1_insufficient_stock", "variantId": "…", "locationId": "…" }
}

The top-level code is the HTTP reason in upper case and carries no more information than the status. requestId is worth logging: quote it if you ask us about a failure.

A 401 is the one refusal with no entry below. It is generic by design (see Authentication).

StatusCodeWhat it means, and what to do
400unit_not_for_saleA line's unit is not one the business sells this item in (the unit is set for buying only, or not used). Nothing was written. details.variantId, details.unit and details.index name the line. Send the line in a unit the item is sold in.
400v1_idempotency_key_invalidThe Idempotency-Key header is not a usable key: it must be 1 to 255 printable ASCII characters, with no control characters or newlines. A UUID is the recommended value.
400v1_idempotency_key_requiredThis endpoint requires an Idempotency-Key header. Generate one value per logical operation (a UUID is ideal), send it with the first attempt, and send the SAME value on every retry of that operation — a retry then returns the original response instead of creating a second order.
400v1_invalid_bodyThe JSON body was missing a required field, malformed, or carried a field this endpoint does not accept. Bodies are strict: an unknown key is refused rather than ignored, so a misspelled field is visible immediately.
400v1_invalid_cursorThe pagination cursor could not be decoded. Pass back the exact nextCursor value from the previous page; do not construct, truncate or URL-mangle one.
400v1_invalid_path_parameterA path parameter is not a well-formed UUID. Identifiers are opaque — pass back exactly what a /v1 response returned.
400v1_invalid_queryA query parameter was missing, malformed, or is not accepted by this endpoint. The message names the parameter. Unknown parameters are rejected rather than ignored, so a typo in a filter name is visible immediately.
400v1_write_rejectedThe write was refused on its merits by the same rule the till obeys — an unconvertible unit, a tender that does not settle the total, or a refund tender through which no money can be handed back. The message names the specific reason.
403api_token_ip_gate_unavailableThe token has an IP allow-list but the API is not configured to determine the client address behind its proxy. The operator must set TRUST_PROXY_HOPS to the number of reverse-proxy hops in front of the API.
403api_token_ip_not_allowedThe token has an IP allow-list and the request came from an address outside it. Check the egress address of the calling server — NAT and cloud egress pools change it.
403api_token_origin_not_allowedThe token has a web-origin allow-list and the request did not come from an allow-listed origin. A server-to-server call sends no Origin header and is refused by such a token; clear the origin list for a backend integration.
403feature_disabledThis Business has not switched on the feature this route belongs to (for /v1/orders: "Orders from delivery apps", in Restaurant settings). Nothing was written. The Business owner switches it on; the same key then works.
403plan_lockedThis Business's plan does not include the partner API, so every call is refused, sales, refunds and voids included. The key itself is valid; the Business owner can upgrade the plan under Billing & plan, and the same keys work again at once.
403v1_location_not_accessibleThe locationId does not belong to this Business, or names a Location that has been retired. Read GET /v1/stock only for Locations this Business currently operates.
404v1_resource_not_foundNo such resource in this Business. Ids are tenant-scoped: an id from another Business is reported as missing, never as forbidden.
409incoming_order_duplicateAn order with this externalId from this platform is already in this Business; nothing was written. To retry a request whose answer you never saw, send the SAME Idempotency-Key instead. The existing order’s id is in details.orderId.
409incoming_order_not_cancellableThe restaurant has already decided this order (accepted, rejected or cancelled), so it can no longer be cancelled through the API; details.status says where it is. Call the restaurant to stop an accepted order.
409quotation_requiredThe Location sells only through quotations: every sale there starts as a quotation that becomes an order, so an order cannot be placed there directly. Nothing was written. details.locationId names the Location. Place the order at another Location, or ask the business to turn the setting off.
409v1_duplicate_customerA customer with that phone number already exists in this Business; a phone is unique within a Business. Look the existing record up with GET /v1/customers?q=<phone> rather than retrying — this endpoint never creates a second row for the same number.
409v1_idempotency_key_reuseThis Idempotency-Key was already used for a DIFFERENT request on this endpoint. Nothing was written. A key identifies one operation for ever — reuse the same key only to retry the identical request, and generate a new key for a new operation.
409v1_insufficient_stockThe order would take stock below zero at that Location. The whole order was rolled back — no bill, no payment and no stock movement was written. details.variantId and details.locationId name what was short.
409v1_line_not_sellableThe item's collection is not open (it is planned, or closed), so a line naming it cannot be sold. Nothing was written. details.variantId names the item, and GET /v1/products shows sellable: false for it. Remove the line, or ask the business to open the collection.
409v1_write_conflictThe sale is not in a state that allows this write — it is already fully refunded, already voided, or the amount would exceed what the bill took. Read GET /v1/sales/{id} for the current status and refundedMinor.
429v1_quota_exceededThis Business has used the partner API calls its plan allows this calendar month (UTC). Sales, refunds and voids are still accepted and counted; every other call is refused until the first day of next month — the Retry-After header gives the seconds until then. The quota is per Business, shared by all its keys; a plan with more calls lifts it at once.
429v1_rate_limited_addressToo many /v1 requests from this network address in the current window. This limit is per calling address and is applied before the credential is checked. Wait the number of seconds in the Retry-After header and retry; a retry of a write is safe as long as it carries the same Idempotency-Key.
429v1_rate_limited_tokenToo many /v1 requests for this API token in the current window. This limit is per token, so a second token in the same Business has its own budget. Wait the number of seconds in the Retry-After header and retry; a retry of a write is safe as long as it carries the same Idempotency-Key.