Documentation

Retries and Backoff

Retry only 429 and 5xx responses with exponential backoff and jitter, honor Retry-After, and leave validation errors alone.


A retry loop that treats every failure the same will burn your per-minute budget and delay the real fix. Retrying a malformed request returns the same 400, and retrying a billing 402 never clears the payment state. The recommendation is narrow: retry 429 and 5xx with capped exponential backoff and jitter, and fail fast on everything else.

ResponseRetry?Action
429 rate_limited with Retry-AfterYesWait the stated seconds, then retry with jitter
429 without Retry-AfterBoundedRetry at most a few times, then inspect quota or credits
500, 502, 503YesBack off exponentially and cap attempts
400, 401, 402, 403, 404, 413, 422NoFix the request, key, plan, or billing state

A time-based 429 is a pause, not a permanent failure, so Retry-After is the strongest signal available. The API does not publish quota headers beyond that value, so do not infer a remaining budget from response headers. A 429 that persists after the stated delay usually means the monthly quota, hard cap, or AI credit balance is exhausted. Escalate instead of extending the loop.

The per-minute limit counts every request, including retries, while the monthly quota counts successful billable requests. That makes a tight retry loop expensive even when the calls keep failing.

type ApiFailure = Error & { status?: number; retryAfter?: number };

async function getJson<T>(url: URL, key: string): Promise<T> {
  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${key}` },
    signal: AbortSignal.timeout(20_000),
  });
  if (res.ok) return (await res.json()) as T;

  const seconds = Number(res.headers.get("Retry-After"));
  const error = new Error(`HTTP ${res.status}`) as ApiFailure;
  error.status = res.status;
  error.retryAfter = Number.isFinite(seconds) ? seconds : undefined;
  throw error;
}

async function withRetry<T>(fn: () => Promise<T>, attempts = 3): Promise<T> {
  for (let i = 0; i < attempts; i++) {
    try {
      return await fn();
    } catch (err) {
      const e = err as ApiFailure;
      const retryable = e.status === undefined || e.status === 429 || e.status >= 500;
      if (!retryable || i === attempts - 1) throw err;
      const waitMs = e.retryAfter ? e.retryAfter * 1000 : 500 * 2 ** i;
      await new Promise((resolve) => setTimeout(resolve, waitMs + Math.random() * 250));
    }
  }
  throw new Error("unreachable");
}

Every data endpoint is a GET, so repeating a request is naturally idempotent. If a retry can trigger a side effect in your own system, use an idempotency key that your application owns; the API has no idempotency-key endpoint.