Errors
The error response envelope, HTTP status codes, and the codes an integrator will actually see.
Every error is a JSON body with an error object and a meta object. Verified
live against a running backend:
$ curl <UPZERO_API_URL>/api/v1/me/monitors{
"error": {
"code": "FORBIDDEN",
"message": "Not authenticated",
"details": {}
},
"meta": {
"request_id": "9fed79f1-2083-4652-b28e-d81c36d5160d",
"timestamp": "2026-09-16T12:52:09.096509+00:00"
}
}details is {} for most errors and an array of field-level entries for a
validation failure:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Human readable message",
"details": [
{ "field": "url", "code": "INVALID_URL", "message": "URL must be a valid HTTP or HTTPS URL" }
]
},
"meta": { "request_id": "...", "timestamp": "..." }
}Status codes
| Code | When |
|---|---|
400 | Malformed request (bad JSON, an id that isn't a UUID) |
401 | Missing, invalid or expired credential |
403 | Valid credential, but not authorized for this action or organization |
404 | The resource does not exist, or the caller cannot see it (never distinguished, to avoid leaking that something exists) |
409 | A conflicting or duplicate resource, or an invalid state transition |
422 | The request was understood but failed validation |
429 | Rate limited, see Rate limits |
500 / 502 / 503 | A server or upstream failure, not the caller's fault |
Two authentication cases that look alike return different codes: a request
with no credential at all is 403 ("Not authenticated"), while a request
whose credential was checked and rejected is 401. Both were confirmed
against a running backend rather than assumed from the status table alone.
Common codes
The ones an integrator hits most:
| Code | HTTP | Meaning |
|---|---|---|
UNAUTHORIZED | 401 | No token, or a token that failed verification |
TOKEN_EXPIRED | 401 | The token expired |
FORBIDDEN | 403 | No credential was presented at all |
ORGANIZATION_ACCESS_DENIED | 403 | Authenticated, but not a member of the target organization |
NOT_FOUND | 404 | Generic resource-not-found |
VALIDATION_ERROR | 422 | Generic validation failure, see details for which field |
LIMIT_EXCEEDED | 403 | A plan limit was reached (monitors, team members, and so on) |
RATE_LIMITED / RATE_LIMITED_ORGANIZATION / RATE_LIMITED_USER | 429 | See Rate limits for which applies and why the three are not interchangeable |
Codes tied to workflows are left out of this table on purpose: workflows are not part of this reference (see Endpoints).
Rate-limit responses are shaped differently
API_STANDARDS.md's example shows a 429 wrapped in the same error
envelope as everything else. The running middleware does not do that: a
429 body is
{ "detail": "Rate limit exceeded. Please slow down.", "retry_after": 30, "code": "RATE_LIMITED" }with no error/meta wrapper, verified against
app/middleware/rate_limiting.py rather than against a live 429 (rate
limiting is off in this environment by default, see
Rate limits). Treat a 429's body shape as this one,
not the general envelope above.