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" }
}
FieldAlways presentMeaning
codeyesA stable, machine-readable error code (see below).
messageyesA human-readable description. Do not match on this.
requestIdyesCorrelates the response with server logs. Include it in bug reports.
detailsnoStructured context — e.g. reason for auth failures, issues for validation.
hintnoA 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

codeHTTP statusWhen
VALIDATION400The request body or query failed schema validation. details.issues lists what.
UNAUTHORIZED401Missing, malformed, or invalid credential. details.reason says which.
FORBIDDEN403Authenticated, but not allowed — e.g. wrong role, or a token used on another tenant.
NOT_FOUND404The tenant, or the addressed resource, does not exist.
CONFLICT409The request collides with existing state — e.g. an email already in use.
RATE_LIMITED429Too many requests. Back off and retry; see Retry-After.
INTERNAL500An unexpected error. Retry; if it persists, send us the requestId.

Login reasons

Login returns 401 for a few distinct situations, distinguished by details.reason:

reasonMeaning
invalid_credentialsWrong email or password.
mfa_requiredCorrect password, but the tenant requires a second factor. Retry with totpCode or emailOtpCode.
invalid_mfaThe supplied MFA code was wrong or expired.
mfa_not_enrolledMFA 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.