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.
| Response | Retry? | Action |
|---|---|---|
429 rate_limited with Retry-After | Yes | Wait the stated seconds, then retry with jitter |
429 without Retry-After | Bounded | Retry at most a few times, then inspect quota or credits |
500, 502, 503 | Yes | Back off exponentially and cap attempts |
400, 401, 402, 403, 404, 413, 422 | No | Fix 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.