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
| Status | Meaning | Do |
|---|---|---|
400 | The request is malformed — usually a required parameter missing or a value the endpoint cannot use | Fix the call. Never retry. |
401 | No key, an unknown key, a revoked key, or a key too new to have propagated | See below |
402 | Account suspended for billing | Settle it in the console. Never retry. |
404 | No such passage id, or no such guide slug | Not an error to retry — the id does not exist |
408 | The request exceeded the server timeout | Retry once, with a narrower query |
429 with retry-after | Over the per-minute rate | Wait ~1s and retry |
429 without retry-after | Past a hard-capped monthly quota | Never retry — it will not change this period |
500 | Something failed server-side | Retry once with backoff |
503 | The node has no shards configured | Not 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:
| Situation | How it arrives |
|---|---|
| No key, unknown key | HTTP 401 — a caller with no session has nowhere to put a JSON-RPC error |
| Over the per-minute rate | HTTP 429 — this is what a client’s own backoff understands |
| Unknown tool | JSON-RPC error -32601 |
| Bad arguments | JSON-RPC error -32602 |
| Suspended account, quota exhausted | A tool error in the result, not a status |
| The tool ran and failed | A 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.