Perception reference
Errors
The error envelope, statuses, codes and rate limits of the document routes.
The document routes (/v1/documents/* and /v1/extraction-schemas/*) and the Workspaces API (/v1/workspaces/*) use Drex's error envelope with a few extra fields:
{
"error": {
"type": "invalid_request_error",
"code": "invalid_source",
"message": "source.type must be \"url\", \"parse_result\" or \"workspace_file\"",
"retryable": false
},
"request_id": "req_0123456789abcdef0123456789abcdef"
}| Field | What it holds |
|---|---|
type | The class of error. Branch on this. |
code | The exact cause: Drex's own code, or the document service's when it refused the request, such as invalid_request, unsupported_file_type or schema_not_found. |
retryable | Whether the same request can succeed later. Retry creates with the same Idempotency-Key. |
detail | Present with the document service's validation errors and conflicts. detail.errors lists the fields that failed. |
These routes send no issues. Responses from Drex carry their request id in the x-request-id header. Error bodies repeat it as request_id; a successful job body doesn't. A request rejected by the hosting platform before it reaches Drex, such as an oversized body above about 4.5 MB, can return 413 without this JSON envelope or a request id. Handle the HTTP status even when those fields are absent.
Statuses and codes
| Status | type | code | When |
|---|---|---|---|
| 400, 422 | invalid_request_error | invalid_request | The body isn't JSON, or the document service refused it. |
| 413 | invalid_request_error when present | payload_too_large when present | The body is too large. A platform rejection can omit the envelope and request id. |
| 415 | invalid_request_error | unsupported_media_type | A workspace file replace that isn't multipart. |
| 422 | invalid_request_error | invalid_source | The source is missing or isn't one of the allowed kinds. |
| 422 | invalid_request_error | unsupported_file_type | The tool doesn't read this type of file. |
| 422 | invalid_request_error | wait_required_for_zero_retention | A create without wait_seconds from a zero-retention account. See Zero retention. |
| 401 | authentication_error | invalid_api_key | The API key is missing or invalid. |
| 402 | insufficient_credit | A create while the balance is not positive and no card pays for usage beyond it. | |
| 402 | payment_required | A create while the account has an unpaid invoice. Pay it on the Billing page. Not retryable until it is paid. | |
| 403 | permission_error | access_denied | A workspace delete by someone other than the owner account's owners and admins or the workspace's creator, a search naming a file you aren't allowed to read, or a QA turn in another member's conversation. |
| 404 | not_found_error | not_found | No such job for this account: a job ID (a UUID) the account didn't create. |
| 404 | not_found_error | schema_not_found | No such saved schema. |
| 404 | not_found_error | workspace_not_found | No such workspace. A workspace you can't use, or one that was deleted, is also workspace_not_found. |
| 404 | not_found_error | file_not_found | No such file. |
| 404 | not_found_error | session_not_found | No such QA session. |
| 409 | conflict_error | idempotency_conflict | Idempotency-Key reused with a different body (document tools). |
| 409 | conflict_error | schema_version_conflict | Two versions of one saved schema created at once. |
| 409 | conflict_error | storage_limit_exceeded | The account's managed storage allowance is used up. Tenant-wide usage and allowance are not exposed through workspace-scoped errors. Not retryable. Allowances depend on deployment configuration; there are no default byte limits. Review billing options or contact support@nace.ai for more space; the paid tier follows billing paid-account status, and saving a card alone does not qualify. Deleting files also frees space. Uploads into a workspace shared with you count against its owner's account, and customer-owned storage does not count. A direct upload by grant or session comes from the document service without a type; from 32 MiB the session's completion job fails with this code instead. |
| 409 | conflict_error | session_busy | A QA session is still answering its last turn. |
| 409 | conflict_error | A job's full content couldn't be prepared. | |
| 410 | not_found_error | result_expired | A result or file has expired. On zero retention, a result's download links close about 21 minutes after its job finishes; the file in your own storage is not deleted. |
| 410 | not_found_error | job_unavailable | A job created before 3 October 2026 whose results are no longer available. |
| 429 | rate_limit_error | rate_limited | Over the per-minute limit; over 60 automatic searches and QA turns per account every 10 minutes; over 150 locates; or over 600 file reads, SQL queries, folder listings and catalog queries together. Wait retry-after seconds. |
| 500 | internal_error | Something failed inside Drex. Retry once, then contact support with the request_id. | |
| 502, 504 | upstream_error | ndi_refused_drex | The document service refused Drex's own request. |
| 502, 504 | upstream_error | ndi_redirect | The document service answered a job file with a redirect Drex can't serve. |
| 502, 504 | upstream_error | The document service timed out. Retry later. | |
| 503 | service_unavailable | document_billing_not_configured | Billing is not configured. |
| 503 | service_unavailable | ndi_not_configured | Uploads are not configured on this deployment. |
| 503 | service_unavailable | job_pending | The job's create is not confirmed yet. The job may be running, and is billed once if it ran. Send the same body again with the same key to get it. |
| 503 | service_unavailable | storage_limit_sync_unavailable | The account's storage allowance could not be checked before a workspace create or upload. Retry shortly; reads and deletes are unaffected. |
| 503 | service_unavailable | ndi_unavailable | The document service is down. |
| 503 | service_unavailable | The document service's own code | A 5xx from the document service itself also answers 503. |
| 529 | overloaded | service_disabled | The API is paused. Retry after retry-after seconds. |
A file that can't be read, such as a corrupt_file, is accepted but its job ends failed, with the cause in the job's error.code. See The job object.
In the SDKs
Each status raises its own error class. Each class reads type, code and detail from the envelope, and its retryable as server_retryable (Python) or serverRetryable (TypeScript), None or undefined when the body has none:
| Status | Python | TypeScript |
|---|---|---|
| 400 | NaceBadRequestError | BadRequestError |
| 401 | NaceAuthenticationError | AuthenticationError |
| 402 | NaceInsufficientCreditError | InsufficientCreditError |
| 403 | NacePermissionDeniedError | PermissionDeniedError |
| 404, 410 | NaceNotFoundError | NotFoundError |
| 409 | NaceConflictError | ConflictError |
| 422 | NaceUnprocessableEntityError | UnprocessableEntityError |
| 429 | NaceRateLimitError | RateLimitError |
| 529 | NaceOverloadedError | OverloadedError |
| Other 5xx | NaceInternalServerError | InternalServerError |
| 413, 415 | NaceAPIError | APIError |
All of them are NaceAPIError (Python) or APIError (TypeScript). A few cases need a closer look:
- A path that's taken: an upload under 32 MiB to a path that's taken raises the
409class with codepath_conflict. A larger upload, sent in parts, fails its completion job with that code instead, and the SDK raises a plainNaceErrorwhose message names it. See Uploads. - Mistakes caught before sending: the SDKs refuse these with a plain
NaceError: a string source that isn't anhttps://URL, aurlsource with nofile_namewhose URL doesn't end in a file extension,wait_secondsoutside 0 to 60, and an emptyIdempotency-Keyor one over 200 characters. The SDKs inferfile_namefrom a URL extension when you omit it. For raw HTTP, includesource.file_nameexplicitly, even if the URL ends in.pdf. - Waiting for a job:
jobs.waitraises its own errors for a failed job and for a wait that ran out. See Read a job. - After a
503 job_pending: the error carries theIdempotency-Keythe request sent, asidempotency_keyin Python andidempotencyKeyin TypeScript. Send the same body with it to resume the job. - In the CLI: it prints the status,
type,code, message andrequest_id, then up to five of the fields that failed, and exits1. Onjob_pendingit also prints the key to rerun with as--idempotency-key.
For more on handling errors, see the Python, TypeScript and CLI pages.
Funding cancellations
A job cancelled because its execution allowance was exhausted can return status: "cancelled" with error.code: "job_cancelled" and error.detail.budget_exhausted: true. This is a job result, rather than an HTTP error status. It is not charged. Prepaid documents have a per-document allowance even when the account has more funds; see Pricing. Resending the same oversized document does not raise that allowance.
Rate limits
-
The limits: creates, upload setup (grants and upload sessions), and job observation (get, list, events, request echoes and parse preview metadata) each have an independent requests-per-minute window per account. Creates allow 120/min free or 600/min paid; job reads allow 600/min free or 3000/min paid (5x); upload setup allows 240/min free or 1200/min paid (2x). An account RPM override scales the same way: 1x creates, 5x reads and 2x upload setup. Unlimited accounts remain uncapped (see Limits). Uploads and polls cannot spend create capacity, and all three windows are separate from inference. File downloads and individual preview parts do not count. NDI can also throttle a call with
429andRetry-After. -
A create refused with
429: it starts no job and is not billed, and nothing creates it for you later. Retry it yourself, with the sameIdempotency-Key, within 13 minutes. A later retry of that key answers503 job_pendinguntil Drex has confirmed with the document service that no job exists under it, then422 job_not_created: send the job again under a new key. If the document service did start the job after all, it is linked to your request and billed once, like any other. -
Automatic retries: the SDKs and the CLI retry
408,429and5xxanswers on their own, reusing the call's key, and never one markedretryable: false. By default they allow two retries without a total retry budget, so slow failures can take several minutes to surface. An explicit retry budget can stop retries earlier. Saved-schema creates are sent once unless you pass a retry policy. To retry later yourself, pass your ownidempotency_keyso the retry reuses it. See Errors and retries. -
Request limits: use a document create endpoint and JSON
source, such asPOST /v1/documents/parse?wait_seconds=60. For a URL source, send a JSON body withsource.typeset to"url",source.urlset to your public HTTPS document URL, andsource.file_nameset to its filename including the extension (for example,"invoice.pdf").wait_secondsis a query parameter, not a JSON body field. Keep create bodies under 1,000,000 bytes. The SDKs capwait_secondsat 60; zero-retention HTTP creates allow up to 100 seconds.
Document batches
Start with 8 concurrent documents, keeping each worker busy through upload, create and completion. Prefer the SDK option wait_seconds=60 on creates (for raw HTTP, POST /v1/documents/parse?wait_seconds=60, with source.file_name in the JSON source and no wait_seconds body field): it holds that request open for up to 60 seconds, then returns the current job. If it is still running, use jobs.wait rather than a tight polling loop. This reduces requests without changing how the job is billed. See the Python batch example and TypeScript batch example.
A Retry-After response header is the number of seconds to wait before retrying, not a job timeout. The current SDKs honor it automatically with their default retry policy (Python nace-sdk 0.7.0 or later). For a raw HTTP client, wait that long and retry creates with the same Idempotency-Key. Larger batches can still reach the create RPM limit or NDI's own limits; keep concurrency bounded and let retries wait for the window.