Errors
Every failure, on every endpoint, comes back the same way:
{
"message": "Invalid cursor \"page-2\"; pass the next_cursor value from a previous response.",
"code": "invalid_cursor",
"errors": { "cursor": ["Invalid cursor \"page-2\"; ..."] }
}
| Field | Notes |
|---|---|
message | For a human. It may be reworded at any time — do not branch on it. |
code | For a program. Stable. This is the one to branch on. |
errors | Per-field messages. Present on validation failures, absent otherwise. |
The codes
| Code | Status | Means |
|---|---|---|
validation_failed | 422 | A parameter was wrong. errors says which. |
invalid_cursor | 422 | A cursor we did not issue. Start again without one. |
scope_conflict | 422 | Mutually exclusive scopes together, e.g. both source and person. |
not_found | 404 | No such slug. |
unauthenticated | 401 | Missing, malformed or revoked key. |
forbidden | 403 | Valid key, but its team may not touch this row. |
no_plan | 402 | The team has no active plan. |
rate_limited | 429 | Too many requests. Wait for Retry-After. |
Which ones are worth retrying
Retry rate_limited, after the number of seconds in the
Retry-After header.
Do not retry anything else. A 402 in particular looks like a
throttle and is not one: the team has no plan, and retrying will not change that. Treating it as
a rate limit is how a client ends up in a loop that never resolves.
404 tells you when you used the wrong kind of key
Passing a database id where a slug belongs is the most common way to mis-address this API, so the message says so explicitly rather than leaving you guessing:
{
"message": "No post found with slug \"48213\". That looks like a numeric id; this API addresses every post by its slug, which you can find on any search or list result.",
"code": "not_found"
}
Over MCP
An MCP tool has no status code, so failures come back as a tool error carrying the same message.
The 402 case carries the same code value in its error data, so a client
that speaks both surfaces sees one taxonomy.