Errors
The status codes, the shape of a failure body, and which failures are worth retrying.
The shape
{
"error": "permission denied",
"message": "…"
}error is the part to branch on. It is sometimes a stable machine code, such as
invalid_code or challenge_expired, and sometimes the failure stated plainly.
message is the human sentence, and is not always present. Some failures add a
field of their own, and those are the useful ones:
| Field | Appears on | Use |
|---|---|---|
required_permission | 403 | The exact permission key the route wanted. |
attempts_left | 401 on a second factor | How many tries remain. |
retry_after_seconds | 429 | How long to wait. |
The OAuth endpoints answer in that specification's own shape instead, with error
and error_description holding codes such as invalid_client, invalid_grant and
invalid_scope.
The codes
| Code | Means | Do |
|---|---|---|
400 | The body or a query parameter did not parse or did not validate. | Fix the request. Never retry unchanged. |
401 | No token, or one that is expired, revoked, or not signed by this zone. | Get a new token once. If it happens again, the credential is wrong. |
403 | Authenticated, but missing a permission. | Read required_permission. Widen the client's scopes. |
404 | No such resource in this tenant. | Check the id, and check the zone. |
409 | The resource is in a state that forbids this. | Read its status, wait, try again. |
429 | Rate limited. | Wait retry_after_seconds. |
500 | The call failed inside the platform. | Retry an idempotent call with backoff. |
503 | A dependency the route needs is unavailable. | Retry with backoff. |
Two that get misread
404 is also the answer for "belongs to someone else." Resources are scoped to
the tenant in your token, and a resource in another tenant is indistinguishable from
one that does not exist. That is deliberate: it stops the API confirming what exists
elsewhere. If a 404 surprises you, check the zone before you check the id.
401 can happen mid-life. Tokens are checked against a revocation list on every
request, so rotating a secret, deleting a client or signing out everywhere ends a
token before its expires_in does. Treat 401 as "fetch a new token", not as "my
clock is wrong".
Retrying
Retry 429, 500, 503 and network failures. Do not retry 400, 403 or 404:
nothing about the next attempt will differ.
Exponential backoff with jitter, capped at a minute, and a ceiling on attempts. For
429 specifically, use the retry_after_seconds the body gives you rather than your
own schedule.
POST needs care, because it is not idempotent: a request that timed out may still
have been applied. Look the resource up by name or tag before creating it again.