# Rate limits and abuse protection

## Limits

| Scope | Limit |
| --- | --- |
| All requests with one key | 120 per minute |
| `POST /shipments/quote` and `POST /addresses/verify` | 30 per minute per key, and 2,000 per day per organization |
| `POST /shipments/{id}/buy` | 30 per minute per key |
| Request body size | 256 KB |

When you go over a limit the response is `429` with the code `rate_limited`, and a `Retry-After` header with the number of seconds to wait.

Every response carries three headers so you can slow down before you hit a limit:

| Header | Meaning |
| --- | --- |
| `RateLimit-Limit` | The limit that applies to this request. |
| `RateLimit-Remaining` | How many requests are left in the current window. |
| `RateLimit-Reset` | Seconds until the window resets. |

## Handle a 429

```js
async function withRetry(makeRequest, attempts = 5) {
  for (let i = 0; i < attempts; i++) {
    const res = await makeRequest();
    if (res.status !== 429 && res.status < 500) return res;
    const wait = Number(res.headers.get("Retry-After")) || 2 ** i;
    await new Promise((r) => setTimeout(r, (wait + Math.random()) * 1000));
  }
  throw new Error("Giving up after repeated rate limits or server errors");
}
```
```python
import random
import time

def with_retry(make_request, attempts=5):
    for i in range(attempts):
        res = make_request()
        if res.status_code != 429 and res.status_code < 500:
            return res
        wait = int(res.headers.get("Retry-After", 2 ** i))
        time.sleep(wait + random.random())
    raise RuntimeError("Giving up after repeated rate limits or server errors")
```
Retrying a purchase is safe only with the same `Idempotency-Key` on every attempt.

## Abuse protection

- **Bad keys are blocked.** Repeated requests with invalid keys from one IP address are blocked for 15 minutes.
- **Keys are stored hashed.** Only a hash of the secret is kept, and the key is shown once when you create it.
- **Keys can expire.** Set an expiry date when you create a key. An expired key stops working.
- **Keys can be limited to addresses.** An IP allowlist, with exact addresses or IPv4 ranges, rejects requests from anywhere else.
- **Spending is capped.** A per label and a per day limit on the key, plus your account's own limit, bound what a leaked key can spend.
- **Permissions are narrow.** A key only does what you ticked, and the MCP server only lists the tools the key may use.
- **No browser access.** The API sends no CORS headers. Call it from a server and never put a key in front end code.
- **HTTPS only.** In production the API is served over HTTPS only. Never send a key over plain HTTP.

**Tip:** Cache what you can. Rates for the same package and destination are not needed twice in a minute, and the event feed and webhooks make polling shipment status unnecessary.