> ## Documentation Index
> Fetch the complete documentation index at: https://skinshark.gg/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Rate limits

> Tier-based aggregate budget, per-route caps, and how to back off cleanly.

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.

| Tier         | Tokens / minute |
| ------------ | --------------- |
| `standard`   | 60              |
| `premium`    | 180             |
| `enterprise` | 360             |

Each request deducts from the bucket according to its cost class:

| Class     | Cost | Endpoints                                                                            |
| --------- | ---- | ------------------------------------------------------------------------------------ |
| Cheap     | 1    | All routes by default — reads, profile, settings, normal writes, sub-user CRUD, etc. |
| Expensive | 5    | The 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:

| Method | Path                                                   |
| ------ | ------------------------------------------------------ |
| `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 group                                    | Window | Limit  |
| ------------------------------------------------- | ------ | ------ |
| API key create / revoke / rotate / IP-allowlist   | 5 min  | 5      |
| Webhook secret create / rotate / revoke           | 5 min  | 5      |
| Webhook URL set / test / revoke                   | 5 min  | 10     |
| Account settings (password, email change, delete) | 5 min  | 5 / 10 |
| Webhook event filter                              | 5 min  | 20     |
| Sub-user delete                                   | 5 min  | 20     |
| Fee setting writes                                | 5 min  | 20     |

### 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.

| Window | Limit |
| ------ | ----- |
| 1 min  | 30    |

### 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 group          | Window | Limit |
| ----------------------- | ------ | ----- |
| Login / Discord connect | 1 min  | 10    |
| Register                | 1 min  | 5     |
| Verify (2FA, email)     | 1 min  | 5     |
| Resend (email codes)    | 1 min  | 3     |
| Password reset request  | 1 hour | 3     |
| Password reset confirm  | 15 min | 5     |
| Refresh access token    | 1 min  | 30    |

### 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:

| Header                  | Meaning                                   |
| ----------------------- | ----------------------------------------- |
| `x-ratelimit-limit`     | Cap for this route.                       |
| `x-ratelimit-remaining` | Calls left in the current window.         |
| `x-ratelimit-reset`     | Epoch seconds at which the window resets. |

The aggregate tier budget does **not** emit per-request headers — it
only signals on rejection, via:

| Header        | When       | Meaning                                  |
| ------------- | ---------- | ---------------------------------------- |
| `retry-after` | `429` only | Seconds until the bucket has room again. |

The `429` body is the standard envelope:

```json theme={null}
{
  "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:

```ts theme={null}
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:

```json theme={null}
{ "requestId": "01900b...", "success": true, "data": { /* ... */ } }
```
