Skip to main content

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:

BucketScope and default budget
GET /ordersUnfiltered 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_PROGRESS order comes with Retry-After; respect it.
  • Cache catalog reads. Prices and denominations do not change every second.
  • Use POST /carts for multi-item purchases instead of N calls to POST /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. :::