code, then gives the cause and the fix, and links to the full page for that code under Problem codes. It covers the failures people hit first: rejected keys, credit and quota refusals, runs that cannot be routed, and retries that do not behave the way you assumed.
Log the request id first
Every response carries anx-request-id header, successful ones included. On a JSON error the same value is in error.request_id. It is the one value that identifies a single request, so log it with the status and the code on every failure, before you change anything.
- You can send your own
x-request-id. The server keeps it when it matches^[A-Za-z0-9._-]{1,128}$, and otherwise mintsreq_followed by 32 hex characters. - A few routes can answer with a plain-text body instead of JSON (see Request format). Those bodies carry no
request_id, so read the header. - Branch on
error.code. Themessageis written for people, and its wording is not something to match on.
Keys and authentication
401 invalid_credential: the key is rejected
401 invalid_credential: the key is rejected
Authorization header carried something that is not an active API key. Any of these gives the same code:- The value is not
otsk_followed by exactly 64 lowercase hex characters, 69 characters in all. A truncated copy fails here, and so does the 13-charactersecret_prefixthat key lists show. - The key does not exist.
- The key was revoked. A revoked key gets
invalid_credential, notkey_revoked: that code only comes back when you try to rotate a revoked key. - The key was rotated. The old secret stops working immediately, with no grace period.
- You sent the key id. A
key_...id names a key in URLs; it is not a credential.
printf %s "$OPENTYPE_API_KEY" | wc -c should print 69. Then compare its first 13 characters with the prefixes and states on the API keys page of the console, or in GET /v1/keys called with a key that holds keys_read. A revoked key cannot be re-enabled and a lost secret cannot be recovered, so create a new key. Retrying with the same value never succeeds.
x-request-id, never the secret. See invalid_credential.401 missing_credentials: no key reached the server
401 missing_credentials: no key reached the server
{"error":{"code":"missing_credentials","message":"a bearer credential is required",...}}Why. The request had no Authorization header, used a scheme other than Bearer, or sent an empty token. A common cause is an environment variable that is not set in the process that makes the call, such as a CI job or a container, so the header goes out as Bearer with nothing after it.Fix. Send Authorization: Bearer <key>. The scheme name is case-insensitive. There is no other auth header and no cookie authentication. Print ${#OPENTYPE_API_KEY} in the failing environment to confirm the variable is set.Log the status, the code and the x-request-id. See missing_credentials.400 secret_in_path: a key secret went into a URL
400 secret_in_path: a key secret went into a URL
GET /v1/keys/{key_id}, DELETE /v1/keys/{key_id} or POST /v1/keys/{key_id}/rotate with a value starting otsk_ in the path. The server refused it, but the secret has already travelled in a URL, and URLs end up in shell history, proxy logs and access logs.Fix. Treat the secret as leaked and rotate it now. Find its key_ id by matching the first 13 characters of the secret against secret_prefix in GET /v1/keys, then rotate with a key that holds keys_write. Rotation is only available through the API:200 response keeps the same id, name and scopes and carries the new secret, once. The old secret stops working at that moment, so update every place that uses it. Rotating a revoked key answers 409 key_revoked; create a new key instead.Log the x-request-id of the refused call and of the rotation, not the secret. See secret_in_path and Key rotation.403 scope_denied: the key lacks a scope
403 scope_denied: the key lacks a scope
{"error":{"code":"scope_denied","message":"the session lacks the runs_write scope",...}}. The message names the missing scope.Why. Every route requires one scope, and the credential does not hold it. POST /v1/runs needs runs_write; reading or streaming a run needs runs_read; usage and quota need usage_read; GET /v1/billing needs billing_read.Fix. A key’s scopes are fixed when it is created and cannot be edited, so create a new key that holds the scope, then revoke the old one if you no longer need it. You can only give a key scopes that you hold yourself. In the console, the Send requests set gives runs_write and runs_read.Log the status, the code, the scope named in the message and the x-request-id. See scope_denied and Scopes.Credit and quota
402 insufficient_credits: the balance does not cover the hold
402 insufficient_credits: the balance does not cover the hold
balance_micros: credited, minus spent, minus the holds of runs still in flight, so many runs in parallel need more headroom than one.Nothing is stored: the run and its hold are rolled back. If auto-recharge is on, an attempt is also queued in the background.Fix.-
Check the balance on the Billing page of the console, or with
GET /v1/billingand a key that holdsbilling_read: -
Add funds from the console or with
POST /v1/billing/checkout, between 1,000 in whole cents. You finish the payment on a Stripe-hosted checkout page, and the credit arrives once the payment succeeds, not in the checkout response. -
Retry with the same
Idempotency-Key. The refused request never used it. - Turn on auto-recharge so a low balance tops itself up. It charges at most once per organization per clock hour.
x-request-id, and alert on this code rather than retrying in a loop. See insufficient_credits and Handling insufficient credits.429 organization_spend_quota_exhausted or organization_token_quota_exhausted
429 organization_spend_quota_exhausted or organization_token_quota_exhausted
POST /v1/runs is the only route that answers 429. Your organization has a spend limit, a token limit, or both, for the current period, which is the current UTC calendar month.- Spend: the check compares the run’s per-request ceiling, at most 20,000 micro-USD, with what is left of the period’s spend limit. It does not use the run’s actual cost, so refusals begin once less than 20,000 micro-USD remain, even though a typical run costs far less.
- Tokens: the estimated input tokens plus
max_output_tokensexceed the tokens left in the period.
GET /v1/quota (scope usage_read):null limit means there is none. For the token code, lower max_output_tokens or trim the input. For the spend code, the remaining spend also counts the holds of runs still in flight, so it recovers a little as those runs settle; otherwise it resets when the next period begins at 00:00:00Z on the first of the next month. Back off instead of retrying in a loop. The refused request never used its Idempotency-Key, so you can reuse it.Log the status, the code, the numbers in the message and the x-request-id. See organization_spend_quota_exhausted, organization_token_quota_exhausted and Spend limits and quotas.Runs that are accepted, then fail
These errors come back after the run was admitted, so itsIdempotency-Key is now tied to a run that stays pending. Retry them with a new key; see a same-key retry stuck at 202.
503 no_route_available on a verdict run
503 no_route_available on a verdict run
{"error":{"code":"no_route_available","message":"no eligible route is available for this request",...}} on a request without "kind": "decision".Why. Neon 1.1 serves decision runs only. A verdict run, which is also what you get when kind is absent, has no model to route to, so it fails every time. Retrying does not help.Fix. Ask the same thing as a decision run. A verdict schema with an enum usually maps to one choice question:Idempotency-Key. The answer’s choice is the most likely intent, and probabilities gives every option.Log the status, the code and the x-request-id. See no_route_available and Verdict runs.413 input_too_large: the input is over 262,144 tokens
413 input_too_large: the input is over 262,144 tokens
413 with code input_too_large.Why. Every run gets an input estimate: ceil(prompt bytes / 4) + ceil(contract bytes / 4). For a decision run the prompt is the state, a string as-is or anything else as its JSON text, and the contract is instructions, questions, draws and think_tokens. There are two ceilings:"the input estimate of N tokens exceeds the ceiling".Fix. Send only the fields of state that your questions need, shorten instructions and descriptions, and split a large question set across several runs. Send the smaller request with a new Idempotency-Key, since the body has changed. The request body itself is capped separately at 4 MiB, which gives 413 body_too_large.Log the status, the code, the size of the state you sent and the x-request-id. See input_too_large and Limits.504 deadline_exceeded: the run ran out of time
504 deadline_exceeded: the run ran out of time
504 with code deadline_exceeded. The message may say “before a route was chosen” even when the model call was the slow part, so rely on the code.Why. Each run has a deadline, deadline_ms. For a decision run it defaults to 30,000 plus 120,000 per 262,144 input tokens and is clamped to 1 to 150,000; for a verdict run it defaults to 30,000 and is clamped to 1 to 120,000. The deadline passed during routing or during the model call.Fix. Raise deadline_ms, up to 150,000 for a decision run, or shrink the input, then retry with backoff and a new Idempotency-Key. Set your HTTP client’s timeout to at least 160 seconds so you receive the 504 instead of cutting the connection yourself.Log the status, the code, the deadline_ms you sent and the x-request-id. See deadline_exceeded.Retries and idempotency
A same-key retry keeps returning 202 with state pending
A same-key retry keeps returning 202 with state pending
POST /v1/runs with the same Idempotency-Key and body, and every attempt answers 202:POST /v1/runs is synchronous: a fresh run returns 200 once it has settled. A 202 is a replay of a stored run that is not finished. When a run fails before any model served it (no_route_available, decision_unavailable, a context input_too_large, deadline_exceeded, budget_exhausted, or a first-attempt provider failure), its hold is released but its state is never moved: it stays pending for good, and so does every replay of that key.Fix. Send the request again with a new Idempotency-Key. Do not poll the pending run: it will not complete. A replay is never charged, so the 202s cost nothing.If you retried while the first request might still have been in flight, wait until its deadline has passed (at most 150 s) and replay once more. A 200 then carries the answer with "replayed": true and is not charged again; a run still pending after its deadline is stuck.Log the Idempotency-Key, the run_id from the 202 body and the x-request-id of every attempt. See Polling and Idempotency.409 idempotency_conflict: the key was used with another body
409 idempotency_conflict: the key was used with another body
state, or system and messages), the kind, and the contract (instructions, questions, draws and think_tokens, or the verdict schema). Keys never expire, so a key reused months later still conflicts.max_output_tokens, deadline_ms and capability_hint are not part of the identity. Changing only those and reusing the key does not conflict: it returns the stored run, not a new one.Fix. If this is a retry, resend the original body unchanged. If it is a new request, use a new key. Keys derived from your own business ids work well, with a version when the question changes: ticket-4822-triage-v2. The refused request created nothing and was not charged.Log the Idempotency-Key, the code and the x-request-id. See idempotency_conflict and Idempotency.Request format
Plain-text 400, 413, 415 or 422 on keys, usage or billing routes
Plain-text 400, 413, 415 or 422 on keys, usage or billing routes
text/plain body instead of the JSON envelope, for example 422 with Failed to deserialize the JSON body into the target type: ....Why. The keys, usage, quota and billing routes read JSON, query and path values with the web framework’s own extractors, which answer in plain text. Every request body refuses fields it does not define:Content-Type: application/json, valid JSON, and only the documented fields with the documented types. Branch on the HTTP status when the body is not JSON. On POST /v1/runs the same mistakes come back as JSON 400 invalid_body instead.Log the status, the first line of the body and the x-request-id header: these bodies carry no request_id. See Errors.400 invalid_body on POST /v1/runs
400 invalid_body on POST /v1/runs
{"error":{"code":"invalid_body","message":"the request body is not valid: state is only valid on a decision run",...}}. Every message starts with the request body is not valid: .Why. The body does not fit the contract. The most common causes:"kind": "decision"is missing.kinddefaults toverdict, so the body is checked as a verdict run and a decision-only field such asstateis refused.- A field the run does not define, or a value of the wrong type:
the body does not match the schema. max_output_tokensis0:max_output_tokens must be greater than zero.- No
Content-Type: application/jsonheader, or malformed JSON.
Idempotency-Key is fine.Log the status, the full message and the x-request-id. See invalid_body.400 unknown_model: the model id is not recognised
400 unknown_model: the model id is not recognised
model field on a decision run accepts neon-1.1 or neon-latest. Any other value is refused.Fix. Send one of those two ids, or leave model out. Live decision responses report decision.model as neon-1.1.Log the status, the code and the x-request-id. See unknown_model and Models and pricing.When to retry
Responses carry no retry hint, in the body or in the headers. Decide from the status and the code:Related
- Problem codes - every code, its status and the fix, one page each.
- Errors - the error envelope, plain-text rejections and how to branch on
code. - Runs - run states, replays, and what a stored run returns.
- Error handling - a retry helper that picks the right idempotency key for you.
- Frequently asked questions - short answers on cost, limits, keys and timeouts.