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 · Rate limits guide · Spend limits · IP allowlist · Misalignment monitoring · Cybersecurity safeguards · Image generation → errors · Production best practices · OpenAPI Error, ErrorResponse, ErrorEvent, Responses incomplete_details · WIF token exchange errors.
Last verified: 2026-09-18. Machine-readable: generated/fragments/errors/openai-errors.json (54 records).
1. Error body shapes (all observed)
// 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 |
| 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
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
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<T>(fn: () => Promise<T>, attempts = 5, cap = 30): Promise<T> {
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) |