Canonical machine-readable errors. Control-plane returns
{"error":{"code","message","details?"}}; the gateway answers in one of two
envelope shapes -- OpenAI's {"error":{"message","type","code","request_id"}}
by default, and Anthropic's {"type":"error","error":{"type","message"}} when
the client sent X-Gateway-Output-Format: anthropic (e.g. /v1/messages).
The gateway's own refusals carry one of a closed set of canonical codes, so a
client cannot tell from an error which provider answered and never has to parse
a message in a language it did not ask for. A provider's own error body never
reaches the client: it goes to the log and the trace as
2papi.upstream.error (see internal/apierr).
| HTTP | code | Meaning |
|---|---|---|
| 400 | invalid_request |
Request body failed parsing/size limits, or no model in the request |
| 401 | auth_failed |
Missing/unknown virtual key, or an upstream account's credentials were rejected (the message says which) |
| 403 | not_allowed |
Key allowlist excludes the alias; a model the key may not use |
| 403 | content_filtered |
Guardrails in block mode found PII or a prompt-injection phrase |
| 404 | model_not_found |
Unknown alias |
| 405 | method not allowed |
Wrong verb. 405 has no canonical cause of its own, so the status stays and the code names the closest one -- a documented exception, not an oversight |
| 429 | rate_limited |
RPM/TPM/token bucket empty, or max in-flight reached; carries Retry-After when the wait is known |
| 429 | quota_exhausted |
Key/team/org budget exhausted. Deliberately without Retry-After: waiting does not refill an account |
| 502 | upstream_unavailable |
No healthy upstream account, adapter missing, or every attempt failed |
| 504 | upstream_timeout |
The provider did not answer in time |
| 500 | gateway_internal |
Our own bug; nothing about the provider |
Codes the gateway emits by classifying a provider's refusal (context_length,
and the ones above when the provider's own status/body maps onto them) are the
same closed set, documented in internal/apierr/apierr.go.
Diagnostic headers on success/failure: X-Gateway-Route, -Attempts,
-Overhead-MS (pure gateway cost), -Upstream-MS, -Proxy,
-Ratelimit-Remaining, -Concurrency-Remaining.
| HTTP | code | Meaning |
|---|---|---|
| 401 | unauthorized |
No/invalid session where one is required |
| 401 | invalid_credentials |
Login: unknown email or wrong password (identical shape) |
| 403 | email_unverified |
Login before email verification |
| 403 | account_disabled / sso_user_disabled |
Suspended account |
| 403 | feature_not_licensed |
Enterprise feature without license (sso, orgs, audit_export…) |
| 403 | ip_not_allowed |
Client IP (X-Forwarded-For) outside the configured ipacl allowlist |
| 400 | invalid_cidrs |
ipacl payload is not an array of valid IPv4 CIDRs / literal IPs |
| 403 | hosted_only |
Self-serve accounts on plain OSS |
| 403 | sso_email_missing / sso_email_unverified |
IdP claims insufficient |
| 400 | invalid_verification_token |
Expired/used/unknown verification token |
gateway_identity_missing/_mismatch (401/403) · sso_state_mismatch /
sso_state_expired (401) · sso_not_configured (409) ·
sso_token_exchange_failed / sso_token_missing (401) ·
sso_provider_error (401) · sso_not_configured also on start.
| HTTP | code | Meaning |
|---|---|---|
| 409 | insufficient_active_gateways |
Publish needs live gateways |
| 426 | upgrade_required |
Gateway too old for schema v2 |
| 409 | ack_version_not_published / ack_identity_mismatch / gateway_capability_mismatch |
Ack does not match published state |
| 404 | config_version_not_found |
Version unknown/rolled back source invalid |
| 409 | model_alias_conflict / model_unavailable / invalid_model_strategy / invalid_model_alias |
Model route validation |
| 400 | provider_model_missing_slug / provider_operation_invalid_response / provider_operation_response_too_large |
Provider operations |
| HTTP | code | Meaning |
|---|---|---|
| 200 | credited:false |
Webhook replay acknowledged, no double credit |
| 401 | invalid_signature |
Paddle HMAC mismatch/stale ts |
| 422 | unsupported_currency / transaction_not_paid / missing_team_reference / invalid_amount |
Payable-check failed |
| 422 | invalid_amount (adjust) |
Manual adjustment ±0 or >10k |
| 503 | webhook_not_configured |
PADDLE_WEBHOOK_SECRET unset |
quota_reset_credit_unavailable · quota_reset_confirmation_required ·
quota_reset_pending · quota_reset_active · quota_reset_state_conflict ·
quota_reset_not_unknown · quota_reset_operation_not_found ·
resolution_invalid / resolution_note_required ·
codex_account_not_found · credential_revision conflict → 409 credential_revision_conflict.
invalid_content_length / payload_too_large (413) · invalid_json
(400) · validation_failed (400, Zod issues attached) · not_found
(404) · internal_error (500; details only with CODEX_TEST_MODE=true).
- Reuse an existing code when the meaning matches.
- New code = lowercase snake_case, added here in the same commit.
- Money-path codes must keep their idempotency guarantees (see credit_transactions UNIQUE + webhook tests).