The error shape
{
"error": {
"code": "VERSION_CONFLICT",
"message": "The menu is at version 16.",
"request_id": "req_01JA9Z",
"details": { "current_version": 16 }
}
}- Rely on
codein your code, not onmessage. Messages may change. - Every response carries
X-Request-Id. Send it to us when you ask for support.
Codes
| HTTP | Code | Meaning |
|---|---|---|
| 400 | VALIDATION_FAILED |
a field is missing or wrong, or an id contains :; details in details[] with path |
| 400 | URL_NOT_ALLOWED |
a webhook or quote URL is not public https |
| 400 | IDEMPOTENCY_KEY_MISSING |
a changing call without Idempotency-Key |
| 401 | KEY_UNKNOWN |
the key id does not exist |
| 401 | KEY_REVOKED |
the key was revoked |
| 401 | KEY_MODE_MISMATCH |
a test key on the live address, or the reverse |
| 401 | TIMESTAMP_SKEW |
the time is more than 300 seconds from ours |
| 401 | NONCE_INVALID |
the nonce is invalid |
| 401 | NONCE_REUSED |
the nonce was used before |
| 401 | SIGNATURE_INVALID |
the signature does not match |
| 403 | SCOPE_MISSING |
the link lacks the scope; its name is in details.scope |
| 403 | LINK_REQUIRED |
you have no link to this place |
| 403 | PARTNER_NOT_CERTIFIED |
a live call before certification |
| 403 | PARTNER_SUSPENDED |
the partner account is suspended |
| 404 | NOT_FOUND |
the id does not exist, or is not yours |
| 404 | MENU_NOT_FOUND |
this place has no menu from you |
| 404 | MENU_VERSION_GONE |
the requested version is no longer kept; the current one is in details.current_version |
| 409 | VERSION_CONFLICT |
the menu changed after the version you sent |
| 409 | IDEMPOTENCY_KEY_REUSED |
the same key with a different body |
| 409 | IDEMPOTENCY_IN_PROGRESS |
the first request with that key has not finished |
| 409 | INVALID_TRANSITION |
the order cannot move to that status |
| 409 | ALREADY_ACCEPTED |
the order was already accepted, e.g. when rejecting it |
| 409 | ORDER_CLOSED |
the order is final; its status is in details.status |
| 409 | LOCATION_ALREADY_LINKED |
this branch of yours is linked to another place |
| 409 | LINK_CODE_EXPIRED |
the link code expired |
| 409 | LINK_CODE_USED |
the link code was used |
| 410 | LINK_REMOVED |
the merchant removed the link |
| 413 | PAYLOAD_TOO_LARGE |
the body is over 2 MB |
| 422 | MENU_INVALID |
the menu breaks the CM v1 rules |
| 422 | PRODUCT_INVALID |
the product breaks the CP v1 rules Coming soon |
| 429 | RATE_LIMITED |
over the limit; wait for Retry-After |
| 500 | INTERNAL |
our fault |
| 503 | UNAVAILABLE |
maintenance or temporary load |
When to retry
| Answer | What to do |
|---|---|
| 5xx, 429, network failure | retry with growing delays and the same Idempotency-Key |
409 IDEMPOTENCY_IN_PROGRESS |
wait a second and retry |
409 VERSION_CONFLICT |
read the menu or publish it in full, then retry |
| other 4xx | do not retry; fix the request |
Idempotency-Key
Every POST, PUT, PATCH and DELETE needs an Idempotency-Key header: 8 to 128 chars of [A-Za-z0-9_.:-].
- We keep the key for 24 hours with a hash of the body, per account and environment, not per API key. So a retry is still recognised after you rotate keys.
- Same key, same body: we return the stored answer with
Idempotent-Replayed: true. The command does not run again. - Same key, different body:
409 IDEMPOTENCY_KEY_REUSED.
The key is not the nonce. A retry takes a new nonce and signature, and keeps the key.
Pick a key that describes the command, like accept-ord_7m1x9c or menu-plc_9q3w7e-<your version number>.
async function withRetry(fn, tries = 5) {
for (let i = 0; ; i++) {
try { return await fn(); }
catch (e) {
const retryable = !e.status || e.status >= 500 || e.status === 429 || e.error?.code === 'IDEMPOTENCY_IN_PROGRESS';
if (!retryable || i >= tries - 1) throw e;
await new Promise((r) => setTimeout(r, Math.min(30000, 500 * 2 ** i)));
}
}
}
// the key is fixed across attempts; the signature is new on each one
await withRetry(() => aurmap('POST', `/v1/orders/${id}/accept`, { prep_minutes: 15 }, `accept-${id}`));function with_retry(callable $fn, int $tries = 5) {
for ($i = 0; ; $i++) {
try {
return $fn();
} catch (RuntimeException $e) {
$status = $e->getCode();
$retryable = $status === 0 || $status >= 500 || $status === 429 || $e->getMessage() === 'IDEMPOTENCY_IN_PROGRESS';
if (!$retryable || $i >= $tries - 1) {
throw $e;
}
usleep(min(30000, 500 * (2 ** $i)) * 1000);
}
}
}
with_retry(fn () => aurmap('POST', "/v1/orders/{$id}/accept", ['prep_minutes' => 15], "accept-{$id}"));Rate limits
| Kind | Limit |
|---|---|
| all calls | 600 a minute per account and environment |
| full menu publish | 12 an hour per place |
| menu patch, state and stock | 120 a minute per place |
| link codes | 60 an hour |
| order commands | 300 a minute |
| events | a daily limit per account and environment |
Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. With 429 we send Retry-After in seconds.
Limits apply from day one, during the free beta too. They are per account and environment, not per key, so creating a new key does not change them. If you need a higher limit, ask support.
Pagination
Lists use a cursor:
aurmap GET '/v1/orders?status=open&limit=100'
aurmap GET '/v1/orders?status=open&limit=100&cursor=c_8Jk2'{ "data": [ { "order_id": "ord_7m1x9c", "…": "…" } ], "next_cursor": "c_8Jk2" }limitfrom 1 to 100, 50 by default.- A cursor is valid for 24 hours. Do not read or build it.
- An empty
next_cursormeans the end.