Errors and rate limits
The error format, every error code, and how rate limits and daily quotas work.
Error format
Every non-2xx response has the same JSON shape and an X-Request-Id header with the same id:
{
"error": {
"code": "invalid_request",
"message": "The request did not match the API specification.",
"requestId": "5b9f1f4e-7a53-4e1a-b8a2-7d5b8c1a9f10",
"details": [{ "location": "query.limit", "message": "must be <= 100" }]
}
}
Branch on error.code. It is stable. message is for people and may change. details is present for invalid_request only, and current for conflict only. Quote the request id when you contact support.
Error codes
| Status | code | Meaning | What to do |
|---|---|---|---|
| 400 | invalid_request | A parameter or body failed validation. | Fix the request using details. |
| 401 | missing_api_key | No Bearer key was sent. | Send Authorization: Bearer <key>. |
| 401 | invalid_api_key | The key is unknown, malformed or revoked. | Create a new key. |
| 403 | subscription_required | The owner has no active subscription. | Subscribe on the pricing page. |
| 403 | subscription_past_due | The last payment failed. | Fix billing on the account page. |
| 403 | insufficient_scope | A write with a read-only key. | Create a read-write key on the account page. |
| 404 | not_found | No such graph on this account (also: it was already deleted). | Check the id with GET /v1/graphs. |
| 409 | conflict | The graph changed since you read it. Nothing was written or deleted. error.current is its current version. | Read the graph again, re-apply your change to what is there now, and retry with the new updatedAt. |
| 409 | graph_limit_reached | The account already holds 100 graphs. | Delete one before creating another. |
| 413 | payload_too_large | The body is over 300 KB. | Send fewer sources. |
| 422 | idempotency_key_reused | The Idempotency-Key was used for a different request, or for a graph since deleted. | Use a new key for a new graph. |
| 429 | rate_limited | Too many requests this minute. | Wait Retry-After seconds. |
| 429 | quota_exceeded | The daily quota is used up. | Wait until 00:00 UTC. |
| 500 | internal_error | Something failed on our side. | Retry with backoff. Reads are always safe to retry; see Writing safely for writes. |
Rate limits and quotas
Limits apply per API key:
| Limit | Value | Window |
|---|---|---|
| Rate limit | 60 requests | any one minute (fixed windows that start on the minute) |
| Write limit | 20 writes | any one minute; create, update and delete count here and against the rate limit |
| Daily quota | 5,000 requests | one UTC day |
Requests rejected for rate limiting do not count against the daily quota. Failed authentication does not count at all.
Every response from an authorized key carries:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed per minute. |
X-RateLimit-Remaining | Requests left in the current minute. |
X-RateLimit-Reset | Seconds until the minute window ends. |
A 429 also carries Retry-After (seconds). A minimal retry loop:
import time, requests
def get(url, headers):
for attempt in range(5):
r = requests.get(url, headers=headers, timeout=10)
if r.status_code != 429:
return r
time.sleep(int(r.headers.get("Retry-After", "5")))
raise RuntimeError("still rate limited")
Need more? Tell us what you are building and we will raise your limits.
Writing safely
Writes need a key with the read_write scope and an active subscription. Three rules keep an agent or script from doing damage:
- Quote the version you read.
PATCHtakesexpectedUpdatedAtin the body andDELETEtakes it as a query parameter; both are the graph’supdatedAtfromGET /v1/graphs/{graphId}. If the graph changed since (for example you edited it in the web app), you get409conflictwith the current version and nothing is changed. Read the graph again, re-apply your change, and retry. Never retry a conflict with a token you did not just read. - Make creates retry-safe. Send an
Idempotency-Keyheader (a UUID is fine) onPOST /v1/graphs. If the request times out, send it again with the same key and body: you get the graph the first request created, not a duplicate. Keys are remembered for 24 hours. A key used with a different body returns422idempotency_key_reused. - After a
500on an update or delete, read the graph before retrying. The change may have been applied. A repeated delete of a graph that is already gone is a plain404.
An account holds at most 100 graphs through the API. Source trees are validated the way the web app validates them: source and fee ids are unique numeric strings, a group’s children and its members’ parentId must agree, and amounts cannot be negative. A problem is reported as invalid_request with a details entry such as body.sources[1].parentId.