Skip to main content
There are two very different “something is missing” situations, and you handle them in opposite ways.
  • A refused value is a successful response that contains { "unavailable": "..." } in place of a value. It is a product fact, not an error — never retry it.
  • A request error is a non-2xx HTTP status: bad key, over the limit, or a malformed body. Some are retryable, some are not.

Refusals are not errors

When a field cannot be proven, it comes back as a refusal envelope inside an otherwise successful 200:
Branch on unavailable. Do not substitute 0 or null, and do not retry — the value will not appear on a second call. recovery.action tells an agent what to do instead. See Response model.

Request errors

These are non-2xx statuses. Retry the transient ones; fix the cause of the rest. HTTP errors are never { "error": ... }. Schema 2 validation errors use { "data": { "unavailable": "invalid_request", "errors": [...] }, "meta": { "schema_version": "2" } }. Legacy schema-1 requests can still return the frozen flat refusal shape.
rate_limit_exceeded (per-minute) clears in seconds; quota_exhausted (daily or monthly) clears only at the window boundary, which can be hours or days away. If the response carries a Retry-After header, honor it. Authenticated tool responses also carry X-Credits-Remaining when Unkey reports remaining credits, so batch jobs can watch their budget directly.

A retry loop

Retry only on 429 and 5xx, prefer a Retry-After header when present, cap the attempts, and cap the total wait so a quota exhaustion fails fast instead of sleeping for hours.
retry.py
The wait budget is also your quota guard: when a 429 is a quota exhaustion, the reset can be hours away, so the loop refuses to sleep that long and surfaces it instead. Treat that as “stop and report,” not an error to swallow. See Plans and limits.

Where this goes wrong

  • Treating a refused value (unavailable is present) as an error and retrying it. It is a stable 200.
  • Retrying invalid_api_key with the same revoked key, or retrying a 422 without fixing the body.
  • Looping on quota_exhausted instead of waiting for the window to reset.
  • Substituting 0 for a refused number, which silently ships a wrong value.

Production patterns

Drop this loop into a cached, key-safe client.