Skip to main content
Every failure returns the same envelope, so a client branches on error.code rather than parsing prose or matching on status alone.

Codes

Every error links here, at the anchor for its own code.

invalid_request

400 · A malformed body, a bad query parameter, or a missing Idempotency-Key on a write. param names the offending field. Don’t retry — fix the request.

invalid_token

401 · The bearer token is missing, malformed, expired or revoked. All four are reported identically so that probing can’t tell them apart. Issue a new token.

insufficient_scope

403 · The token doesn’t carry a scope the endpoint requires; requiredScopes lists what would work. Retrying won’t help — see 403 can appear without your token changing.

not_found

404 · No such resource for this token. Also returned for a resource that exists but belongs to another organization — see below.

method_not_allowed

405 · Known path, wrong method.

conflict

409 · The request contradicts current state: a duplicate email, an Idempotency-Key reused with a different body, or a transition that no longer applies (reviewing a reimbursement that someone already reviewed). Retrying the same call gives the same answer.

rate_limited

429 · The token’s bucket for this minute is exhausted. Retry-After says how long to wait. See Rate limits.

internal_error

500 · Our fault. It’s already reported to our error tracker along with the requestId in your response. Retry with backoff.

provider_error

502 · An upstream provider — email, SMS, payments — rejected the call, so the operation did not take effect. Safe to retry.

404 means “not visible to you”

A resource belonging to another organization returns 404, not 403. Existence is itself information: a 403 would confirm that an id is real, letting anyone enumerate ids across organizations. This applies to chapters too — a national reading a chapter that isn’t its direct child gets a 404.

403 can appear without your token changing

Effective permissions are re-derived on every request:
So a call that worked yesterday can return insufficient_scope today because your role was lowered, or because an admin blocked that scope for the whole organization. Treat insufficient_scope as a condition to surface to a human, not to retry.

Retrying safely

Retry 429, 500 and 502 with exponential backoff and jitter. Do not retry 4xx other than 429 — the request will fail identically. Every POST requires an Idempotency-Key, so a retry of a write is safe by construction: reusing the key replays the original response instead of creating a second record. See Idempotency.
Include requestId when you contact support. It identifies the exact request in our logs, including the endpoint, the token used and the failure.