Reference
Errors
The error envelope, every status code with its type and codes, which errors to retry, refunds, and the SDK error classes.
Every error is a JSON body with a stable type and code. The type tells you the class of problem and maps one to one to the HTTP status. The code tells you the exact reason. Errors before admission never charge your balance, and server errors after admission are refunded.
The error envelope
{
"error": {
"type": "validation",
"code": "state_path_not_found",
"message": "Question 'spoed' refers to {{bericht.txt}}, which is not in the state.",
"param": "questions.spoed.instructions",
"request_id": "req_01M5CJXHG0M9S346Q3D25VT4F5"
}
}
| Field | Always present | Meaning |
|---|---|---|
type |
yes | The class of error. Stable, and one to one with the HTTP status. |
code |
yes | The machine-readable reason. Stable. New codes may be added within v1, so handle unknown codes by their type. |
message |
yes | English text for people. Do not parse it. |
param |
no | The input field that caused the error, as a dotted path, when one field did. |
request_id |
yes | The request id, the same as the x-request-id header. Quote it to support. |
balance_micro_cents |
402 only | Your balance when the request arrived. |
required_micro_cents |
402 only | What the request would have cost. |
Status codes
| HTTP | type |
Codes | Retry |
|---|---|---|---|
| 400 | invalid_request |
invalid_json, duplicate_key, unsupported_media_type, invalid_header |
No |
| 401 | authentication |
missing_api_key, invalid_api_key, revoked_api_key, expired_api_key, invalid_session, invalid_code, code_attempts_exceeded |
No |
| 402 | insufficient_balance |
insufficient_balance |
After a top-up |
| 403 | permission |
insufficient_scope, account_suspended, csrf_failed |
No |
| 404 | not_found |
model_not_found, model_retired, route_not_found, key_not_found |
No |
| 405 | method_not_allowed |
method_not_allowed |
No |
| 409 | conflict |
idempotency_key_reused, idempotency_in_progress |
idempotency_in_progress only, after retry-after |
| 413 | too_large |
body_too_large, state_too_long, question_too_long, request_too_long |
No |
| 422 | validation |
unknown_field, missing_field, invalid_type, invalid_value, field_not_allowed, invalid_question_id, too_many_questions, too_many_options, invalid_levels, invalid_min_confidence, duplicate_label, state_path_not_found, state_not_json, top_up_limit_exceeded |
No |
| 429 | rate_limited |
requests_per_minute, tokens_per_minute, concurrency, test_daily_quota, email_codes_per_hour |
Yes, after retry-after |
| 500 | internal |
internal_error |
Yes, with the same idempotency key |
| 503 | unavailable |
no_capacity, upstream_unavailable, billing_unavailable, maintenance |
Yes, after retry-after |
Codes in detail
| Code | Status | When |
|---|---|---|
invalid_json |
400 | The body is empty, not UTF-8 or not JSON. This includes a \u escape that is half of a surrogate pair, such as "\ud800" without the low half that must follow it. A pair such as "\ud83d\ude00", one emoji, is fine. |
duplicate_key |
400 | An object in the body repeats a key, including a label repeated character for character in options. param is the JSON pointer of the repeated member, for example /questions/afdeling/options/garantie. |
unsupported_media_type |
400 | A request with a body did not send content-type: application/json. |
invalid_header |
400 | idempotency-key or x-client-request-id is empty, too long or not printable ASCII. param names the header. |
missing_api_key |
401 | No authorization header. |
invalid_api_key |
401 | The key is not a valid key. |
revoked_api_key |
401 | The key was revoked. Create a new one in the console. |
expired_api_key |
401 | The key passed its expiry date. |
invalid_session |
401 | Console only: the session cookie is missing or no longer valid. |
invalid_code |
401 | Console sign-in only: the code is wrong or has expired. Check the newest email, or ask for a new code. |
code_attempts_exceeded |
401 | Console sign-in only: the code had 5 wrong tries and no longer works, even with the right digits. Ask for a new code. |
insufficient_balance |
402 | The balance does not cover this request. Nothing was charged. |
insufficient_scope |
403 | The key lacks the scope for this route, such as usage:read. |
account_suspended |
403 | The account is suspended. |
csrf_failed |
403 | Console only: the x-dex-csrf header is missing or wrong. |
model_not_found |
404 | No model has this name. See GET /v1/models. |
model_retired |
404 | This exact version has been retired. |
route_not_found |
404 | No such route. |
key_not_found |
404 | No key with this id in your account. |
method_not_allowed |
405 | The path exists, but not with this method, for example GET /v1/decide. The allow header lists the methods it has. |
idempotency_key_reused |
409 | This idempotency key was used in the last 24 hours with a different request body. |
idempotency_in_progress |
409 | The first request with this idempotency key is still running. |
body_too_large |
413 | The body is over 256 KiB. |
state_too_long |
413 | The rendered state is over 16,384 tokens. |
question_too_long |
413 | A rendered question is over 4,096 tokens. |
request_too_long |
413 | The request is over 16,384 billable tokens in total, or its questions together are over 4,096 (then param is questions). |
unknown_field |
422 | A field the API does not define, for example the typo questons. |
field_not_allowed |
422 | A field of another question type: options outside a pick, levels outside a rate. |
missing_field |
422 | A required field is absent, for example questions, a question's type or instructions, or the start query parameter of getUsage. |
invalid_type |
422 | A value has the wrong JSON type: a number for instructions, an array for questions, a number for the state. |
invalid_value |
422 | The type is right but the value is not allowed: an empty or over-long string (the state, instructions, criteria, a label, a description), a state, instructions, criteria or description of only whitespace, an empty questions or options object, an empty state object or array, a string outside its allowed values or pattern (model, fallback, an unknown question type, a label or a rate level with spaces at either end), a JSON state nested deeper than 32 levels or with an empty or over-long key. For a level, param names the level, such as questions.spoed.levels.2. On getUsage: a malformed date, granularity or key_id, an end not after start, or a range longer than the granularity allows. |
invalid_question_id |
422 | A question id does not match ^[A-Za-z][A-Za-z0-9_-]{0,63}$. |
too_many_questions |
422 | More than 32 questions. |
too_many_options |
422 | A pick has more than 255 options. |
invalid_levels |
422 | levels is not 2 to 10 unique strings of 1 to 200 characters. Levels must differ by more than case or Unicode normalization: Low and low count as the same level. |
invalid_min_confidence |
422 | min_confidence is not a number from 0 to 1. |
duplicate_label |
422 | Two pick labels differ only in case or Unicode normalization, such as Billing and billing, or a precomposed and a combining é. A label repeated character for character is duplicate_key (400). |
state_path_not_found |
422 | A {{path}} reference names a field the state does not have. |
state_not_json |
422 | A {{path}} reference appears while the state is a string. |
top_up_limit_exceeded |
422 | Console only: a top-up would take a new account past EUR 500 in its first 30 days. |
requests_per_minute |
429 | The requests-per-minute limit is used up. |
tokens_per_minute |
429 | The billable-tokens-per-minute limit is used up. |
concurrency |
429 | Too many requests are in flight at once. |
test_daily_quota |
429 | The account used its free test tokens for today. retry-after is the time until they reset at 00:00 UTC. The SDKs do not retry it. |
email_codes_per_hour |
429 | Console sign-in only: this address was sent 5 codes in the last hour, or too many codes were asked for from one network. retry-after is the real wait, up to an hour. |
internal_error |
500 | Something went wrong on our side. Any charge was refunded. |
no_capacity |
503 | No GPU capacity, and the fallback is not allowed or not available for this request. |
upstream_unavailable |
503 | The GPU path and the fallback both failed. |
billing_unavailable |
503 | The balance store could not be reached. |
maintenance |
503 | The service is in maintenance. |
Which error comes first
Dex checks a request in this order and stops at the first failure:
- Authentication (401), then scope (403).
- Body size (413
body_too_large), content type (400unsupported_media_type), then JSON parsing (400invalid_json). - Duplicate keys (400
duplicate_key), then the schema (422, with a code per rule). - The model name (404).
- Field references, and label and level uniqueness ignoring case (422).
- Rendering and token limits (413).
- Idempotency (409).
- Rate limits and quotas (429).
- Balance (402).
- Dispatch to a serving path (503 or 500).
Steps 1 to 9 never charge your balance.
When a body breaks several schema rules at once, the most specific code wins, in this order: unknown_field, field_not_allowed, missing_field, invalid_question_id, too_many_questions, too_many_options, invalid_levels, invalid_min_confidence, invalid_value, invalid_type. For a 422, param is a dotted path into the body, such as questions.spoed.instructions, or the name of a query parameter. For duplicate_key, param is a JSON pointer.
New codes can appear within v1 under an existing type. Act on the type, and treat an unknown code like the other codes of its type.
Retrying
- 429, 503 and 409
idempotency_in_progressalways carry aretry-afterheader in whole seconds, from 1 to 60. Wait that long, then retry. Two codes wait longer:test_daily_quotagives the time until 00:00 UTC, and the console's sign-in code limit gives the real wait, up to 3,600 seconds. - 500 is safe to retry with the same idempotency key.
- 402 succeeds after you top up.
- Every other 4xx will fail again. Fix the request instead.
- Send an
idempotency-keyonPOST /v1/decideso a retry can never charge twice. See Idempotency. - Back off when there is no
retry-after, for example after a network error. The SDKs retry 429 (but nottest_daily_quota), 500, 502, 503, 504, 409idempotency_in_progress, timeouts and network errors, with full-jitter exponential backoff from a 250 ms base, at most 3 retries. - Watch for duplicate keys if you build JSON by hand or merge objects. A repeated key is a 400
duplicate_key, never silently the last value.
Refunds
- Errors before admission (all 4xx) never charge. A 402 means nothing was charged.
- A request that fails with 500 or 503 after the charge is refunded in full before the response is sent. The tokens it took from your tokens-per-minute limit and, for a test key, from the daily test quota are given back too.
- A replay of a completed request with the same idempotency key is never charged. It returns the original response, including the original
usage.
SDK error classes
Until the SDKs are on PyPI and npm, install them from Downloads.
Each SDK raises one class per error type. The class comes from error.type in the body, or from the HTTP status when the body cannot be read.
type |
HTTP | Class |
|---|---|---|
invalid_request |
400 | InvalidRequestError |
authentication |
401 | AuthenticationError |
insufficient_balance |
402 | InsufficientBalanceError, with balance_micro_cents and required_micro_cents |
permission |
403 | PermissionDeniedError |
not_found |
404 | NotFoundError |
conflict |
409 | ConflictError |
too_large |
413 | TooLargeError |
validation |
422 | RequestValidationError |
rate_limited |
429 | RateLimitError |
internal |
500 | InternalServerError |
unavailable |
503 | UnavailableError |
All of these extend APIStatusError, which carries status, type, code, message, param, request_id, retry_after, idempotency_key and the response headers. An unknown type raises a plain APIStatusError, and so does a 405 method_not_allowed, which no SDK call can cause.
Errors without an HTTP answer have their own classes: APIConnectionError (with APITimeoutError under it), ResponseParseError for a body that is not valid JSON, and ConfigurationError, for example when no API key is set. The TypeScript SDK adds AbortedError when your AbortSignal stops a call. Every class extends DexError.
from thinqit_dex import RequestValidationError, InsufficientBalanceError
try:
decision = client.decide(state=state, questions=questions)
except RequestValidationError as err:
print(err.code, err.param, err.request_id)
except InsufficientBalanceError as err:
print(err.required_micro_cents - err.balance_micro_cents, "micro-cents short")import { RequestValidationError, InsufficientBalanceError } from "@thinqit/dex";
try {
const decision = await client.decide({ state, questions });
} catch (err) {
if (err instanceof RequestValidationError) {
console.log(err.code, err.param);
} else if (err instanceof InsufficientBalanceError) {
console.log("Top up in the console:", err.message);
} else {
throw err;
}
}Getting help
Every response and every error has a request id: req_ followed by 26 characters. Quote it when you contact support. Support asks for the request id, never for the content of your request.