# Rate limits

> HTML version: https://storeshot.art/docs/rate-limits/

StoreShot limits how fast an account can call the API and how many live generations it can run.
The numbers depend on your plan (table below); the headers of each response are always the
authoritative values.

## Requests per minute

Every authenticated request is counted per minute in two buckets: one per API key (`key`) and one
per account (`user`, shared by all its keys and access tokens; access tokens use only this one).
Test keys count like live keys. A request is counted only if every bucket allows it, so a refused
request counts nowhere. `/health`, `/status`, `/devices` and the OpenAPI document are never
limited.

Every authenticated response carries two headers
([draft-ietf-httpapi-ratelimit-headers](https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/)):

- `RateLimit-Policy`: each bucket as `"<name>";q=<requests>;w=<seconds>`.
- `RateLimit`: the most restrictive bucket now, as `"<name>";r=<remaining>;t=<seconds until reset>`.

```sh
curl -sS -D - -o /dev/null https://api.storeshot.art/v1/me \
  -H "Authorization: Bearer $STORESHOT_API_KEY" | grep -i '^ratelimit'
```

Over a limit the answer is `429 RATE_LIMITED` with a `Retry-After` header (seconds) and
`details.scope` (`key` or `user`). Wait that long, then retry:

<!-- tabs -->
```js
// fetch() that waits out 429 answers (Retry-After seconds), up to 3 times.
async function fetchWithRetry(url, options = {}, attempts = 3) {
  for (let attempt = 1; ; attempt += 1) {
    const res = await fetch(url, options);
    if (res.status !== 429 || attempt === attempts) return res;
    const seconds = Number(res.headers.get('retry-after') ?? '1');
    await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
  }
}

const res = await fetchWithRetry('https://api.storeshot.art/v1/me', {
  headers: { Authorization: `Bearer ${process.env.STORESHOT_API_KEY}` },
});
console.log(res.status, res.headers.get('ratelimit'));
```

```python
import os, time, requests

def request_with_retry(method, url, attempts=3, **kwargs):
    """requests.request() that waits out 429 answers (Retry-After seconds), up to 3 times."""
    for attempt in range(1, attempts + 1):
        res = requests.request(method, url, **kwargs)
        if res.status_code != 429 or attempt == attempts:
            return res
        time.sleep(int(res.headers.get("Retry-After", "1")))

res = request_with_retry(
    "GET",
    "https://api.storeshot.art/v1/me",
    headers={"Authorization": f"Bearer {os.environ['STORESHOT_API_KEY']}"},
)
print(res.status_code, res.headers.get("RateLimit"))
```

## Generations in progress and per day

Live generations are also capped per account (test-key generations are not counted):

- **In progress at once**: over it, a new generation or set is `429 CONCURRENCY_LIMITED`
  (`Retry-After: 10`). A screenshot set counts as one, whatever its size.
- **Credits per UTC day**: over it, `429 RATE_LIMITED` with `details.policy: daily_generations`,
  until the next UTC midnight. Every image of a set counts.

Webhook test events are limited to 10 per webhook per hour (`details.policy: webhook_tests`).

## Limits per plan

| Plan | Requests / min per key | Requests / min per account | Generations in progress | Credits per UTC day |
|---|---|---|---|---|
| No plan | 60 | 120 | 3 | 100 |
| Pro | 120 | 240 | 4 | 200 |
| Agency | 120 | 240 | 6 | 500 |
| Scale | 300 | 600 | 10 | 1000 |

Plans and prices are on the [pricing page](/pricing/) and in [Credits](/docs/credits/).
