Errors
The error envelope, every error code the Auth API returns, and what each one means in practice.
Every Auth API response follows the same envelope:
{ "success": true, "data": { "..." : "..." } }
{ "success": false, "error": "Human-readable message", "code": "MACHINE_CODE" }Validation failures (Zod) include a details array describing each invalid
field:
{
"success": false,
"error": "Validation failed",
"code": "VALIDATION_ERROR",
"details": [
{ "path": ["password"], "message": "String must contain at least 6 character(s)" }
]
}The HTTP status code is meaningful — match on code for fine-grained
behavior, on the status code for coarse routing.
Auth & sessions (401)
| Code | When |
|---|---|
UNAUTHORIZED | No Authorization header / no Client Key on a protected route. |
INVALID_CREDENTIALS | Wrong password, missing user, or INACTIVE user (deliberately ambiguous). |
INVALID_TOKEN | Token signature invalid, malformed, or revoked. |
TOKEN_EXPIRED | Access token's exp is in the past. |
Authorization (403)
| Code | When |
|---|---|
FORBIDDEN | Authenticated, but missing the required permission/scope. |
HIERARCHY_VIOLATION | Trying to manage a role/user at or above the actor's level. |
PROTECTED_RESOURCE | Trying to modify a system role (e.g. super_admin). |
CROSS_TENANT_ACCESS | The resource belongs to a different tenant than the caller. |
HIERARCHY_VIOLATION includes both actorLevel and targetLevel in
details — useful for UI messages.
Resource not found (404)
| Code | When |
|---|---|
TENANT_NOT_FOUND | The tenantCode (or tenant id) does not resolve. |
USER_NOT_FOUND | The user id does not exist in the caller's tenant. |
ROLE_NOT_FOUND | The role id does not exist. |
PERMISSION_NOT_FOUND | The permission id does not exist. |
Validation & conflict (400 / 409)
| Code | When |
|---|---|
VALIDATION_ERROR | Body / query / params failed Zod schema validation. |
PASSWORD_POLICY_VIOLATION | Password fails the tenant's policy on register or update. Includes violations[]. |
CONFLICT | Duplicate identifier (email, username, CPF/CNPJ already taken in this tenant). |
Rate / capacity (429)
| Code | When |
|---|---|
RATE_LIMIT_EXCEEDED | The route's per-IP rate limit is exhausted. |
SESSION_LIMIT_EXCEEDED | The user is at the tenant's max-concurrent-sessions cap (when configured to reject rather than rotate). Includes currentSessions and maxSessions. |
Server-side (500)
| Code | When |
|---|---|
INTERNAL_ERROR | Catch-all for unexpected exceptions. The response body never leaks stack traces in production. |
If you see INTERNAL_ERROR, capture the timestamp and tenant code; the
service writes a structured Pino log line for every one of them and they're
correlated by request id.
Recommended client behavior
Treat code, not message
Messages are intentionally human-friendly and may change. The code field
is the stable contract.
401 → refresh, not relogin
On TOKEN_EXPIRED, refresh first; on INVALID_TOKEN or
INVALID_CREDENTIALS, force a relogin.
403 → check the code
FORBIDDEN means "you don't have the permission". HIERARCHY_VIOLATION
means "the action would violate the level rule" — the user might still
fix it by escalating, instead of giving up.
409 is recoverable
CONFLICT is the user-friendly path: surface "already taken" to the
user instead of a generic error.