unlob Docs
Browse documentation

Errors

Every status worth handling, and what to do about each one.

Shape

Errors are a plain-text reason; the status code carries the meaning. There is no JSON error envelope to parse, and no error code field — the status and the body text are the contract.

HTTP/1.1 429 Too Many Requests
retry-after: 1
x-ratelimit-limit: 600
x-ratelimit-remaining: 0

rate limit exceeded

The table

StatusMeaningDo
400The request is malformed — usually a required parameter missing or a value the endpoint cannot useFix the call. Never retry.
401No key, an unknown key, a revoked key, or a key too new to have propagatedSee below
402Account suspended for billingSettle it in the console. Never retry.
404No such passage id, or no such guide slugNot an error to retry — the id does not exist
408The request exceeded the server timeoutRetry once, with a narrower query
429 with retry-afterOver the per-minute rateWait ~1s and retry
429 without retry-afterPast a hard-capped monthly quotaNever retry — it will not change this period
500Something failed server-sideRetry once with backoff
503The node has no shards configuredNot yours to fix; retry with backoff

401 on a brand-new key

The most common surprise. Keys propagate to the serving fleet on a refresh cycle, so a key minted seconds ago can answer 401 for up to about a minute.

The rule: 401 on a key that has never worked is worth one retry after a minute. 401 on a key that worked yesterday is not — it was revoked, or the account lapsed, and retrying will not change either. Check the console.

401 deliberately does not distinguish “wrong key” from “revoked key”, so probing tells an attacker nothing.

The two 429s

Both mean stop, and they mean it for different lengths of time:

if r.status_code == 429:
    if "retry-after" in r.headers:
        time.sleep(int(r.headers["retry-after"]))   # a second; then retry
    else:
        raise QuotaExhausted(r.text)                 # this month; do not retry

Branching on the header rather than the status is what stops a retry loop hammering a hard-capped key thousands of times to no effect. See Rate limits and quotas.

408 — narrow the query, do not just retry

A timeout usually means the query was expensive: a very broad term with no filters, a large limit, or mode=semantic over a wide vertical. Retrying the identical request is likely to time out identically.

Add a filter, name the vertical, or lower limit, and it will usually go through.

Successful responses that are not what you wanted

Two cases return 200 and still need handling.

partial: true — part of the corpus was unreachable, so the answer is incomplete. Retry, or report it as incomplete. Never treat it as evidence of absence. This is the most important non-error in the API; see Reading a result.

An empty results array — usually genuine. The index is admission-controlled, so it holds what was judged worth keeping. Confirm with why_not before concluding the web is silent on a topic.

Over MCP

The split matters, and it is deliberate:

SituationHow it arrives
No key, unknown keyHTTP 401 — a caller with no session has nowhere to put a JSON-RPC error
Over the per-minute rateHTTP 429 — this is what a client’s own backoff understands
Unknown toolJSON-RPC error -32601
Bad argumentsJSON-RPC error -32602
Suspended account, quota exhaustedA tool error in the result, not a status
The tool ran and failedA tool error in the result

A tool error looks like this — note it is a successful JSON-RPC response:

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [{ "type": "text", "text": "account suspended — settle billing in the console" }],
    "isError": true
  }
}

Billing refusals are tool errors rather than statuses because a client reads a transport error on an established session as the server having gone away, and tears the session down. A billing problem should not present as an outage.

Next