Errors
The error envelope, every error code Febasi Auth returns, and what each one means in practice.
Every Auth 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. |
MISSING_API_KEY | A dual-auth route was hit with neither a JWT nor an X-API-Key. |
INVALID_API_KEY | The provided X-API-Key is unknown, revoked, or expired. |
MISSING_TENANT_CONTEXT | The handler could not resolve a tenant — usually a Client Key call missing the X-Tenant-Code header. |
Authorization (403)
| Code | When |
|---|---|
FORBIDDEN | JWT principal lacks the required permission. |
INSUFFICIENT_SCOPE | Client Key principal lacks the required scope, or scope-vs-permission mismatch on dual-auth routes. |
TENANT_ACCESS_DENIED | A Client Key tried to operate against a tenant it is not authorized for (per tenantAccessLevel / allowedTenantIds). |
IP_NOT_ALLOWED | Source IP is not in the configured tenant or per-key allowlist. Carries the failing IP in the message. See IP allowlist. |
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. |
NOT_FOUND | Generic "no such resource" — used by token revoke and a few other endpoints when the target id is unknown. |
Validation & conflict (400 / 409)
| Code | When |
|---|---|
VALIDATION_ERROR | Body / query / params failed schema validation. For IP allowlist updates, details.invalidEntries lists the malformed entries. |
IP_LOCKOUT_PREVENTED | A tenant config or Client Key update would leave the caller's current IP outside the proposed allowlist. Pass ?force=true to override (audit-logged). Platform admin:* callers bypass the check. |
PASSWORD_POLICY_VIOLATION | Password fails the tenant's policy on register or update. Includes violations[]. |
USER_CONTEXT_REQUIRED | POST /permissions/check was called by a Client Key with no user context. The endpoint needs a JWT-bound user to check. |
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 hit sessionLimits.maxConcurrentSessions on a tenant configured with enforceLogout: false. Includes currentSessions and maxSessions. |
Both share status 429 but mean different things. RATE_LIMIT_EXCEEDED is
recoverable by waiting Retry-After; SESSION_LIMIT_EXCEEDED requires
ending another session first — retrying without that will fail again
regardless of Retry-After.
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.