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:

json
{ "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:

json
{
  "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:

json
{ "detail": "API key not found." }

Status codes

StatusMeaning
200The request succeeded.
401The credential was missing or not accepted. One body for every refusal cause; see Authentication.
403The action is not available to your workspace. Creating a key without API access enabled answers 403; see Keys and access management.
404The 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.
405The method is not supported. /v1 endpoints accept GET only, except POST /v1/citations/resolve and POST /v1/portfolio/import.
413The 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.
422The request failed validation. The body names the parameter or field; this is the one 4xx without the error envelope.
429A traffic ceiling was reached; see below.
503The 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):

HeaderMeaning
RateLimit-PolicyThe 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.
text
RateLimit-Policy: "key-minute";q=120;w=60, "workspace-day";q=50000;w=86400

Every /v1 REST response to a request an API key authenticates additionally carries the state of the minute window, traffic refusals included:

HeaderMeaning
RateLimitThe 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-LimitThe per-minute ceiling for this key.
X-RateLimit-RemainingRequests left in the current window; equal to r.
X-RateLimit-ResetWhen the current window resets, as a Unix timestamp in seconds; t seconds from the response.
text
RateLimit: "key-minute";r=87;t=42

429 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.

json
{
  "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.

json
{
  "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.

json
{
  "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.

json
{
  "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 in Retry-After, then retry. On any other 503, retry with exponential backoff and a bounded number of attempts.
  • Pace steady workloads against RateLimit (r requests left, t seconds to the reset) rather than reacting to refusals.