Authentication
One key, two headers that both work, and the same rules on REST and MCP.
The header
Either of these authenticates a request. They are equivalent; pick whichever your HTTP client makes easier.
x-api-key: nai_...
Authorization: Bearer nai_...
The same two work on https://api.unzoi.com/mcp. MCP clients that let you set arbitrary headers usually use
x-api-key; ones that only offer a bearer field use Authorization.
curl -s "https://api.unzoi.com/search?q=inflation" -H "x-api-key: $UNZOI_KEY"
curl -s "https://api.unzoi.com/search?q=inflation" -H "Authorization: Bearer $UNZOI_KEY" What does not need a key
The documentation the API serves is public, deliberately. An agent deciding whether this API is worth signing up for has to be able to read what it does before it has a key:
/openapi.json— the full OpenAPI 3.1 document/mcp/tools.json— the MCP tool schemas/llms.txt— the whole surface as plain text/docs/search-recipes.md— worked examples, and which tool to reach for, written for a model/docs/agent-workflow.md— the order to call the tools in, and the rules that keep answers honest/docs/completeness.md— readingpartial,coverageandfrom_clampedbefore concluding absence/docs/changelog.md— what changed in the contract, by version
Everything else answers 401 without a key.
Handling keys
A key is shown once, at creation. Only its hash is stored, so a lost key is rotated, not recovered — rotation issues the new key before revoking the old one, so there is no window where neither works.
- Never put a key in a browser. There is no CORS-credentialed mode and no per-origin restriction: a key in front-end JavaScript is a key anyone can read and spend. Proxy through your own server.
- One key per deployment, not per developer. Every key on a tenant shares the plan and its credits, so separate keys buy you revocation granularity and usage attribution, not separate budgets.
- Short-lived keys exist. The console can issue a key with a TTL of up to 24 hours — useful for a demo, a CI run, or handing an agent a credential you do not intend it to keep.
How a request is refused
Four things can turn a request away, and they mean different things.
| Status | Meaning |
|---|---|
| 200 | Success |
| 400 | Invalid request, charged nothing. Bounds, enums, dates and offsets are validated BEFORE any index work, so this never means a partially-served query. `credit_budget_exceeded`: the call's worst case is more than `max_credits`. |
| 401 | Missing or invalid API key. `code` is `unauthorized`. |
| 402 | Account suspended (subscription canceled or payment failed). `code` is `suspended`. |
| 429 | Refused before running, and charged nothing. `credit_rate_limited`: this minute's credits are spent. `message_rate_limited`: too many free calls or refused requests this minute. `insufficient_credits`: the key stops at its monthly allocation and does not have this call's worst case left. `retry-after` says how long to wait. |
| 503 | The index could not be read for this request. This is NOT an empty result set and NOT a statement that nothing matched — retry rather than caching it as a negative. `coverage` says which part was unreadable. |
Order matters: the key is checked first, then suspension, then the call is priced and checked
against the max_credits you passed, the credits left this month and the credits a minute. A
429 therefore never means "your key is wrong", and a 401 never means "you are over
budget".
On MCP, two of these move
An MCP client that receives an HTTP error status for a tool call concludes the server has gone away, and stops. That is the correct reading for a missing key — a client with no credential cannot open a session at all — but it is the wrong reading for an account that simply needs paying.
So the MCP endpoint splits them:
| Condition | REST | MCP |
|---|---|---|
| Missing or invalid key | 401 | 401 — the session never opens |
| This minute's credits spent | 429 + retry-after | A tool error, credit_rate_limited — transient, retry the call |
| Too many messages this minute | 429 + retry-after | 429 + retry-after — transient, send the same message again |
| Account suspended | 402 | A tool error on an open session |
| Not enough credits left this month, on a key that stops there | 429 | A tool error on an open session |
In the last two cases initialize and tools/list still succeed, and the refusal arrives on
the call that actually costs something, carrying a machine-readable code the agent can act on.
MCP errors shows the exact shape.
What every response tells you
Authenticated responses carry your standing as headers, so backing off correctly never needs a second request.
| Header | Meaning |
|---|---|
| x-credits-charged | Credits this call cost, equal to `credits_charged` in the body. 0 for a preview. |
| x-credits-limit | Credits included this calendar month. Absent when unlimited. |
| x-credits-per-minute | Credits per minute allowed by this plan. |
| x-credits-per-minute-remaining | Credits left in the current minute, after this call. |
| x-credits-per-minute-reset | Seconds until this minute's credits refill. |
| x-credits-remaining | Credits left this month. |
| x-credits-reset | Seconds until the monthly allocation resets. |
| x-credits-used | Credits used this month, including this call. |
| x-plan-history-days | How far back this plan may query. Absent means the full archive. |
Limits and plans covers what to do with each of them, and
GET /account returns the same figures as a body.
Keys are issued and revoked at console.unzoi.com.