Error codes
Every error code the API returns, what causes it, and what to do about it.
Error shape
{
"error": {
"code": "PERMISSION_DENIED",
"message": "You do not have permission to perform this action."
}
}Authentication and authorization
| Code | Status | Cause | Do |
|---|---|---|---|
| 401 | Missing, malformed, expired, or revoked token. | Re-authenticate. |
| 403 | The account or its workspace is suspended. | Do not re-authenticate. Surface the message. |
| 403 | Role or token ability insufficient. | Do not retry. Check the role and the abilities. |
| 403 | This client version has been withdrawn. | Run |
| 409 | The account has no workspace yet. | Send the user to the URL in the body. |
| 403 | You cannot approve your own request or execution. | Someone else must approve it. |
Device flow
| Code | Status | Cause | Do |
|---|---|---|---|
| 400 | Not approved yet. | Normal. Keep polling at the given interval. |
| 400 | The ten-minute window closed. | Start a new device code. |
| 400 | The user refused. | Stop. Do not retry. |
| 400 | No such device or user code. | Start over. |
| 401 | The token is not recognised. | Re-authenticate. |
Plan and entitlement
| Code | Status | Cause |
|---|---|---|
| 402 | A cap was hit. The body names |
| 402 | The tier does not include this feature. The body names |
{
"error": {
"code": "FEATURE_NOT_IN_PLAN",
"message": "Audit logs & access history is available on the Team plan.",
"feature": "audit_logs",
"feature_label": "Audit logs & access history",
"current_plan": "pro",
"required_plan": "team",
"required_plan_label": "Team"
}
}These carry enough detail to build an upgrade prompt without a second request, which is why they are richer than the rest.
Resources and validation
| Code | Status | Cause |
|---|---|---|
| 404 | No such resource, or none you can see. The two are deliberately indistinguishable. |
| 405 | Wrong method for this path. Access approve and deny are |
| 422 | That person is already in the workspace. |
| 422 | The resource is not in a state where this makes sense - approving an already-approved request, for example. |
| 422 | A runbook with no steps cannot be executed. |
| 422 | The execution does not exist, or does not belong to this runbook. |
| 422 | The target has no credential registered. |
| 422 | The target requires host key verification and no pinned fingerprint is stored. |
Validation failures return 422 with Laravel's standard errors map keyed by field, alongside the message.
Transport and rate limiting
| Code | Status | Cause | Do |
|---|---|---|---|
| 426 | The request was not TLS, and not loopback. | Fix the client. Do not retry over plaintext. |
- | 429 | Rate limited. | Back off. |
A 429 on a credential endpoint is the reveal limiter - 60 a minute, much tighter than the general 300. A fleet-wide ssh exec can reach it legitimately.
Pipeline API
| Code | Status | Cause |
|---|---|---|
| 401 | Bad or missing release token. Unset means the endpoint is closed. |
| 422 | An artifact URL is on a host that is not allowed, or a checksum is malformed. |
Three mistakes worth naming
- Treating 403 as an expired credential
It is not. A client that re-authenticates on
403loops forever against a suspended account, and looks like an attack from the server's side.- Assuming token ids are integers
They are UUID strings. Sanctum's parent model types the property as an integer, which is wrong here and has caused breakage on both sides.
- POSTing to access approve and deny
They are
PUT, and aPOSTreturns405.
Something here wrong or missing? Tell us.