SIMWAY
Partners

Rate limits

Four classes, priced by what a route costs us rather than by a single global number. The limits are published per tier, they are also machine-readable on GET /me, and every response tells you where you stand.

The four classes

Each cell below is a window, not a rate: that many requests may arrive at once, and the sustained rate is the limit divided by the window. A route with no class declared is read.

read

Everything that answers from our own database. The default for any route that does not say otherwise.

BandLimitWindowSustained
Sandbox101s10/s
Bronze101s10/s
Silver201s20/s
Gold and Platinum401s40/s

Routes: GET /me, GET /balance, GET /ledger, GET /deposits, GET /deposits/{id}, GET /plans/{id}, GET /plans/{id}/price, GET /destinations, GET /orders, GET /orders/{id}, GET /esims, GET /esims/{iccid}

write

The two routes that move money: POST /orders, which spends it, and POST /deposits, which mints the invoice that adds it. Size the budget for both — a batch that tops up mid-run is competing with its own ordering.

BandLimitWindowSustained
Sandbox105s2/s
Bronze105s2/s
Silver255s5/s
Gold and Platinum505s10/s

Routes: POST /deposits, POST /orders

supplier

Reserved for routes that call the upstream supplier per request. No route uses it today — the limits are published so that an endpoint added to this class later is not a surprise.

BandLimitWindowSustained
Sandbox11s1/s
Bronze11s1/s
Silver21s2/s
Gold and Platinum41s4/s

Routes: No route uses it today.

catalogue

GET /plans only. One request is the whole price list computed for one buyer.

BandLimitWindowSustained
Sandbox6060s1/s
Bronze6060s1/s
Silver12060s2/s
Gold and Platinum24060s4/s

Routes: GET /plans

The one route with no limit

GET /health is subject to nothing. It takes no key and no rate class, so a monitoring system can poll it as often as it likes.

Headers, and pacing yourself with them

Three headers are on every response in a rate-limited class, not only on 429s. A well-behaved client reads them continuously and never sees a 429 at all.

HeaderMeaning
X-RateLimit-LimitHow many requests the window permits.
X-RateLimit-RemainingHow many are left in it. Floors at 0.
X-RateLimit-ResetUnix seconds at which the window ends.
Retry-AfterWhole seconds, never 0. On a 429 only, and only on the window kind.
Pacing
const remaining = Number(res.headers['x-ratelimit-remaining']);
const resetAt   = Number(res.headers['x-ratelimit-reset']) * 1000;

if (remaining <= 1) {
  await sleep(Math.max(0, resetAt - Date.now()));
}

Buckets are per key, not per account

Two keys means two independent buckets. That is the lever, not an accident: a partner running a nightly catalogue import on one key and live checkout on another has told us those are separate workloads and the import cannot starve the checkout. If you would rather they shared a bucket, use one key.

A bucket is keyed on the key and the rate class together, so listing the catalogue never eats the budget for placing orders.

The mechanism, and its honest cost

Fixed windows, not token buckets. The consequence is worth stating rather than discovering: a caller can spend a full window at the end of one and a full window at the start of the next, so the true worst case is 2× the limit over a moment.

That is acceptable because the rate limit is a fairness and cost control. It is not what keeps your spend inside your balance — a conditional debit does that — and it is not what prevents duplicate orders — a uniqueness constraint does that.

The other 429

Separately from all of the above, 30 failed authentications per 60 seconds per source address is a hard stop. It is checked before we look anything up and is consumed only by failures, so a working integration never touches it.

Read your own limits

GET /me returns rate_limits computed live from your band. It is the machine-readable version of this page and it moves with your tier without a redeploy on either side — so read it at startup rather than copying the table above into a constant.