Reference
Rate limits
Per-key rate limits, response headers, the 429 response, and how to back off.
The Static Ads Lab API is rate-limited per API key. Exceed the limit and you'll get 429 RATE_LIMIT_EXCEEDED.
Default limit
The default per-key limit is 100 requests per 60 seconds (rolling 60-second window). Per-key overrides can raise this — contact us if your use case needs more.
In practice:
- A user-facing app generating ads in response to user actions will not hit the limit.
- A nightly batch job firing thousands of
POST /v1/image-adsin parallel may.
Response headers
Every response (success or failure) includes:
| Header | Value |
|---|---|
RateLimit-Limit | The current per-key limit (e.g. 100) |
RateLimit-Remaining | Requests remaining in the current window |
RateLimit-Reset | Seconds until the current window resets |
On a 429, you also get:
| Header | Value |
|---|---|
Retry-After | Seconds to wait before retrying |
The 429 response
HTTP/1.1 429 Too Many Requests
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 17
Retry-After: 17
Content-Type: application/json
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit of 100 requests per 60s exceeded. Retry after 17 seconds."
},
"meta": { "request_id": "req_...", "timestamp": "..." }
}Recommended retry pattern
async function fetchWithRetry(url, init, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const r = await fetch(url, init);
if (r.status !== 429) return r;
const retryAfter = Number(r.headers.get("Retry-After") ?? "5");
const backoff = retryAfter * 1000 * 2 ** attempt;
await new Promise((res) => setTimeout(res, backoff));
}
throw new Error("Exceeded retry budget on rate limit");
}For burst protection, queue your requests client-side rather than firing N in parallel.
Polling guidance
Polling a resource (e.g. GET /v1/image-ads/:id) counts toward the limit. If you have many ads in flight:
- Use batch polling:
GET /v1/image-ads?ids=ia_a,ia_b,…returns N statuses in one request. - Or use SSE:
Accept: text/event-streamonGET /v1/image-ads/:idstreams progress without repeated polling.
See Async jobs for both.
What does not count
- Webhook deliveries (when supported) — you don't pay for them in rate budget.
- Failed billable charges — the API call still counts, but no wallet charge applies.