Rate limits
Requests per minute per key and per account, the RateLimit headers, 429 and Retry-After, and the generation caps per plan.
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):
RateLimit-Policy: each bucket as"<name>";q=<requests>;w=<seconds>.RateLimit: the most restrictive bucket now, as"<name>";r=<remaining>;t=<seconds until reset>.
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:
Node.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_LIMITEDwithdetails.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 and in Credits.