Handling Errors
Canonical Error Envelope
Structured application errors use this envelope. Framework validation can
instead return a detail array, SlowAPI can return a string-valued error,
and intermediary errors may not be JSON. Check value types and HTTP status.
The example below must not trigger an automatic retry:
{ "error": { "code": "NO_HEALTHY_SESSION", "message": "No healthy upstream session available.", "type": "account_error", "request_id": "req_abc123", "retryable": false }}Some errors (particularly validation and routing errors) may be wrapped in a
detail field: {"detail": {"error": {...}}}. Always check for error at
both the top level and under detail.
Error codes may arrive in UPPER_SNAKE_CASE or lowercase_snake_case. Always compare case-insensitively.
retryable may be true, false, or null (when retry semantics don’t
apply). Treat null as false for safety.
| Field | Description |
|---|---|
code | Machine-readable error code (compare case-insensitively) |
message | Human-readable error description |
type | Error classification |
request_id | Correlation ID — also in X-Request-Id response header |
retryable | Whether the operation is safe to retry (true/false/null) |
Retry Strategy
For a request known to be safe to repeat, transient errors can use bounded
exponential backoff. retryable: true does not prove a write was not applied.
Reconcile run/job status before retrying an uncertain write. The example below
is only for callers that have established replay safety; it does not retry
network exceptions automatically:
import timeimport requests
def retry_request(url, headers, json_body, max_retries=4): for attempt in range(max_retries): response = requests.post(url, headers=headers, json=json_body, timeout=60) if 200 <= response.status_code < 300: return response try: payload = response.json() except ValueError: return response detail = payload.get("detail") if isinstance(payload, dict) else None error = payload.get("error") if isinstance(payload, dict) else None if not isinstance(error, dict) and isinstance(detail, dict): error = detail.get("error", detail) if isinstance(error, dict) and error.get("retryable") is True: wait = min(2 ** attempt, 60) # 1s, 2s, 4s, 8s, max 60s time.sleep(wait) continue return response # Non-retryable error return responseNon-Retryable Errors
Do not retry these errors — fix the root cause instead:
| Status | Code | Action |
|---|---|---|
| 401 | INVALID_API_KEY | Check your API key |
| 401 | MISSING_API_KEY | Add Authorization: Bearer gmk_live_... |
| 401 | EXPIRED_API_KEY | Create a new key |
| 401 | REVOKED_API_KEY | Create a new key |
| 403 | ACCOUNT_REAUTH_REQUIRED | Reconnect the ChatGPT account in the User App |
| 403 | CONVERSATION_ACCESS_DENIED | Use the correct API key |
| 403 | ASSET_ACCESS_DENIED | Check conversation ownership |
| 400 | UNSUPPORTED_PARAMETER | Remove the unsupported parameter (tools, n>1, etc.) |
| 404 | CONVERSATION_NOT_FOUND | Verify the conversation ID |
| 404 | PROJECT_NOT_FOUND | Verify the project ID |
| 404 | ASSET_NOT_FOUND | Verify the asset ID |
| 413 | FILE_TOO_LARGE | Use a smaller file (max 10 MB) |
Note: Error codes may arrive in lowercase (e.g., conversation_not_found).
Always compare case-insensitively.
Streaming Errors
During streaming, errors are emitted as structured response.failed lifecycle
events — not as assistant content text. See Streaming
for the full SSE error contract.
# Python streaming error handlingimport jsonimport httpx
with httpx.stream("POST", url, headers=headers, json=body) as resp: for line in resp.iter_lines(): if not line.startswith("data: "): continue data = line[6:] if data == "[DONE]": break chunk = json.loads(data) lifecycle = chunk.get("lifecycle_event") if lifecycle and lifecycle["type"] == "response.failed": err = lifecycle["error"] print(f"Error: {err['code']} — {err['message']}") if err.get("retryable"): # Retry with backoff pass break # Stop processing content = chunk["choices"][0]["delta"].get("content", "") print(content, end="")Request ID
Every response includes an X-Request-Id header. If you send an X-Request-Id
header in your request, it is echoed back. Always include this ID when reporting
issues to support.
Next Steps
- Errors — Complete error code reference
- Streaming — SSE error contract
- Rate Limits — Rate limiting details