Errors
Every error has the same shape, and the code is the stable part.
Every error, from every endpoint, has the same shape.
{ "error": { "code": "not_found", "message": "Post not found" } }code is the stable contract: branch your code on it. message is English prose meant to be shown or logged, and it can change wording without notice. Never match on it.
code | HTTP | What happened |
|---|---|---|
unauthenticated | 401 | No key, or a key that is malformed, unknown or revoked |
forbidden | 403 | API access is off for this workspace. Not about your role: no endpoint here is gated by role |
not_found | 404 | The resource does not exist, or is not in this workspace |
invalid_input | 400 | A field is missing, malformed or out of range |
invalid_state | 409 | The resource exists but is in a state where this cannot happen |
rate_limited | 429 | Too many requests, see Rate limits |
method_not_allowed | 405 | The path exists, but not with this verb. The Allow header lists the ones it takes |
upstream_failure | 500 | Something on our side failed |
A resource in another workspace answers 404, never 403. Telling the difference would confirm that the id exists somewhere, which is itself a leak.
What is worth retrying. rate_limited and upstream_failure are the only two: the first after the Retry-After window, the second right away. The other five describe your request, and repeating it word for word gets the same answer. A write that answered upstream_failure may or may not have landed, so read the resource before sending it again.
Method not allowed
A path that exists but not with that verb answers 405 with an Allow header listing what it does accept. The body is the same error shape, so a client never has to special-case it.