When something fails, the payload carries a stable code. That code is the part
you should branch on.
Messages get reworded by whoever is improving the wording that week. HTTP
statuses are coarse: a 403 could be four genuinely different problems with four
genuinely different fixes. The code is the only part that is both specific and
promised not to change. Treat the message as something to show a person and the
code as something to write an if about.
{ "success": false, "error": { "code": "WORKSPACE_SUSPENDED", "message": "workspace is suspended" }}Authentication and permission
Section titled “Authentication and permission”You are not who you need to be, or you are but you still may not do that.
| Code | Status | Trigger | What to do |
|---|---|---|---|
UNAUTHORIZED |
401 | No token, or the token was rejected | Sign in again. omazy auth login |
FORBIDDEN |
403 | Authenticated, but the role lacks the permission | Ask an owner to widen the role |
INSUFFICIENT_SCOPE |
403 | The token is valid but was not granted this scope | Re-authorise with broader consent |
ACCOUNT_REQUIRED |
403 | The action needs a completed account | Finish account setup, usually an email |
LEGAL_HOLD |
403 | A legal hold blocks this change | Not overridable. Talk to whoever set it |
WORKSPACE_SUSPENDED |
423 | The workspace is suspended | An owner resolves billing |
The distinction between UNAUTHORIZED and FORBIDDEN is the one people get
backwards. UNAUTHORIZED means we do not know who you are. FORBIDDEN means we
know exactly who you are and the answer is still no. Retrying the login fixes
the first and does nothing at all for the second.
Plan and limits
Section titled “Plan and limits”Nothing is broken. You have simply asked for more than the current plan or the current minute allows.
| Code | Status | Trigger | What to do |
|---|---|---|---|
PAYMENT_REQUIRED |
402 | Payment is needed to continue | Check Billing |
UPGRADE_REQUIRED |
403 | The feature exists on a higher plan | Upgrade, or use the included alternative |
RATE_LIMIT_EXCEEDED |
429 | Too many requests | Honour Retry-After, back off exponentially |
COMPUTE_BUDGET_EXCEEDED |
429 | Refresh rate or compute budget exceeded | Poll less often |
PAYLOAD_TOO_LARGE |
413 | The body exceeded the size limit | Send less, or upload and reference it |
MODEL_NOT_ALLOWED |
The requested model is blocked by a guard | Pick a permitted model, or change the guard |
On RATE_LIMIT_EXCEEDED, add jitter to your backoff. Retrying immediately from
every worker at once turns a rate limit into an outage, and the limit was the
thing trying to prevent that.
Capability not configured
Section titled “Capability not configured”The feature exists. It has not been switched on here.
| Code | Status | Trigger |
|---|---|---|
NOT_CONFIGURED |
501 | The thing exists but is not set up for this app |
NOT_IMPLEMENTED |
501 | The operation is not available on this deployment |
STORAGE_DISABLED |
503 | Object storage is not configured |
SEARCH_DISABLED |
503 | The search backend is not configured |
ANALYTICS_DISABLED |
503 | The analytics store is not configured |
INGEST_DISABLED |
503 | Event ingest is switched off |
INSPECTOR_UNAVAILABLE |
503 | The queue inspector is not configured |
CIPHER_UNAVAILABLE |
Credential encryption is not configured |
These are the friendliest failures on this page, because the fix is a setting
rather than a rewrite. CIPHER_UNAVAILABLE is the one to escalate rather than
work around: it means the server cannot encrypt a credential you were about to
give it, and a stored secret is not something to improvise.
Validation and conflict
Section titled “Validation and conflict”The request was understood and refused on its merits.
| Code | Status | Trigger | What to do |
|---|---|---|---|
BAD_REQUEST |
400 | Malformed or missing parameters | Read the message, fix the request |
UNPROCESSABLE_ENTITY |
422 | Well-formed but not valid here | Usually an id that belongs to another app |
FORM_VALIDATION |
422 | One or more fields failed validation | Field details are in the payload |
UNSUPPORTED_MEDIA_TYPE |
415 | Wrong content type | Check Content-Type |
NOT_FOUND |
404 | No such object, or not visible to you | Check the id, and check the scope |
CONFLICT |
409 | The change collides with current state | Re-fetch and retry |
CHANNEL_EXISTS |
409 | That channel is already connected | Edit the existing one |
HANDLE_TAKEN |
The handle is in use | Pick another | |
HANDLE_RESERVED |
The handle is reserved | Pick another | |
LAST_OWNER |
Removing or demoting the only owner | Promote someone first |
NOT_FOUND deserves suspicion. It is also what you get when the object exists
but sits in a different workspace or app than the one your request was scoped
to. Before hunting for a deleted record, check that you are pointed where you
think you are. omazy --debug will tell you in one line.
LAST_OWNER exists because a workspace with no owner cannot be recovered by
anyone inside it. The rule is not being difficult, it is the only thing standing
between you and a locked room with the key inside.
Upstream and internal
Section titled “Upstream and internal”Not your request.
| Code | Status | Trigger | What to do |
|---|---|---|---|
UPSTREAM_REJECTED |
502 | A provider refused the call | Check provider health and credentials |
UPSTREAM_UNAVAILABLE |
503 | A provider could not be reached | Retry with backoff |
INTERNAL_ERROR |
500 | Something failed on our side | Retry once, then send us the request id |
For INTERNAL_ERROR, capture the request id. It is the difference between us
finding the exact failure in seconds and asking you to describe what happened.
The CLI explains some of these for you
Section titled “The CLI explains some of these for you”The CLI attaches a suggested remedy to the codes it recognises, so a failed command tells you what to do rather than only what went wrong.
It covers 18 of the codes above. It also still carries four remedies for codes
this platform does not emit (AUTH_TOKEN_EXPIRED, AUTH_TOKEN_REPLAY,
BUSINESS_NOT_AUTHORIZED, OCC_CONFLICT). They are harmless, and they are
listed here so nobody spends an afternoon looking for where OCC_CONFLICT comes
from. The answer is that it does not.