Skip to main content
That is the envelope, returned with the HTTP status listed for the code. There are 26 codes, defined once in a shared contract and mirrored by the Python SDK.

The envelope

  • code — the stable identifier, one of 26. The only field to branch on.
  • category — one of eight. It selects the SDK exception class.
  • problem, cause, fix — what went wrong, why, and what to do. Written for a human.
  • docUrl — the documentation anchor for this code.
  • retryabletrue for four codes. See Retrying.
  • requestIdreq_ plus 16 hex characters, logged server-side under the same id.
Branch on code. Never branch on message text — problem, cause and fix are written for a human and the server rewrites them per instance, so a malformed CSV names the offending row and column in its cause.

Not every error is an envelope

A request body that fails to parse, or that matches none of the accepted shapes, returns a bare 400 like that one — or {"error":"invalid_request"}. No code, no category, no requestId. So “every error carries a request id” is false. Typed envelopes carry one; malformed input does not, and there is nothing to quote to support.
  • The SDK cannot type these. With no code to resolve, it falls back to the base HollerithError carrying the text An unexpected error occurred. An except ValidationError block will not catch it.
  • Catch HollerithError at the outer edge of any integration, not only the subclasses you expect.
  • A 400 with no code means the body shape was wrong, most often a misspelled field. A typo in the inline POST /v1/predictions body falls through to the context-backed parser and returns invalid_request without saying which shape failed.

Categories and SDK exceptions

The category field, not the code, selects the exception class. Eight categories, eight classes, all subclassing HollerithError.
Every exception exposes code, problem, cause, fix, doc_url, retryable and request_id. Printing one renders all of them on separate lines.

The catalogue

The What to do column is the fix string the API returns for that code.

auth — 401, AuthenticationError

permission — 403, PermissionDeniedError

quota — 429, QuotaExceededError

validation — 413 and 422, ValidationError

fitted_context_incompatible is a validation error, not a not_found one, despite its name sitting beside three context codes below. It raises ValidationError.

not_found — 404, NotFoundError

Reading /v1/predictions/{id}/result before the job succeeds returns job_not_found, not a 409 or a 425. Poll the job until its status is succeeded, then read the result.

conflict — 409, ConflictError

unavailable — 503, ServiceUnavailableError

worker_warming_up is defined in the contract but nothing in the control plane emits it. A cold or unreachable worker reaches you as worker_unavailable instead, which the SDK also raises itself when it cannot reach the API or object storage.

server — 500, ServerError

Retrying

Exactly four codes are retryable: rate_limited, worker_warming_up, worker_unavailable and cache_unavailable. Retry those, unchanged. Every other code will return the same answer however many times you send it. quota_exceeded is a 429 that is not retryable. A client that retries on status code rather than on the retryable field will loop against it until the daily reset. The SDK has no backoff-retry layer. What it does is narrower than that, in three ways:
  • Only ServiceUnavailableError with retryable=true is absorbed. A QuotaExceededError is raised to you on the first occurrence.
  • Only inside a poll loop. A 503 on the call that submits the job reaches you; a 503 while polling an already-submitted job is swallowed.
  • At a fixed poll_interval, default 1.0 second, with no exponential growth, until timeout (default 900.0 seconds) lapses and a TimeoutError is raised.
One asymmetry to know: while predict() and evaluate() always absorb a retryable 503 during polling, fit() only does so when you pass on_warming=. Without that hook, a transient warm-up during a fit wait propagates.
Rate limiting and daily quota are yours to handle. Nothing in the SDK spaces out your calls.

Two codes that carry more than one meaning

training_ref_expired

One code, two situations: the staged input for a job expired before the worker read it, and a succeeded job whose result aged out. The first needs a re-run, the second means you read the result too late. Scored predictions and evaluation metrics live for 1 hour. Fetch them inside that window or re-run the job.

rate_limited

This code is currently never emitted. No rate limiter is wired for the public API, so it is defined and typed but unreachable today. Handle it anyway. It costs one branch, and its retryable flag is true when it does arrive.

When you contact support

Send the requestId. The API logs the same id server-side against the failing request, so it is the fastest way to the exact log line.
Two cases have no id to send. A bare invalid_json or invalid_request response carries none, and errors the SDK raises before any request — missing_api_key is the common one — have request_id set to None. Quote the code and the call you made instead.