Rate limits
Limits are applied per account, per endpoint, in a fixed window. Filtered
reads with unhide=true additionally have a separate budget for each order.
- Default: 1000 requests / 60 seconds for each user on each endpoint.
- Individual endpoints can be configured with tighter or looser rules.
- DTU pre-checks use the dedicated
POST /orders (check)bucket: 120 requests / 60 seconds per account. - Exceeding the window returns HTTP 429 with
statusCode: 8(LIMIT_REACHED).
Because the counters are per endpoint, a hot polling loop on GET /orders
cannot starve your ability to place new ones.
Check your effective limits
Do not hardcode 1000/60 — ask:
curl https://approute.io/api/v1/rate-limits \
-H "X-API-Key: $APPROUTE_API_KEY"
See GET /rate-limits for the
response shape.
Reading orders and revealing codes
GET /rate-limits lists five variants for GET /orders:
| Bucket | Scope and default budget |
|---|---|
GET /orders | Unfiltered listing; 2 requests / 60 seconds per account with the seeded rule |
GET /orders (filtered) | orderId or referenceId, with unhide omitted or false; 60 requests / 60 seconds per account with the seeded rule |
GET /orders (unhide pending) | unhide=true, status IN_PROGRESS or PARTIALLY_COMPLETED; 60 requests / 60 seconds per order |
GET /orders (unhide completed) | unhide=true, status SUCCESS or CANCELLED; 1 request / 60 seconds per order |
GET /orders (unhide admission) | Every unhide=true attempt before reading the supplier-facing service; 61 requests / 60 seconds per order |
These are defaults; GET /rate-limits reports the effective configuration.
Listing and masked polling fall back to the global defaults if their rules are
absent. The two unhide phase budgets retain their own defaults described below.
PARTIALLY_COMPLETED is a terminal partial result and may contain delivered
codes. It uses the pending budget, but does not require continued polling.
The first response that contains
a completed order uses the separate completed budget, so pending polling does
not consume that quota. The completed limit also applies when codes are already
cached. If a completed response is lost, retrying may require waiting for the
next window. Save the codes after a successful response.
Use the API key that created the order: other keys on the account do not gain
visibility of it. The orderId and referenceId aliases of the same order share
its counter; changing the identifier does not give you another budget. Different
orders have independent unhide budgets.
Poll with unhide=true from the start
Use the same filtered request while waiting and when retrieving the result:
curl -i --get https://approute.io/api/v1/orders \
-H "X-API-Key: $APPROUTE_API_KEY" \
--data-urlencode 'referenceId=your-order-reference' \
--data-urlencode 'unhide=true'
Read data.page.items[].status: the outer status: SUCCESS only means the list
request succeeded. While an item is IN_PROGRESS, schedule another poll and
respect any Retry-After. A single loop polling every six seconds makes about
10 reads per minute, within the default pending budget of 60 per order. Other
workers reading the same order share that budget.
Once the item is SUCCESS, PARTIALLY_COMPLETED or CANCELLED, save the result
and stop polling. A SUCCESS response consumes the separate completed allowance;
another unhide=true read of that order in the same window receives 429,
even when the codes are cached. If you only need to reread status, omit unhide
or set it to false; voucher codes will be masked and the account-wide filtered
budget applies.
Checkout and admission
When a checkout referenceId selects several children, any selected SUCCESS
or CANCELLED child puts the entire request in the completed bucket, even while
other children are pending. Add the child's orderId to the checkout reference
to select that child with its own counter and state budget.
The admission budget bounds downstream reads, including attempts that later
receive a state-limit 429. With the defaults, after 60 pending replies, request
61 may return the now-completed order using its completed quota. If it is still
pending, request 61 receives 429 and further reads wait until the admission
window rolls over. Respect Retry-After instead of exhausting the polling budget.
Configured global pending and completed rules take precedence over their
60/60 and 1/60 fallbacks. Admission is derived from their sum and the greatest
common divisor of their windows; it has no independent setting. For example,
pending 12/90 seconds and completed 2/60 seconds produce admission
14/30 seconds. Its source is rule if either component is configured,
otherwise default.
Handling 429
Back off and retry — this one is temporary. Every 429 carries a
Retry-After header (in seconds); wait that long before retrying, and add
jitter if several workers share the key. Keep the same referenceId on
purchase retries so a late-arriving original is replayed rather than
duplicated.
:::danger 429 and 409 are not the same limit
HTTP 429 LIMIT_REACHED means a per-window rate limit (ours, or a 429
relayed from the upstream shop service). Retrying later works.
HTTP 409 API_KEY_LIMIT_EXCEEDED (statusCode: 13,
errorCode: "api_key_transaction_limit_exceeded") means the key's lifetime
spend cap is exhausted. Retrying never works — the limit has to be raised.
Before 2026-05 both cases returned 429. If you still branch on 429 for "key exhausted", you are now silently retrying a permanent failure. :::
Staying under the limit
- Poll asynchronous orders with back-off, not in a tight loop — an
IN_PROGRESSorder comes withRetry-After; respect it. - Cache catalog reads. Prices and denominations do not change every second.
- Use
POST /cartsfor multi-item purchases instead of N calls toPOST /orders. - Coordinate your workers. Account-wide endpoint counters are shared across API keys; unhide counters are scoped to the order or selected checkout child.
- Keep DTU pre-checks (
ordersType="dtu"+checkOnly=true) under 120 per minute and cache the verdict per account — see the note below.
:::caution DTU pre-checks have their own Public API bucket
POST /orders with checkOnly=true consumes
POST /orders (check): 120 requests / 60 seconds per account. The
counter key is principal.user_id, so every API key belonging to the same
account shares that budget; issuing another key does not increase it.
On excess the response is HTTP 429 with statusCode=LIMIT_REACHED.
statusMessage names POST /orders (check) and its configured budget, and
Retry-After: 60 tells you how long to wait before retrying.
The limit applies to the pre-check only — placing the order is not
throttled this way, and POST /orders validates the top-up data anyway. If a
pre-check is not essential to your flow, skip it rather than retrying it.
:::