Static Ads Lab
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-ads in parallel may.

Response headers

Every response (success or failure) includes:

HeaderValue
RateLimit-LimitThe current per-key limit (e.g. 100)
RateLimit-RemainingRequests remaining in the current window
RateLimit-ResetSeconds until the current window resets

On a 429, you also get:

HeaderValue
Retry-AfterSeconds 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": "..." }
}
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-stream on GET /v1/image-ads/:id streams 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.