Skip to content

Error codes

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.

{ "statusCode": 403, "code": "PLAN_LIMIT_REACHED", "message": "The free plan allows 3 members", "details": { "limit": "members", "used": 3, "max": 3, "plan": "free" }, "requestId": "9b2f…" }

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

CodeStatusMeaning
BAD_REQUEST400Malformed request, or a 400 without a more specific code
VALIDATION_FAILED400The body or query failed validation; details.fields lists { field, message }
UNAUTHORIZED401Missing or expired access token, or wrong credentials
FORBIDDEN403Not allowed (for example a platform-admin-only route)
PERMISSION_REQUIRED403The caller lacks details.requiredPermission
NOT_FOUND404Unknown resource, or one in another organization
CONFLICT409A unique value is taken
PAYLOAD_TOO_LARGE413Body over the size limit
UNSUPPORTED_MEDIA_TYPE415Wrong Content-Type (most routes take JSON only)
TOO_MANY_REQUESTS429Rate limited
INTERNAL_ERROR500Anything unexpected

Authentication

CodeStatusMeaning
EMAIL_NOT_VERIFIED403Login (or creating an organization) before the email is verified
REGISTRATION_CLOSED403The product is registration: 'invite-only'
INVALID_OR_EXPIRED_TOKEN400A verification, password-reset or invitation link is unknown, used, revoked or expired
PASSWORD_NOT_SET409A password action on an account that signs in with Google only
INVALID_TWO_FACTOR_CODE401Wrong TOTP or recovery code (400 on the profile’s 2FA routes)
TWO_FACTOR_CHALLENGE_INVALID401The 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:

CodeMeaning
OAUTH_CANCELLEDThe user cancelled on Google’s screen
OAUTH_EMAIL_NOT_VERIFIEDGoogle says the email is not verified
OAUTH_STATE_INVALIDThe sign-in took too long or came from another browser: try again
OAUTH_PROVIDER_UNAVAILABLEGoogle could not be reached
OAUTH_FAILEDAny other failure of the exchange
OAUTH_ACCOUNT_IN_USEThat Google account is linked to another user (409 when linking)
OAUTH_EMAIL_MISMATCHThe Google account’s email differs from the invitation’s

Organizations, members and units

CodeStatusMeaning
ORGANIZATION_REQUIRED403An 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_FOUND404Unknown organization
ORGANIZATION_SUSPENDED403The organization is suspended (400 when a platform action needs it active)
NOT_A_MEMBER403Switching to an organization the caller does not belong to
PLATFORM_ADMIN_NO_SWITCH403A platform admin asked to switch organization: they have no active one and work across all of them
ORGANIZATION_OVERRIDE_FORBIDDEN403X-Organization-Id sent by someone who is not a platform admin
ORGANIZATION_OVERRIDE_NOT_ALLOWED400X-Organization-Id on a route about the caller’s own session, membership or payment, a platform route or a public one
SLUG_TAKEN409The organization slug exists (deleted organizations keep theirs)
SINGLE_ORGANIZATION_MODE409Creating an organization in a single-organization product
ORGANIZATION_SIGNUP_CLOSED403Self-service signup is off, or the caller is a platform admin
ORGANIZATION_LIMIT_REACHED403The caller already created details.max organizations
ALREADY_MEMBER409Inviting or accepting for someone who is already a member
INVITATION_EMAIL_MISMATCH403Accepting an invitation that was sent to another email
LAST_ORG_ADMIN409The change would leave the organization without an organization-wide admin
LAST_PLATFORM_ADMIN409Removing the last platform admin
UNIT_FORBIDDEN403Acting on a unit outside the caller’s reach
OWNERSHIP_FORBIDDEN403Creating a record for another owner, or changing a record’s owner
UNIT_NOT_EMPTY409Deleting a unit that still has sub-units, members or data
UNIT_NAME_TAKEN409A sibling unit already has that name
UNKNOWN_UNIT400A unit id that does not exist in this organization (reserved: core and Keelmint no longer raise it)

Roles

CodeStatusMeaning
PERMISSION_NOT_HELD403Granting or taking away permissions the caller does not hold; details.permissions lists them
ROLE_NAME_TAKEN409A role with that name exists in the organization, or is a system role
ROLE_IN_USE409Deleting a role that a member or a pending invitation holds
UNKNOWN_ROLE400A role id that does not exist here, or belongs to another organization
ROLE_HIERARCHY409A built-in role edit that breaks ORG_ADMIN ⊇ UNIT_ADMIN ⊇ USER; details.permissions lists what the role above lacks
ROLE_ADMIN_CORE409Taking from ORG_ADMIN or UNIT_ADMIN a permission it needs to manage members, invitations or roles; details.permissions lists them
PERMISSION_ORG_WIDE_ONLY400An 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_DEFAULTABLE400Making 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_ONLY400Assigning UNIT_ADMIN organization-wide: it is assigned in a unit only
ROLE_LOCKED409Changing PLATFORM_ADMIN, or renaming or deleting a built-in role

Billing

CodeStatusMeaning
PLAN_LIMIT_REACHED403Adding 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_PLAN403The route needs a plan feature; details (feature, plan) only for a caller with Billing.Read organization-wide or a platform admin
BILLING_DISABLED404A billing route in a product without billing
BILLING_NOT_CONFIGURED409Checkout, portal or webhook on a development server without Stripe keys
ALREADY_SUBSCRIBED409Checkout while a subscription is live: change plans in the Customer Portal
NO_BILLING_ACCOUNT409The Customer Portal before the first checkout
UNKNOWN_PLAN400A plan key that is not in the product config

Helpdesk

CodeStatusMeaning
UNSUPPORTED_FILE_TYPE415An attachment that is not an accepted image
TOO_MANY_ATTACHMENTS400More than five images
UPLOAD_QUOTA_EXCEEDED429The user’s upload quota is used up
TICKET_CLOSED409Replying to a closed ticket