Errors
The error envelope, every error type and message, and the headers that come with them.
Every error from https://console.nace.ai is a JSON body with one shape and an HTTP status that names the error class. For how to retry, see Errors and retries.
Error envelope
{
"error": {
"type": "invalid_request_error",
"message": "questions.a.type: must be \"noul\", \"choice\", or \"score\"",
"issues": [
{ "path": "questions.a.type", "message": "must be \"noul\", \"choice\", or \"score\"" }
]
},
"request_id": "req_0123456789abcdef0123456789abcdef"
}| Field | Type | Present | Meaning |
|---|---|---|---|
error.type | string | always | One of the six types in the table below. The HTTP status is a function of this field. |
error.message | string | always | Human-readable description. Each type's messages are listed below. |
error.issues | array | invalid_request_error only | Every validation problem found, as { "path", "message" } objects. The first issue is also the source of error.message. |
request_id | string | always | req_ followed by 32 hex characters. Same value as the x-request-id header. Quote it in support requests. |
Error types
| Type | HTTP | When | Retryable |
|---|---|---|---|
authentication_error | 401 | Missing or invalid Bearer API key. | no |
insufficient_credit | 402 | The account has no spendable credit. | no |
payment_required | 402 | The account has an unpaid invoice. | no |
invalid_request_error | 422 | The request body failed validation or exceeded model limits. | no |
rate_limit_error | 429 | Account RPM or in-flight concurrency was exceeded. | yes |
internal_error | 500 | An unexpected server failure occurred. | yes |
overloaded | 529 | Drex is at capacity, paused, or its rate limiter is down, or the model timed out or is unavailable. | yes |
Response headers
| Header | Responses | Value |
|---|---|---|
x-request-id | all | The request id, identical to request_id in the body. |
x-typesafe-request-id | all | The same request id, under the name TypeSafe SDK clients read. |
access-control-expose-headers | all | retry-after, retry-after-ms, x-request-id, x-typesafe-request-id, so browsers can read the other four. |
retry-after | 429, 529 | Whole seconds to wait. Drex divides retry-after-ms by 1000 and rounds up. |
retry-after-ms | 429, 529 | Milliseconds to wait. The exact value the limiter computed. |
server-timing | 200 on POST /v1/systemone | Per-phase timings for the request. |
Order of checks
POST /v1/systemone runs these checks in order and stops at the first failure.
- Kill switch.
529 overloaded. - API key.
401 authentication_error, then402 payment_requiredor402 insufficient_credit. - Rate limits.
429 rate_limit_error, or529 overloadedfor shared capacity. - Body parsing and validation.
422 invalid_request_error. - Model evaluation.
422 invalid_request_errorfor a request over the token limit,529 overloadedwhen the model service does not answer. - Anything unexpected.
500 internal_error.
A request that reaches step 4 has already used a slot in the rate limiter, so a request that fails validation still counts toward the per-minute limit. See Limits.
GET /v1/models runs steps 1 and 2 only, and step 2 stops after the key check.
authentication_error (401)
One message:
Invalid API key. Pass a nace_sk_ key as `Authorization: Bearer <key>`.Returned by both endpoints when any of these is true:
- The
Authorizationheader is missing. - The header is not exactly
Bearerfollowed bynace_sk_and 43 URL-safe characters (A-Z,a-z,0-9,_,-). - The key does not exist.
- The key was revoked. A revoked key can keep working for up to 5 seconds on an instance that had it cached.
Real response, captured against production with no key:
HTTP/2 401
access-control-expose-headers: retry-after, retry-after-ms, x-request-id, x-typesafe-request-id
content-type: application/json
x-request-id: req_bd2dffad68f5f5fa4ce702e9bd516fb4
x-typesafe-request-id: req_bd2dffad68f5f5fa4ce702e9bd516fb4
{"error":{"type":"authentication_error","message":"Invalid API key. Pass a nace_sk_ key as `Authorization: Bearer <key>`."},"request_id":"req_bd2dffad68f5f5fa4ce702e9bd516fb4"}The dashboard Playground uses the same type with its own message, Sign in to use the playground., when the browser session has ended.
insufficient_credit (402)
One message:
This account has no spendable credit. Top up to keep going.Returned by POST /v1/systemone when the account's spendable credit is zero and no card pays for usage beyond it. Spendable credit is the sum of what remains on grants that have not expired. Any positive amount admits the request, so the last request before the balance reaches zero still runs. An account with a saved card and auto-pay on is never refused for lack of credit. GET /v1/models does not check credit and keeps working. Top up or add a card on the dashboard Billing page. See Pricing.
payment_required (402)
One message:
This account has an unpaid invoice. Pay it on the Billing page to keep going.Returned by POST /v1/systemone, the Playground, and the document routes when a charge to the account's card failed, or an invoice is still unpaid at the end of the day it was issued. The account is paused: requests that spend credit are refused, and reads keep working, including GET /v1/models. The response is not retryable until the invoice is paid. Pay it with the Pay button on the dashboard Billing page. The pause lifts as soon as Stripe reports the invoice as paid. See Pricing.
invalid_request_error (422)
error.message is the first issue, written as path: message, or the bare message when the path is empty. error.issues lists every problem the validator found in one pass. Paths are dotted: questions.<id>, questions.<id>.criteria.<key>, questions.<id>.criteria.<index>.
Every issue Drex itself produces:
| Path | Message | When |
|---|---|---|
| (empty) | body must be JSON | The body does not parse as JSON. |
| (empty) | body must be a JSON object | The body parses, but is an array, string, number, boolean, or null. |
model | unknown model "jev-latest"; use "drex-v1.0", "drex-v1.5" or "drex-latest" | model is present and does not match drex-* or nacedm-*. The quoted value is whatever was sent. |
state | is required | The state key is absent. Any JSON value, including null, satisfies it. |
questions | must be an object | questions is missing or not an object. |
questions | must contain at least one question | questions is {}. |
questions | at most 512 questions allowed | More entries than the limit in Limits. |
questions | question ids must be non-empty | A key is empty or whitespace only. |
questions.<id> | must be an object | The question is not an object. |
questions.<id>.type | must be "noul", "choice", or "score" | Unknown or missing type. No further checks run on that question. |
questions.<id>.instructions | must be a string | instructions is present and is not a string. null and objects are rejected. |
questions.<id>.instructions | noul needs instructions or criteria | A noul question has neither a non-empty instructions string nor a non-empty criteria.true or criteria.false. |
questions.<id>.criteria | must be an object | A noul question's criteria is not an object. |
questions.<id>.criteria.true, .false | must be a string | A noul criterion is present, not null, and not a string. Other keys under criteria are ignored. |
questions.<id>.criteria | must be an object with options | A choice question's criteria is missing or not an object. |
questions.<id>.criteria | choice needs at least 1 option | A choice question's criteria is {}. |
questions.<id>.criteria.<label> | must be a string or null | A choice option description is neither. |
questions.<id>.criteria | must be an array of level labels | A score question's criteria is missing or not an array. |
questions.<id>.criteria | score needs at least 1 level | A score question's criteria is []. |
questions.<id>.criteria.<index> | must be a string | A score level label is not a string. |
state... or questions... | is media (<kind>); <model> reads text only | The field holds an image, audio, video or PDF: a base64 data URL, a raw base64 file, or an OpenAI, Anthropic or Gemini media content part. <kind> names which, for example image/png data URL, and <model> is the version that would have answered. At most 10 per field. See State. |
| (empty) | request body must be at most 1048576 bytes | Every check above passed, and state plus questions, serialized as JSON, is larger than the limit in UTF-8 bytes. This is the only issue in the array. |
state | exceeds the 131,072-token state limit | The state alone is over the model's state limit; the number is the model's. error.message is Request too long: the state exceeds the 131,072-token state limit of drex-v1.5. Shorten the state. |
state | exceeds the 139,264-token limit | The state plus the longest question is over the model's row limit; the number is the model's. error.message is Request too long: the state plus its longest question exceed the 139,264-token limit of drex-v1.5. Shorten the state or that question. |
Two more sources exist and are rare. If the model service rejects a request that passed every check above, Drex forwards the service's own validation messages as issues, or invalid request when the service's detail cannot be read. If the validator somehow finishes with no issue to report, error.message is Invalid request..
Example outputs, all real captures. Only request_id is illustrative.
A TypeSafe client that still sends its default model:
{
"error": {
"type": "invalid_request_error",
"message": "model: unknown model \"jev-latest\"; use \"drex-v1.0\", \"drex-v1.5\" or \"drex-latest\"",
"issues": [
{ "path": "model", "message": "unknown model \"jev-latest\"; use \"drex-v1.0\", \"drex-v1.5\" or \"drex-latest\"" }
]
},
"request_id": "req_0123456789abcdef0123456789abcdef"
}One field, two issues. instructions was an object, which fails the type check and also leaves the noul question without instructions or criteria:
{
"error": {
"type": "invalid_request_error",
"message": "questions.a.instructions: must be a string",
"issues": [
{ "path": "questions.a.instructions", "message": "must be a string" },
{ "path": "questions.a.instructions", "message": "noul needs instructions or criteria" }
]
},
"request_id": "req_0123456789abcdef0123456789abcdef"
}A state of about 230 KB sent to drex-v1.0, under the byte limit but over its row limit:
{
"error": {
"type": "invalid_request_error",
"message": "Request too long: the state plus its longest question exceed the 32,768-token limit of drex-v1.0. Shorten the state or that question.",
"issues": [
{ "path": "state", "message": "exceeds the 32,768-token limit" }
]
},
"request_id": "req_0123456789abcdef0123456789abcdef"
}A state of about 1.2 MB, rejected before the model service sees it:
{
"error": {
"type": "invalid_request_error",
"message": "request body must be at most 1048576 bytes",
"issues": [
{ "path": "", "message": "request body must be at most 1048576 bytes" }
]
},
"request_id": "req_0123456789abcdef0123456789abcdef"
}rate_limit_error (429)
Two messages. Both carry retry-after and retry-after-ms.
| Message | When | retry-after-ms |
|---|---|---|
Requests per minute exceeded for this account. | The account was admitted its full per-minute quota within the last 60 seconds. | Time until the oldest counted request leaves the 60-second window, and never less than 1000. |
Too many concurrent requests for this account. | The account already has its maximum number of requests in flight. | 1000. |
The quotas per tier are in Limits. Both limits are per account, so every API key on the account and the dashboard Playground draw from the same counters.
overloaded (529)
Three messages. All carry retry-after and retry-after-ms. None of them is caused by the request body, so the same request can succeed on retry.
| Message | When | retry-after-ms |
|---|---|---|
Drex is temporarily unavailable. Retry after 30 seconds. | The operators have paused the API. Both endpoints return this. | 30000 |
Drex is at capacity. Retry shortly. | The shared in-flight pool for the request's model fleet is full, the free-tier pool is full, or the rate limiter itself could not be reached. | 2000 |
Drex is temporarily unavailable. Retry shortly. | The model service did not answer within 55 seconds, answered with an error, rejected Drex's own credentials, or reported that it was overloaded. | 2000 |
The response never says which of these upstream conditions occurred and never includes the model service's status code or body. Drex logs that detail under the request_id.
internal_error (500)
One message:
Something went wrong on our side.An exception that no other branch handled. Retry once, then send the request_id to support@nace.ai. No credit is debited for a request that returns 500, or for any other error response.
Document routes
The /v1/documents/* routes use the same envelope with two more fields, error.code (the exact cause) and error.retryable, and three more types: not_found_error (404, 410), conflict_error (409) and service_unavailable (503). The full table is in Perception errors.