# OpenAI error catalogue & robust handling — atlas **Status:** catalogue `DOCUMENTED`; 20+ shapes `LIVE_VERIFIED` by deliberate cheap probes on 2026-09-18 (all rejected before generation, cost $0) plus errors logged by the other atlas agents (`reports/live-requests.jsonl`, `tmp-live/`). Rate-limit/overload/quota families are `DOCUMENTED` only (not triggerable safely). **Sources:** [Error codes guide](https://developers.openai.com/api/docs/guides/error-codes) · [Rate limits guide](https://developers.openai.com/api/docs/guides/rate-limits) · [Spend limits](https://developers.openai.com/api/docs/guides/spend-limits) · [IP allowlist](https://developers.openai.com/api/docs/guides/ip-allowlist) · [Misalignment monitoring](https://developers.openai.com/api/docs/guides/safety-checks/misalignment-monitoring) · [Cybersecurity safeguards](https://developers.openai.com/api/docs/guides/safety-checks/cybersecurity) · [Image generation → errors](https://developers.openai.com/api/docs/guides/image-generation) · [Production best practices](https://developers.openai.com/api/docs/guides/production-best-practices) · OpenAPI `Error`, `ErrorResponse`, `ErrorEvent`, Responses `incomplete_details` · [WIF token exchange errors](https://developers.openai.com/api/reference/workload-identity-federation#token-exchange-errors). **Last verified:** 2026-09-18. Machine-readable: `generated/fragments/errors/openai-errors.json` (54 records). ## 1. Error body shapes (all observed) ```jsonc // A. canonical (every model/resource endpoint) {"error": {"message": "Missing required parameter: 'model'.", "type": "invalid_request_error", "param": "model", "code": "missing_required_parameter"}} // B. bare string (Administration endpoints without the scope — HTTP 403) {"error": "You have insufficient permissions for this operation. Missing scopes: api.management.read. Check that you have the correct role in your organization, and if you're using a restricted API key, that it has the necessary scopes."} // C. empty body (Cloudflare edge, unknown URL) — HTTP 404, only CF-RAY header, no x-request-id // D. streaming (after HTTP 200): SSE `event: error` / `response.failed` carrying {code, message, param, sequence_number} // E. optional extensions: error.misalignment {classification, message, continuation_instruction}; error.moderation_details {moderation_stage, categories[]} ``` `type`, `message`, `param`, `code` are all required by the spec but `param`/`code` are frequently `null`. **`type` is almost always `invalid_request_error`, even for 401/403/404** — discriminate on HTTP status + `code`, not on `type`. ## 2. Catalogue | HTTP | `type` | `code` | Meaning / message | Retry? | Verified | |---|---|---|---|---|---| | 400 | invalid_request_error | `invalid_type` | wrong JSON type for `param` | no | ✔ probe | | 400 | invalid_request_error | `missing_required_parameter` | required field absent | no | ✔ probe | | 400 | invalid_request_error | `unknown_parameter` | unrecognised field (strict schema) | no | ✔ probe | | 400 | invalid_request_error | `unsupported_parameter` | field not allowed for this model/endpoint (`temperature` on reasoning models, `dimensions` on ada-002…) | no | ✔ other agents | | 400 | invalid_request_error | `invalid_value` | out-of-range / bad enum (e.g. `max_tokens is too large: … supports at most 32768`) | no | ✔ probe | | 400 | invalid_request_error | `integer_below_min_value`, `integer_above_max_value`, `string_above_max_length`, `array_above_max_length` | bound violations | no | ✔ / docs | | 400 | invalid_request_error | `mutually_exclusive_parameters` | e.g. `previous_response_id` + `conversation` | no | ✔ other agents | | 400 | invalid_request_error | `invalid_json` | body not parseable | no | ✔ probe | | 400 | invalid_request_error | `unsupported_content_type` | non-JSON Content-Type on JSON endpoint | no | ✔ probe | | 400 | invalid_request_error | `invalid_beta` | missing/incorrect `OpenAI-Beta` | no | ✔ other agents | | 400 | invalid_request_error | `previous_response_not_found` | chained response unavailable (deleted, `store=false`, WS state lost) → resend full context | no | ✔ other agents | | 400 | invalid_request_error | `websocket_connection_limit_reached` | Responses WS 60-min cap → reconnect | yes (new conn) | docs | | 400 | invalid_request_error | `context_length_exceeded` | prompt + output > context window | no | docs | | 400 | invalid_request_error | `invalid_prompt` | prompt rejected before generation | no | docs | | 400 | invalid_request_error | (null, `param: service_tier`) | "Invalid service_tier argument… not allowed for this project" | no | docs | | 400 | invalid_request_error | (null) | `X-Client-Request-Id` non-ASCII / > 512 chars | no | ✔ probe | | 400 | image_generation_user_error | `moderation_blocked` (+`moderation_details`) | image prompt/input blocked | no | docs | | 400 | invalid_request_error | `content_policy_violation` | legacy policy rejection | no | docs (LEGACY) | | 400 | invalid_request_error | `invalid_image`, `invalid_image_format`, `image_parse_error`, `invalid_base64_image`, `image_file_too_large`… | image input problems | no | docs | | 400 | invalid_request_error | `response_already_completed` | multi-agent `response.inject` on a finished response (stream event) | no | docs (BETA) | | 400 | invalid_request_error | `fine_tune_not_found`, `upload_not_pending`, `invalid_function_parameters`, `invalid_offer`, `call_id_not_found` | resource-specific validation codes | no | ✔ other agents | | 401 | invalid_request_error | `invalid_api_key` | "Incorrect API key provided: sk-inv***" | no | ✔ probe | | 401 | invalid_request_error | (null) | "Missing bearer authentication in header" | no | ✔ probe | | 401 | invalid_request_error | `mismatched_organization` | `OpenAI-Organization` ≠ key's org | no | ✔ probe | | 401 | invalid_request_error | `invalid_project` | "No such project" (`OpenAI-Project`) | no | ✔ probe | | 401 | invalid_request_error | (null) | audit logs without `api.audit_logs.read` | no | ✔ probe | | 401 | invalid_request_error | `ip_not_authorized` | outside IP allowlist | no | docs | | 401 | — | — | "You must be a member of an organization to use the API" | no | docs | | 403 | *(string body)* | — | admin endpoint without scope (`api.management.read`, `api.usage.read`, `api.roles.read`, `api.groups.read`, `api.mtls.read`) | no | ✔ probe ×12 | | 403 | invalid_request_error | `insufficient_permissions` | structured scope error (`api.external_storage.read`) | no | ✔ probe | | 403 | invalid_request_error | `misalignment_policy_violation` | misalignment monitoring block (may carry `error.misalignment`) | **never** | docs | | 403 | invalid_request_error | `cyber_policy` | cybersecurity safeguard (ZDR orgs; may arrive mid-stream) | no | docs | | 403 | invalid_request_error | `unsupported_country_region_territory` | unsupported location | no | docs | | 403 | — | — | `mtls.auth.openai.com` wrong method/path | no | docs | | 403 | — | — | `GET /v1/safety/cases/{id}` unknown id | no | ✔ other agents | | 404 | invalid_request_error | `model_not_found` | model absent **or not accessible** (project model permissions, org not enabled) | no | ✔ probe | | 404 | invalid_request_error | (null) | "Response with id … not found", conversations, files, evals after delete | no | ✔ probe | | 404 | not_found_error | `not_found_error` | typed 404 on newer resources (vaults, skills, chatkit, agents, videos) | no | ✔ other agents | | 404 | invalid_request_error | `fine_tune_not_found`, `safety_alert_not_found` | resource-specific 404 codes | no | ✔ other agents | | 404 | *(empty)* | — | unknown URL / retired routes (`/v1/assistants`, `/v1/threads/*`, `/v1/realtime/sessions`) | no | ✔ probe | | 405 | invalid_request_error | (null) | "Invalid method for URL (PATCH /v1/models)" | no | ✔ probe | | 409 | invalid_request_error | — | concurrent modification (SDK `ConflictError`) | once | docs | | 422 | invalid_request_error | — | well-formed but unprocessable (SDK `UnprocessableEntityError`, docs: "try again") | maybe | docs | | 429 | rate_limit_error | `rate_limit_exceeded` | RPM/TPM/RPD/IPM exhausted ("Rate limit reached for … Please try again in Xs") | **yes** (Retry-After) | docs | | 429 | rate_limit_error | `slow_down` | ramp too fast (> +50 %/15 min above 1M TPM) | yes, slower | docs | | 429 | insufficient_quota | `insufficient_quota` | no quota/billing | **no** | docs | | 429 | insufficient_quota | `credit_balance_exhausted` | prepaid credits gone | no | docs | | 429 | insufficient_quota | `organization_spend_limit_exceeded` / `project_spend_limit_exceeded` | configured hard spend limits | no (until raised/reset) | docs | | 429 | insufficient_quota | `organization_usage_limit_exceeded` | OpenAI-assigned tier limit | no | docs | | 429 | insufficient_quota | `billing_hard_limit_reached` | legacy name | no | LEGACY | | 500 | server_error | (null) | "The server had an error while processing your request" | **yes** | ✔ 1× other agents | | 503 | service_unavailable_error | `server_is_overloaded` | model capacity exhausted (`Retry-After`) | **yes** | docs | | 200 + stream | `error` event | `server_error`, `rate_limit_exceeded`, `invalid_prompt`, `vector_store_timeout`, image codes… | error after the stream started (`response.failed.response.error`) | depends; never replay consumed output blindly | docs | | 200 | `status: incomplete` | `incomplete_details.reason ∈ {max_output_tokens, max_messages, content_filter, steered}` | not an error object; Chat `finish_reason: length|content_filter` | no | docs | | 4xx | OAuth | `invalid_subject_token`, `invalid_grant`, `invalid_request` | WIF token exchange | no | docs | | — | SDK | `APIConnectionError`, `APITimeoutError` | network / timeout (600 s default) | yes | docs | ## 3. Retryability matrix | Signal | Retry | Wait | Notes | |---|---|---|---| | network error / timeout | yes (≤ 3–5) | exp. backoff 0.5→8 s + jitter | idempotency: Responses/Chat are not idempotent — a timed-out request may have completed and been billed (`background: true` + polling avoids double work) | | 408, 409, 425 | yes (once/twice) | short | | | 429 `rate_limit_exceeded`, `slow_down` | yes | ≥ `Retry-After` (else backoff); after `slow_down` also cut rate | failed requests still count toward RPM | | 429 `insufficient_quota` family (`credit_balance_exhausted`, `*_spend_limit_exceeded`, `*_usage_limit_exceeded`) | **no** | — | needs billing/config action | | 500 `server_error`, 502/504 | yes | backoff | check status.openai.com | | 503 `server_is_overloaded` | yes | ≥ `Retry-After` | switch model/tier as fallback | | `x-should-retry: false` | no | — | SDK honours it | | 400/401/403/404/405/422(schema) | no | — | fix request / credentials / permissions | | 403 `misalignment_policy_violation`, `cyber_policy`, 400 `moderation_blocked` | **never automatically** | — | escalate to a human | | stream `error` after partial output | no blind replay | — | reconcile with `GET /v1/responses/{id}` | SDK defaults: Python/Node/Ruby/Java/Go retry 2× on 408/409/429/5xx and connection errors, honouring `retry-after-ms`/`Retry-After` ≤ 60 s (bigger → give up and surface the error). Python maps 503 to `InternalServerError`, not `RateLimitError` — catch both. ## 4. Robust handling — Python ```python import random, time import openai from openai import OpenAI client = OpenAI(max_retries=0, timeout=60) # do our own retry so we can honour long Retry-After and log everything NON_RETRYABLE_429 = {"insufficient_quota", "credit_balance_exhausted", "organization_spend_limit_exceeded", "project_spend_limit_exceeded", "organization_usage_limit_exceeded"} def call_with_retry(fn, attempts=5, cap=30.0): for attempt in range(1, attempts + 1): try: return fn() except (openai.APIConnectionError, openai.APITimeoutError) as e: delay = min(cap, 0.5 * 2 ** attempt) * (1 + random.random() / 4) except openai.RateLimitError as e: # 429 if e.code in NON_RETRYABLE_429 or (e.body or {}).get("type") == "insufficient_quota": raise # billing problem: retrying cannot help ra = e.response.headers.get("retry-after") or e.response.headers.get("retry-after-ms") delay = (float(ra) / (1000 if "ms" in str(e.response.headers.get("retry-after-ms", "")) else 1)) if ra and ra.replace(".", "").isdigit() else min(cap, 0.5 * 2 ** attempt) delay += random.random() / 4 except openai.InternalServerError as e: # 500 / 502 / 503 server_is_overloaded / 504 if e.response.headers.get("x-should-retry") == "false": raise ra = e.response.headers.get("retry-after") delay = float(ra) if ra and ra.isdigit() else min(cap, 0.5 * 2 ** attempt) * (1 + random.random() / 4) except openai.PermissionDeniedError as e: # 403 — never retry; may be a safety block if e.code in {"misalignment_policy_violation", "cyber_policy"}: notify_operator(e); raise raise except openai.APIStatusError as e: # 400/401/404/405/409/422 … log.error("openai %s %s code=%s param=%s req=%s", e.status_code, e.message, e.code, e.param, e.request_id) raise if attempt == attempts: raise time.sleep(delay) resp = call_with_retry(lambda: client.responses.create(model="gpt-5.4-nano", input="Reply with OK.", max_output_tokens=16)) if resp.status == "incomplete": # not an exception! print("incomplete:", resp.incomplete_details.reason) # max_output_tokens | max_messages | content_filter | steered print(resp.output_text, resp._request_id) ``` Streaming: wrap `for event in stream:`; on `event.type == "error"` or `"response.failed"` stop, record `event.code`, and reconcile with `client.responses.retrieve(id)` instead of re-sending. ## 5. Robust handling — TypeScript ```ts import OpenAI from "openai"; const client = new OpenAI({ maxRetries: 0, timeout: 60_000 }); const NON_RETRYABLE_429 = new Set(["insufficient_quota", "credit_balance_exhausted", "organization_spend_limit_exceeded", "project_spend_limit_exceeded", "organization_usage_limit_exceeded"]); const sleep = (s: number) => new Promise((r) => setTimeout(r, s * 1000)); export async function withRetry(fn: () => Promise, attempts = 5, cap = 30): Promise { for (let attempt = 1; ; attempt++) { try { return await fn(); } catch (err) { const backoff = Math.min(cap, 0.5 * 2 ** attempt) * (1 + Math.random() / 4); let delay: number | null = null; if (err instanceof OpenAI.APIConnectionError) delay = backoff; // network / timeout else if (err instanceof OpenAI.RateLimitError) { // 429 const type = (err.error as any)?.type; if ((err.code && NON_RETRYABLE_429.has(err.code)) || type === "insufficient_quota") throw err; const ra = err.headers?.get("retry-after-ms") ?? err.headers?.get("retry-after"); delay = ra && !Number.isNaN(Number(ra)) ? Number(ra) / (err.headers?.get("retry-after-ms") ? 1000 : 1) + Math.random() / 4 : backoff; } else if (err instanceof OpenAI.InternalServerError) { // 500/502/503/504 if (err.headers?.get("x-should-retry") === "false") throw err; const ra = err.headers?.get("retry-after"); delay = ra && !Number.isNaN(Number(ra)) ? Number(ra) : backoff; } else if (err instanceof OpenAI.PermissionDeniedError) { // 403 — never retry if (err.code === "misalignment_policy_violation" || err.code === "cyber_policy") notifyOperator(err); throw err; } else if (err instanceof OpenAI.APIError) { // 400/401/404/405/409/422 console.error("openai", err.status, err.code, err.param, err.requestID, err.message); throw err; } else throw err; if (attempt >= attempts) throw err; await sleep(delay); } } } const resp = await withRetry(() => client.responses.create({ model: "gpt-5.4-nano", input: "Reply with OK.", max_output_tokens: 16 })); if (resp.status === "incomplete") console.warn("incomplete:", resp.incomplete_details?.reason); console.log(resp.output_text, resp._request_id); ``` A framework-free classifier for raw HTTP responses (handles the string-body 403 and the bodyless 404) lives in `examples/shared/errors/classify_error.{py,ts}` and is exercised by `tests/openai/test_errors.py`. ## 6. Live probes behind this page (2026-09-18, all $0) | Probe | Result | |---|---| | `POST /v1/responses` model `does-not-exist-model` | 404 `model_not_found` | | `max_output_tokens: "sixteen"` | 400 `invalid_type` param `max_output_tokens` | | body without `model` | 400 `missing_required_parameter` | | extra field | 400 `unknown_parameter` | | truncated JSON | 400 `invalid_json` | | `Content-Type: text/plain` | 400 `unsupported_content_type` | | `Authorization: Bearer sk-invalid` | 401 `invalid_api_key` | | no Authorization | 401 "Missing bearer authentication in header" | | bad `OpenAI-Organization` / `OpenAI-Project` | 401 `mismatched_organization` / `invalid_project` | | `GET /v1/responses/resp_000…` | 404 (code null) | | `GET /v1/this_endpoint_does_not_exist` | 404 empty body | | `PATCH /v1/models` | 405 | | `max_tokens: 100000000` (chat, gpt-4.1-nano) | 400 `invalid_value` "supports at most 32768 completion tokens" (not `context_length_exceeded`) | | non-ASCII `X-Client-Request-Id` | 400 | | 14 Administration GETs | 403 string-body (`api.management.read`, `api.usage.read`, `api.roles.read`, `api.groups.read`, `api.mtls.read`), 403 structured `insufficient_permissions` (external storage), 401 (audit logs) |