Errors
Every error response — from any endpoint — uses the same JSON envelope, so you can handle failures in one place.
The envelope
{
"code": "UNAUTHORIZED",
"message": "Invalid or expired token",
"requestId": "6a366e29-df3b-476e-b192-c180409477af",
"details": { "reason": "invalid_token" }
}
| Field | Always present | Meaning |
|---|---|---|
code | yes | A stable, machine-readable error code (see below). |
message | yes | A human-readable description. Do not match on this. |
requestId | yes | Correlates the response with server logs. Include it in bug reports. |
details | no | Structured context — e.g. reason for auth failures, issues for validation. |
hint | no | A short suggestion for fixing the request. |
Match on code (and details.reason where present), never on message — the
prose can change, the codes will not.
Codes
code | HTTP status | When |
|---|---|---|
VALIDATION | 400 | The request body or query failed schema validation. details.issues lists what. |
UNAUTHORIZED | 401 | Missing, malformed, or invalid credential. details.reason says which. |
FORBIDDEN | 403 | Authenticated, but not allowed — e.g. wrong role, or a token used on another tenant. |
NOT_FOUND | 404 | The tenant, or the addressed resource, does not exist. |
CONFLICT | 409 | The request collides with existing state — e.g. an email already in use. |
RATE_LIMITED | 429 | Too many requests. Back off and retry; see Retry-After. |
INTERNAL | 500 | An unexpected error. Retry; if it persists, send us the requestId. |
Login reasons
Login returns 401 for a few distinct situations, distinguished by
details.reason:
reason | Meaning |
|---|---|
invalid_credentials | Wrong email or password. |
mfa_required | Correct password, but the tenant requires a second factor. Retry with totpCode or emailOtpCode. |
invalid_mfa | The supplied MFA code was wrong or expired. |
mfa_not_enrolled | MFA is required but the user has no factor enrolled yet. |
Rate limiting
Sensitive endpoints (login, signup, OTP requests) are rate limited per IP. A
429 includes a Retry-After header — honour it rather than retrying
immediately.