Sign-up and API keys are open. Paid top-ups open soon.What changed
Docs menu

Reference

Errors

The error envelope, every status code with its type and codes, which errors to retry, refunds, and the SDK error classes.

View as Markdown

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:

  1. Authentication (401), then scope (403).
  2. Body size (413 body_too_large), content type (400 unsupported_media_type), then JSON parsing (400 invalid_json).
  3. Duplicate keys (400 duplicate_key), then the schema (422, with a code per rule).
  4. The model name (404).
  5. Field references, and label and level uniqueness ignoring case (422).
  6. Rendering and token limits (413).
  7. Idempotency (409).
  8. Rate limits and quotas (429).
  9. Balance (402).
  10. 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_progress always carry a retry-after header in whole seconds, from 1 to 60. Wait that long, then retry. Two codes wait longer: test_daily_quota gives 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-key on POST /v1/decide so 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 not test_daily_quota), 500, 502, 503, 504, 409 idempotency_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.