Skip to main content
The platform enforces two complementary limits on every authenticated request:
  1. Aggregate tier budget — a per-merchant token bucket sized by your plan. Every request consumes at least one token; expensive endpoints consume more.
  2. Per-route caps — tight per-key (or per-IP for unauth flows) limits on specific sensitive routes, layered on top of the budget.
Both must permit the request; if either is exhausted you get 429.

Aggregate tier budget

Tokens refill on a sliding 60-second window. Sub-users share the merchant’s bucket — sub-user count never multiplies your budget.
TierTokens / minute
standard60
premium180
enterprise360
Each request deducts from the bucket according to its cost class:
ClassCostEndpoints
Cheap1All routes by default — reads, profile, settings, normal writes, sub-user CRUD, etc.
Expensive5The five worker-fanout / external-API endpoints listed below.
The expensive endpoints — these dispatch to marketplace workers or hit external-marketplace APIs, so each call costs 5 tokens against your tier budget:
MethodPath
GET/market/items/{itemId}/listings
GET/market/listings/{listingId}
POST/market/buy
POST/market/buy/quick
POST/market/transactions/{tradeId}/items/{itemId}/cancel
So a standard merchant (60 tokens / minute) can burn the whole budget on 60 cheap calls or 12 expensive ones, or any mix in between. A premium merchant gets 36 expensive calls; enterprise gets 72. Need more? Upgrade tier or contact support.

Per-route caps

Independent of the tier budget, these tighter limits apply on specific endpoints. They use their own counters — exhausting one doesn’t affect the others or the tier budget.

Sensitive merchant operations (per-merchant key)

Endpoint groupWindowLimit
API key create / revoke / rotate / IP-allowlist5 min5
Webhook secret create / rotate / revoke5 min5
Webhook URL set / test / revoke5 min10
Account settings (password, email change, delete)5 min5 / 10
Webhook event filter5 min20
Sub-user delete5 min20
Fee setting writes5 min20

Normal writes (per-merchant key)

POST /merchant/users (create), POST /merchant/users/{id}/fund, POST /merchant/users/{id}/suspend|reactivate, trade URL CRUD, API key name update.
WindowLimit
1 min30

Public auth flow (per-IP)

Login, register, refresh, password reset, email/2FA challenge, Discord OAuth start. Tighter per-IP caps to slow brute-force.
Endpoint groupWindowLimit
Login / Discord connect1 min10
Register1 min5
Verify (2FA, email)1 min5
Resend (email codes)1 min3
Password reset request1 hour3
Password reset confirm15 min5
Refresh access token1 min30

Webhooks (per-IP)

Inbound provider webhooks (gatepay, onramp, crypto, marketplace callbacks): 60/min per source IP.

Response headers

For routes covered by a per-route cap, the standard rate-limit headers are returned on every response:
HeaderMeaning
x-ratelimit-limitCap for this route.
x-ratelimit-remainingCalls left in the current window.
x-ratelimit-resetEpoch seconds at which the window resets.
The aggregate tier budget does not emit per-request headers — it only signals on rejection, via:
HeaderWhenMeaning
retry-after429 onlySeconds until the bucket has room again.
The 429 body is the standard envelope:
{
  "requestId": "01900b...",
  "success": false,
  "error": {
    "code": 1005,
    "key": "RATE_LIMITED",
    "message": "Too many requests"
  }
}

Backing off

Honour retry-after when present, otherwise use exponential backoff:
async function withRateLimitRetry<T>(fn: () => Promise<Response>, body: () => Promise<T>): Promise<T> {
  for (let i = 0; i < 5; i++) {
    const res = await fn();
    if (res.status !== 429) return body();

    const retryAfterHeader = res.headers.get("retry-after");
    const seconds = retryAfterHeader ? Number(retryAfterHeader) : 2 ** i;
    await new Promise((r) => setTimeout(r, seconds * 1000));
  }
  throw new Error("rate limit retry exhausted");
}

Per-IP caveats

If you’re calling from a single egress IP across many merchant keys (e.g. a multi-tenant proxy), you may hit per-IP caps on auth endpoints before per-key caps. Split egress IPs or contact support.

Request ID

Every response envelope carries a requestId field — include it in support tickets so we can trace the exact request:
{ "requestId": "01900b...", "success": true, "data": { /* ... */ } }