DrexDocs

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"
}
FieldTypePresentMeaning
error.typestringalwaysOne of the six types in the table below. The HTTP status is a function of this field.
error.messagestringalwaysHuman-readable description. Each type's messages are listed below.
error.issuesarrayinvalid_request_error onlyEvery validation problem found, as { "path", "message" } objects. The first issue is also the source of error.message.
request_idstringalwaysreq_ followed by 32 hex characters. Same value as the x-request-id header. Quote it in support requests.

Error types

TypeHTTPWhenRetryable
authentication_error401Missing or invalid Bearer API key.no
insufficient_credit402The account has no spendable credit.no
payment_required402The account has an unpaid invoice.no
invalid_request_error422The request body failed validation or exceeded model limits.no
rate_limit_error429Account RPM or in-flight concurrency was exceeded.yes
internal_error500An unexpected server failure occurred.yes
overloaded529Drex is at capacity, paused, or its rate limiter is down, or the model timed out or is unavailable.yes

Response headers

HeaderResponsesValue
x-request-idallThe request id, identical to request_id in the body.
x-typesafe-request-idallThe same request id, under the name TypeSafe SDK clients read.
access-control-expose-headersallretry-after, retry-after-ms, x-request-id, x-typesafe-request-id, so browsers can read the other four.
retry-after429, 529Whole seconds to wait. Drex divides retry-after-ms by 1000 and rounds up.
retry-after-ms429, 529Milliseconds to wait. The exact value the limiter computed.
server-timing200 on POST /v1/systemonePer-phase timings for the request.

Order of checks

POST /v1/systemone runs these checks in order and stops at the first failure.

  1. Kill switch. 529 overloaded.
  2. API key. 401 authentication_error, then 402 payment_required or 402 insufficient_credit.
  3. Rate limits. 429 rate_limit_error, or 529 overloaded for shared capacity.
  4. Body parsing and validation. 422 invalid_request_error.
  5. Model evaluation. 422 invalid_request_error for a request over the token limit, 529 overloaded when the model service does not answer.
  6. 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 Authorization header is missing.
  • The header is not exactly Bearer followed by nace_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:

PathMessageWhen
(empty)body must be JSONThe body does not parse as JSON.
(empty)body must be a JSON objectThe body parses, but is an array, string, number, boolean, or null.
modelunknown 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.
stateis requiredThe state key is absent. Any JSON value, including null, satisfies it.
questionsmust be an objectquestions is missing or not an object.
questionsmust contain at least one questionquestions is {}.
questionsat most 512 questions allowedMore entries than the limit in Limits.
questionsquestion ids must be non-emptyA key is empty or whitespace only.
questions.<id>must be an objectThe question is not an object.
questions.<id>.typemust be "noul", "choice", or "score"Unknown or missing type. No further checks run on that question.
questions.<id>.instructionsmust be a stringinstructions is present and is not a string. null and objects are rejected.
questions.<id>.instructionsnoul needs instructions or criteriaA noul question has neither a non-empty instructions string nor a non-empty criteria.true or criteria.false.
questions.<id>.criteriamust be an objectA noul question's criteria is not an object.
questions.<id>.criteria.true, .falsemust be a stringA noul criterion is present, not null, and not a string. Other keys under criteria are ignored.
questions.<id>.criteriamust be an object with optionsA choice question's criteria is missing or not an object.
questions.<id>.criteriachoice needs at least 1 optionA choice question's criteria is {}.
questions.<id>.criteria.<label>must be a string or nullA choice option description is neither.
questions.<id>.criteriamust be an array of level labelsA score question's criteria is missing or not an array.
questions.<id>.criteriascore needs at least 1 levelA score question's criteria is [].
questions.<id>.criteria.<index>must be a stringA score level label is not a string.
state... or questions...is media (<kind>); <model> reads text onlyThe 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 bytesEvery 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.
stateexceeds the 131,072-token state limitThe 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.
stateexceeds the 139,264-token limitThe 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.

MessageWhenretry-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.

MessageWhenretry-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.

On this page