Every error response carries a stable code that programs can branch on, next to a human message that may change. Use this table when you handle an error in your client, or when you see a code in a log.
The type is ApiError and the list is ERROR_CODES, both in libs/core/shared/src/lib/types.ts. A 5xx is always INTERNAL_ERROR with no cause in the body; the cause is in the server log under the same requestId. How the client shows these: Logging and errors.
Generic
Code
Status
Meaning
BAD_REQUEST
400
Malformed request, or a 400 without a more specific code
VALIDATION_FAILED
400
The body or query failed validation; details.fields lists { field, message }
UNAUTHORIZED
401
Missing or expired access token, or wrong credentials
FORBIDDEN
403
Not allowed (for example a platform-admin-only route)
PERMISSION_REQUIRED
403
The caller lacks details.requiredPermission
NOT_FOUND
404
Unknown resource, or one in another organization
CONFLICT
409
A unique value is taken
PAYLOAD_TOO_LARGE
413
Body over the size limit
UNSUPPORTED_MEDIA_TYPE
415
Wrong Content-Type (most routes take JSON only)
TOO_MANY_REQUESTS
429
Rate limited
INTERNAL_ERROR
500
Anything unexpected
Authentication
Code
Status
Meaning
EMAIL_NOT_VERIFIED
403
Login (or creating an organization) before the email is verified
REGISTRATION_CLOSED
403
The product is registration: 'invite-only'
INVALID_OR_EXPIRED_TOKEN
400
A verification, password-reset or invitation link is unknown, used, revoked or expired
PASSWORD_NOT_SET
409
A password action on an account that signs in with Google only
INVALID_TWO_FACTOR_CODE
401
Wrong TOTP or recovery code (400 on the profile’s 2FA routes)
TWO_FACTOR_CHALLENGE_INVALID
401
The login’s 2FA challenge expired or ran out of attempts: log in again
Google login reports its errors as ?error=<code> on the page it redirects back to, not as a JSON body:
Code
Meaning
OAUTH_CANCELLED
The user cancelled on Google’s screen
OAUTH_EMAIL_NOT_VERIFIED
Google says the email is not verified
OAUTH_STATE_INVALID
The sign-in took too long or came from another browser: try again
OAUTH_PROVIDER_UNAVAILABLE
Google could not be reached
OAUTH_FAILED
Any other failure of the exchange
OAUTH_ACCOUNT_IN_USE
That Google account is linked to another user (409 when linking)
OAUTH_EMAIL_MISMATCH
The Google account’s email differs from the invitation’s
Organizations, members and units
Code
Status
Meaning
ORGANIZATION_REQUIRED
403
An organization route called without an active organization. 400 for a platform admin: a create or a one-organization route called without exactly one X-Organization-Id
ORGANIZATION_NOT_FOUND
404
Unknown organization
ORGANIZATION_SUSPENDED
403
The organization is suspended (400 when a platform action needs it active)
NOT_A_MEMBER
403
Switching to an organization the caller does not belong to
PLATFORM_ADMIN_NO_SWITCH
403
A platform admin asked to switch organization: they have no active one and work across all of them
ORGANIZATION_OVERRIDE_FORBIDDEN
403
X-Organization-Id sent by someone who is not a platform admin
ORGANIZATION_OVERRIDE_NOT_ALLOWED
400
X-Organization-Id on a route about the caller’s own session, membership or payment, a platform route or a public one
SLUG_TAKEN
409
The organization slug exists (deleted organizations keep theirs)
SINGLE_ORGANIZATION_MODE
409
Creating an organization in a single-organization product
ORGANIZATION_SIGNUP_CLOSED
403
Self-service signup is off, or the caller is a platform admin
ORGANIZATION_LIMIT_REACHED
403
The caller already created details.max organizations
ALREADY_MEMBER
409
Inviting or accepting for someone who is already a member
INVITATION_EMAIL_MISMATCH
403
Accepting an invitation that was sent to another email
LAST_ORG_ADMIN
409
The change would leave the organization without an organization-wide admin
LAST_PLATFORM_ADMIN
409
Removing the last platform admin
UNIT_FORBIDDEN
403
Acting on a unit outside the caller’s reach
OWNERSHIP_FORBIDDEN
403
Creating a record for another owner, or changing a record’s owner
UNIT_NOT_EMPTY
409
Deleting a unit that still has sub-units, members or data
UNIT_NAME_TAKEN
409
A sibling unit already has that name
UNKNOWN_UNIT
400
A unit id that does not exist in this organization (reserved: core and Keelmint no longer raise it)
Roles
Code
Status
Meaning
PERMISSION_NOT_HELD
403
Granting or taking away permissions the caller does not hold; details.permissions lists them
ROLE_NAME_TAKEN
409
A role with that name exists in the organization, or is a system role
ROLE_IN_USE
409
Deleting a role that a member or a pending invitation holds
UNKNOWN_ROLE
400
A role id that does not exist here, or belongs to another organization
ROLE_HIERARCHY
409
A built-in role edit that breaks ORG_ADMIN ⊇ UNIT_ADMIN ⊇ USER; details.permissions lists what the role above lacks
ROLE_ADMIN_CORE
409
Taking from ORG_ADMIN or UNIT_ADMIN a permission it needs to manage members, invitations or roles; details.permissions lists them
PERMISSION_ORG_WIDE_ONLY
400
An organization-wide-only permission (Billing.*, Organization.*, Roles.Create/Update/Delete) in UNIT_ADMIN or in a role held in a unit, or such a role assigned in a unit
ROLE_NOT_DEFAULTABLE
400
Making a role that manages members, invitations or roles (or holds an organization-wide-only permission) a default role, or editing a default role into one
ROLE_UNIT_ONLY
400
Assigning UNIT_ADMIN organization-wide: it is assigned in a unit only
ROLE_LOCKED
409
Changing PLATFORM_ADMIN, or renaming or deleting a built-in role
Billing
Code
Status
Meaning
PLAN_LIMIT_REACHED
403
Adding beyond the plan’s limit; details (limit, used, max, plan) only for a caller with Billing.Read organization-wide or a platform admin
FEATURE_NOT_IN_PLAN
403
The route needs a plan feature; details (feature, plan) only for a caller with Billing.Read organization-wide or a platform admin
BILLING_DISABLED
404
A billing route in a product without billing
BILLING_NOT_CONFIGURED
409
Checkout, portal or webhook on a development server without Stripe keys
ALREADY_SUBSCRIBED
409
Checkout while a subscription is live: change plans in the Customer Portal