API documentation
Errors and rate limits
Error bodies
Every 4xx refusal from a /v1 endpoint carries one structured error object with a stable machine-readable code:
{ "error": { "code": "not_found", "message": "Resource not found." } }The code and its message are canonical per status code, never per call site, so a refusal does not reveal why a particular request was turned away. Codes served today: invalid_request (400), unauthenticated (401), forbidden (403), not_found (404), method_not_allowed (405), conflict (409), content_too_large (413), rate_limited (429).
Traffic refusals (the ceilings below) carry the same envelope plus a retry_after_seconds field:
{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Retry after the number of seconds in Retry-After.",
"retry_after_seconds": 21
}
}Branch on error.code, never on message text. Codes are stable: new codes may be added over time, existing codes are never renamed.
One refusal shape is different: a 422 request-validation refusal answers a body that names the offending parameter or field, not the error envelope. Each of its entries carries type, loc and msg, never the value you sent.
The management endpoints under /org/api-keys carry a single detail message instead:
{ "detail": "API key not found." }Status codes
| Status | Meaning |
|---|---|
200 | The request succeeded. |
401 | The credential was missing or not accepted. One body for every refusal cause; see Authentication. |
403 | The action is not available to your workspace. Creating a key without API access enabled answers 403; see Keys and access management. |
404 | The resource does not exist in your workspace. The API does not distinguish between a resource that does not exist and one that belongs to another workspace. |
405 | The method is not supported. /v1 endpoints accept GET only, except POST /v1/citations/resolve and POST /v1/portfolio/import. |
413 | The request body is too large. A request body carries at most 1,000,000 bytes, a book import at most 2,000,000. This refusal can arrive without a body. |
422 | The request failed validation. The body names the parameter or field; this is the one 4xx without the error envelope. |
429 | A traffic ceiling was reached; see below. |
503 | The request could not be served safely; retry later. See quota_unavailable and server_busy below. |
Rate limits
Two ceilings apply to every request an API key authenticates:
- a per-key rate limit over a one-minute window, and
- a per-workspace daily quota across all the workspace's keys, which resets at midnight UTC.
The ceilings are not printed in this documentation: every /v1 response states them in the RateLimit-Policy header below, so a client can read them before it holds a key.
Response headers
The API publishes the IETF RateLimit header fields (draft-ietf-httpapi-ratelimit-headers, Structured Fields syntax) next to the older X-RateLimit-* headers. Header names are case-insensitive.
Every /v1 REST response carries the policy, whether or not the request was authenticated (401, 403, 404 and 429 refusals included):
| Header | Meaning |
|---|---|
RateLimit-Policy | The ceilings that apply, one item per ceiling. q is the quota in requests, w the window in seconds. The key-minute item is the per-key rate limit; the workspace-day item is the per-workspace daily quota, whose window is the UTC calendar day. |
RateLimit-Policy: "key-minute";q=120;w=60, "workspace-day";q=50000;w=86400Every /v1 REST response to a request an API key authenticates additionally carries the state of the minute window, traffic refusals included:
| Header | Meaning |
|---|---|
RateLimit | The live state of the key-minute policy: r is the number of requests left in the current window, t the number of seconds until the window resets. |
X-RateLimit-Limit | The per-minute ceiling for this key. |
X-RateLimit-Remaining | Requests left in the current window; equal to r. |
X-RateLimit-Reset | When the current window resets, as a Unix timestamp in seconds; t seconds from the response. |
RateLimit: "key-minute";r=87;t=42429 responses additionally carry a Retry-After header, in seconds. On a rate_limited 429 it equals t; on a daily_quota_exceeded 429 it counts to midnight UTC and takes precedence over t.
MCP responses do not carry these headers: a rate-limited MCP tool call comes back as a tool result carrying retry_after_seconds inside the result body instead; see MCP.
429 with code rate_limited
The per-key rate limit was exceeded. The window is short; wait for Retry-After seconds and retry.
{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Retry after the number of seconds in Retry-After.",
"retry_after_seconds": 21
}
}429 with code daily_quota_exceeded
The workspace's daily quota was exhausted. Retry-After counts to midnight UTC, when the quota resets.
{
"error": {
"code": "daily_quota_exceeded",
"message": "Daily quota exceeded. The quota resets at UTC midnight.",
"retry_after_seconds": 14580
}
}503 with code quota_unavailable
Usage accounting could not be reached, so the request was refused rather than served unmetered. There is no Retry-After; retry with backoff.
{
"error": {
"code": "quota_unavailable",
"message": "The daily quota could not be verified. The request was refused."
}
}503 with code server_busy
Too many reads of very large companies were running at once, so this one was refused before any data was sent. Only GET /v1/nodes/{node_id} answers it, for a company with a very large record book. Wait for Retry-After seconds and retry, or page the company's records with GET /v1/nodes/{node_id}/records instead; see the API reference.
{
"error": {
"code": "server_busy",
"message": "Too many large reads are running. Retry after the number of seconds in Retry-After.",
"retry_after_seconds": 3
}
}Retry guidance
- On 429, wait for the number of seconds in
Retry-After, then retry. - On 503 with
server_busy, wait for the number of seconds inRetry-After, then retry. On any other 503, retry with exponential backoff and a bounded number of attempts. - Pace steady workloads against
RateLimit(rrequests left,tseconds to the reset) rather than reacting to refusals.