BetterIAM

Error codes

What each of the 219 Better IAM error codes means, why it happens, and how to handle it, grouped by HTTP status.

Every failure in Better IAM is an IamError with three fields: a stable code such as ACCESS_DENIED, a message written for people, and an HTTP status. Codes are part of the public contract and do not change between releases. Messages may be reworded at any time and can contain names from your data, so show them to people but never parse them: branch on code, never on message.

Over HTTP a refused call answers with that status and the body { "error": { "code": "ACCESS_DENIED", "message": "Access denied" } }. A RATE_LIMITED answer also carries retryAfterMs in the body and a Retry-After header in seconds. An exception that is not an IamError becomes 500 INTERNAL_ERROR with a generic message, so internals never leak. The browser client rethrows the envelope as IamClientError with the same code and status, plus retryAfterMs and, when you create the client with requestId, the requestId it sent as X-Request-Id, which you can quote in support tickets and find on the server's http spans. The CLI prints CODE: message to standard error and exits with status 1.

Each code below says what went wrong, why it happens, and what the caller should do. Server codes are grouped by HTTP status, followed by the SCIM error types, the codes only the browser client and the CLI raise, and the decision reasons the simulation APIs return. On the server, catch IamError and compare its code:

import { IamError } from 'better-iam';

try {
  await iam.require({ headers, tenantId, action: 'documents:write', resource: { type: 'document', id } });
} catch (error) {
  if (error instanceof IamError && error.code === 'ACCESS_DENIED') return forbidden();
  if (error instanceof IamError && error.code === 'RATE_LIMITED') return retryLater(error);
  throw error;
}

Handling errors in a UI

Map codes to what the person should do next, not to messages. A few groups cover most screens: sign in again (UNAUTHENTICATED, SESSION_NETWORK_MISMATCH), step up (MFA_REQUIRED, RECENT_AUTH_REQUIRED), fix the form (INVALID_INPUT, INVALID_CREDENTIALS, WEAK_PASSWORD, shown next to the field), wait (RATE_LIMITED), not allowed (ACCESS_DENIED, where you can offer to request access), and reload because something changed (CONFLICT, VERSION_CONFLICT, INVALID_TRANSITION). Show anything else as a generic failure with the request ID. The Next.js, NestJS, SvelteKit, Nuxt, and Node middleware integrations already treat UNAUTHENTICATED, MFA_REQUIRED, EMAIL_UNVERIFIED, TENANT_INACTIVE, and TENANT_UNAVAILABLE as a signed-out request (Next.js also the network refusals SESSION_NETWORK_MISMATCH, IP_BLOCKED, and IP_NOT_ALLOWED).

import { IamClientError } from 'better-iam/client';

type NextStep =
  | { kind: 'sign-in' }
  | { kind: 'step-up'; code: string }
  | { kind: 'wait'; ms: number }
  | { kind: 'message'; text: string };

/** Decides what the UI does after a failed call. */
export function nextStep(error: unknown): NextStep {
  if (!(error instanceof IamClientError)) throw error; // a network failure or an aborted call
  switch (error.code) {
    case 'UNAUTHENTICATED':
    case 'SESSION_NETWORK_MISMATCH':
      return { kind: 'sign-in' };
    case 'MFA_REQUIRED':
    case 'RECENT_AUTH_REQUIRED':
      return { kind: 'step-up', code: error.code };
    case 'RATE_LIMITED':
      return { kind: 'wait', ms: error.retryAfterMs ?? 60_000 };
    case 'ACCESS_DENIED':
      return { kind: 'message', text: 'You do not have permission to do this.' };
    case 'CONFLICT':
    case 'VERSION_CONFLICT':
      return { kind: 'message', text: 'This changed in the meantime. Reload and try again.' };
    default:
      // requestId is set when the client was created with { requestId: true }.
      return { kind: 'message', text: `Something went wrong (reference ${error.requestId ?? 'none'}).` };
  }
}

CLEARANCE_REQUIRED

A decision reason, not a thrown error: the resource carries a classification label that the session's security clearance does not dominate.

With the clearances option, the first step of every decision on an application action (anything but iam:*) checks the resource's label against every party of the session: the person, an agent key's sponsor, each agent of a delegated session, and the administrator behind "view as". When one of them lacks the level, a compartment, or the citizenship the label needs, or the label is missing on a type that requires one or is invalid, the decision is { allowed: false, reason: 'CLEARANCE_REQUIRED' } whatever roles and policies say, root administrators included unless clearances.appliesToRoot is false. authorize, authorizeMany, and the HTTP routes report it as ACCESS_DENIED and require throws ACCESS_DENIED; the reason itself shows only in reviews such as policies.simulate, access invariants, and effectiveActions. The denial is audited with the metadata { mandatory: 'clearance' } and never says which dimension failed. No access request, package, or step-up lifts it, so accessPaths.find offers no path.

How to fix: a security officer grants or raises the person's clearance and reads them into the compartments the label needs, or declassifies the resource. Officers see the failing dimension with clearances.explain.

400 Bad request

The request itself is the problem: an input failed validation, or the operation is not allowed in the current state of the record. Fix the input or the order of operations; retrying the same request will fail the same way.

CodeMeaning
AUDIT_ARCHIVE_FAILEDThe audit-archive command could not archive the audit events of one or more tenants.
AUDIT_ARCHIVE_INVALIDThe audit-verify-archive command found that a tenant's archived audit chain does not verify.
AUDIT_CHAIN_BROKENThe audit-verify command found that a tenant's audit chain does not verify.
BREACHED_PASSWORDThe new password appears in a known data breach.
CONFIG_DRIFTconfig-plan --fail-on-drift found differences between the tenant and the configuration file.
CONFIG_EXISTSbetter-iam init found an existing configuration file and left it untouched.
CONFIG_WARNINGSconfig-validate --strict found names in the configuration file that the file itself does not define.
configurationAn OAuth, SAML, or SCIM option is missing or invalid, so the integration refuses to start.
CONFIRMATION_INVALIDThe link that confirms a public data-subject request is wrong, already used, or expired.
CREDENTIAL_CHAINING_DISABLEDA session token cannot be minted from a credential that is itself temporary.
DELIVERY_REQUIREDThe operation sends email, but the deployment has no email delivery callback.
DOCTOR_FINDINGSdoctor --strict found problems more serious than informational notes.
DOMAIN_NOT_ALLOWEDThe domain belongs to a shared mailbox provider, so no organization can claim it.
FINDINGSanalyze --fail-on found access findings at or above the chosen severity.
HOST_OUTSIDE_PATTERNSAn SSH host name or address falls outside the organization's host patterns.
HOSTNAME_NOT_ALLOWEDThe hostname belongs to the deployment itself, so no organization can claim it.
INVALID_ACTIONThe action name is not in the catalog, or a tenant-defined action is malformed.
INVALID_CIPHERTEXTA key management ciphertext could not be decrypted.
INVALID_CONFIGThe server, a storage adapter, or the CLI was given an invalid configuration.
INVALID_CREDENTIALThe credential id passed to a credentials method does not refer to an API key.
INVALID_CSRA certificate request could not be used.
INVALID_FILTERA storage query was given an invalid filter, ordering, or pagination.
INVALID_NONCEThe holder proof names no valid nonce.
INVALID_POLICYA policy document or access-package rule does not follow the policy grammar.
INVALID_PROOFThe holder proof does not verify.
INVALID_RECORDA record written to storage is not plain JSON or breaks a storage limit.
INVALID_REQUESTA policy evaluation input or an HTTP request target could not be understood.
INVALID_RESOURCE_TYPEThe resource type is unknown, reserved, or cannot be used this way.
INVALID_SPONSORThe sponsor of an AI agent or a guest is missing or is not an active person of the tenant.
INVALID_TICKETThe inference ticket given to inference.record is unknown, already used, expired, or its caller no longer exists.
INVARIANTS_BROKENcheck-invariants --fail-on-broken found invariants that are broken or cannot be evaluated.
INVITATION_INVALIDThe invitation link is invalid, expired, already used, or revoked.
LINKING_DISABLEDAccount linking is turned off on this deployment.
MAX_DEPTHCreating or moving this tenant would make the hierarchy deeper than allowed.
MISSING_ENVA CLI command needs an environment variable that is not set.
NOT_INITIALIZEDRoot recovery was attempted before the platform was bootstrapped.
OAUTH_CALLBACKThe OAuth sign-in callback arrived at a URL that does not match the connection's redirect URI.
OAUTH_STATEThe OAuth sign-in could not be matched to the browser that started it.
PASSWORD_REUSEDThe new password matches one of the person's recent passwords.
PROTECTED_IDENTITYA root administrator cannot be linked during organization onboarding.
RECONCILE_ATTENTIONreconcile --fail-on-attention found access-package rules that need attention.
RESOURCE_RESOLVER_REQUIREDAn authorization check names an application resource type, but no resolveResource callback is configured.
ROLE_CHAINING_DISABLEDA role cannot be assumed from a session that is itself an assumed role.
ROLLBACKA deliberate signal inside the storage conformance suite that discards a test transaction.
SIGNAL_REJECTEDThe Shared Signals receiver refused a security event token.
SNAPSHOT_INVALIDThe file given to store-import is not a valid Better IAM snapshot.
SNAPSHOT_TRUNCATEDThe snapshot ended early, or its record counts do not match its trailer.
SOURCE_ADDRESS_UNKNOWNThe organization binds SSH certificates to the caller's address, and the server could not determine it.
TOO_MANY_HOSTSAn SSH certificate would name more hosts or principals than allowed.
UNREADABLE_SECRETSrotate-secrets found stored values that none of the configured secrets can open.
UNSUPPORTED_FILTERA query plan's filter uses a condition the chosen target cannot express.
VERIFIED_EMAIL_REQUIREDA first sign-in through an external provider did not come with a verified email address.
WEAK_PASSWORDThe new password does not meet the password rules.
WEAK_TRUST_CONDITIONSA web-identity trust must name exactly which workload may use it, and these conditions do not.

AUDIT_ARCHIVE_FAILED

The audit-archive command could not archive the audit events of one or more tenants.

The command prints its full result first and names each failed tenant with the error code it hit. Tenants that succeeded are archived, and every tenant keeps its own archive position.

How to fix: resolve the listed errors (often the archive sink's storage or permissions) and run the command again; it continues from each tenant's position (continuous audit archiving).

HTTP 400 · thrown by @better-iam/cli

Example messages:

  • … tenant(s) could not be archived: ${result.failed.map((failure) =>

AUDIT_ARCHIVE_INVALID

The audit-verify-archive command found that a tenant's archived audit chain does not verify.

The command checks the archive files alone, without the database: every sequence must appear once, hashes are recomputed, and links must be intact. It also fails when two archived copies of the same sequence disagree, and prints the result, including the failure reason and sequence, before it exits.

How to fix: treat it as a possible tampering or data-loss incident, and compare the failing range with the database (audit-verify) and your backups (audit chain).

HTTP 400 · thrown by @better-iam/cli

Example messages:

  • No archived events of … were found

AUDIT_CHAIN_BROKEN

The audit-verify command found that a tenant's audit chain does not verify.

Each audit event carries a hash of the one before it, so a changed, removed, or reordered event breaks the chain. The command prints the verification result, including where it failed, before it exits.

How to fix: treat it as a possible tampering or data-loss incident: find the failing sequence in the output, compare it with your archive or backups, and review who can write to the database (audit chain).

HTTP 400 · thrown by @better-iam/cli

Example messages:

  • The audit chain does not verify

BREACHED_PASSWORD

The new password appears in a known data breach.

It is raised wherever a password is set (sign-up, password change, password reset, and other flows that set one) when the deployment screens passwords with authentication.passwordPolicy.isBreached, for example the built-in pwnedPasswords() client.

How to fix: ask the person to choose a different password, and show the message next to the password field (password screening).

HTTP 400 · thrown by @better-iam/auth

Example messages:

  • This password appears in a known data breach; choose a different one

CONFIG_DRIFT

config-plan --fail-on-drift found differences between the tenant and the configuration file.

The command prints the full plan first; the count covers the items it would create, update, or delete. The flag exists so a CI job fails when someone changed production outside the reviewed file.

How to fix: review the plan, then apply the file with config-apply or update the file to the intended state (configuration as code).

HTTP 400 · thrown by @better-iam/cli

Example messages:

  • The tenant differs from the configuration in … item(s)

CONFIG_EXISTS

better-iam init found an existing configuration file and left it untouched.

How to fix: edit the existing file, or pass --config with a new path to generate a fresh one.

HTTP 400 · thrown by @better-iam/cli

Example messages:

  • Configuration already exists; it was not overwritten

CONFIG_WARNINGS

config-validate --strict found names in the configuration file that the file itself does not define.

The command prints its findings first; the message counts the unresolved references. Without --strict they are only warnings, because the names may already exist in the tenant. Planning or applying the file fails when they do not.

How to fix: define the missing roles, groups, or other records in the file, correct the names, or drop --strict when they are meant to exist in the tenant already (config-validate).

HTTP 400 · thrown by @better-iam/cli

Example messages:

  • … unresolved reference(s)

configuration

An OAuth, SAML, or SCIM option is missing or invalid, so the integration refuses to start.

It is thrown while the integration starts or a connection is configured: an OAuth encryptionKey or SCIM provisioning key that is not 32 bytes, an endpoint without HTTPS, a provider without an issuer or client secret, duplicate connection IDs, or an empty allowedEmailDomains list. The message names the option.

How to fix: correct the option named in the message and restart.

HTTP 400 · thrown by @better-iam/oauth, @better-iam/saml, @better-iam/scim

Example messages:

  • OAuth encryptionKey must encode exactly 32 bytes.
  • OAuth endpoints require HTTPS (or explicitly enabled localhost HTTP).
  • OAuth connection IDs must be unique and configuration complete.
  • The provider issuer is required.
  • allowedEmailDomains must list one or more exact domain names.
  • microsoftTenant must be a tenant ID, domain, or alias.

CONFIRMATION_INVALID

The link that confirms a public data-subject request is wrong, already used, or expired.

privacy.confirmPublic accepts the token emailed to the requester once, within seven days of the request, and only while the request still waits for confirmation. Unknown request IDs get the same answer, so the call reveals nothing about which requests exist. Attempts are rate limited per request.

How to fix: open the latest confirmation email and use its link. If the request lapsed or was already confirmed, file a new one with privacy.submitPublic (privacy guide).

HTTP 400 · thrown by @better-iam/server

Example messages:

  • This confirmation link is invalid or has expired

CREDENTIAL_CHAINING_DISABLED

A session token cannot be minted from a credential that is itself temporary.

sts.getSessionToken accepts only a signed-in session or an API key as its source. Role sessions and other session tokens are refused, so a temporary credential can never be renewed or extended by deriving a new one from it.

How to fix: call sts.getSessionToken with the person's own session or the service account's API key (sts).

HTTP 400 · thrown by @better-iam/server

Example messages:

  • Temporary credentials cannot mint session tokens
  • Delegated sessions cannot assume roles

DELIVERY_REQUIRED

The operation sends email, but the deployment has no email delivery callback.

Organization and member invitations (tenants.create, identities.invite, and their resend calls), certification reminders (certifications.remind), the access digest, and expiry reminders enqueue email and refuse to run without authentication.sendEmail.

How to fix: configure sendEmail in the server options (configuration); in tests, a callback that records messages is enough.

HTTP 400 · thrown by @better-iam/server

Example messages:

  • Reminders require an email delivery callback
  • Clearance reminders require an email delivery callback
  • Guest invitations require an email delivery callback
  • Guest review reminders require an email delivery callback
  • Member invitations require an email delivery callback
  • Public requests need an email delivery callback

DOCTOR_FINDINGS

doctor --strict found problems more serious than informational notes.

doctor checks the schema, durability settings, secrets, delivery transports, and scheduled jobs, and prints every finding; with --strict, any finding above info severity fails the run.

How to fix: fix each check named in the output, or run without --strict while you work through them (doctor).

HTTP 400 · thrown by @better-iam/cli

Example messages:

  • … finding(s): …

DOMAIN_NOT_ALLOWED

The domain belongs to a shared mailbox provider, so no organization can claim it.

domains.add refuses consumer email domains (a default block list, replaced by the domains.blockedDomains option), because verifying one would route everyone with such an address to a single organization.

How to fix: claim the organization's own domain instead.

HTTP 400 · thrown by @better-iam/server

Example messages:

  • Shared mailbox providers cannot be claimed by an organization

FINDINGS

analyze --fail-on found access findings at or above the chosen severity.

The command prints the report first; the count covers unsuppressed findings at the --fail-on level or higher, so a CI job fails when new risk appears.

How to fix: fix the findings, or record accepted ones with analysis.suppress so they stop failing the run (scan for risky configuration).

HTTP 400 · thrown by @better-iam/cli

Example messages:

  • … finding(s) at or above … severity

HOST_OUTSIDE_PATTERNS

An SSH host name or address falls outside the organization's host patterns.

When hostPatterns is set in the SSH settings, the host authority only vouches for names matching it, so ssh.createHost and ssh.updateHost refuse other names and addresses, and ssh.updateSettings refuses patterns that would leave an existing host uncovered.

How to fix: use a name inside the patterns, or widen them with ssh.updateSettings.

HTTP 400 · thrown by @better-iam/server

Example messages:

  • Pattern … must end in a verified domain of this organization or fix an address prefix
  • … is outside the names this organization can vouch for (verify its domain or set hostPatterns)

HOSTNAME_NOT_ALLOWED

The hostname belongs to the deployment itself, so no organization can claim it.

hostnames.add refuses the deployment's base URL host, its trusted origins, and every name in its organization subdomain space (anything matching or under a hosts.patterns template), because those addresses already route to the deployment or to other organizations.

How to fix: claim a hostname on the organization's own domain, such as login.acme.com.

HTTP 400 · thrown by @better-iam/server

Example messages:

  • This hostname belongs to the deployment itself and cannot be claimed

INVALID_ACTION

The action name is not in the catalog, or a tenant-defined action is malformed.

Policy documents, listAccessible, access paths, invariants, policies.whoCan, and impact previews accept only actions the catalog knows (wildcards excepted). actions.register needs the {resourceType}:{verb} form under a tenant-defined resource type that is registered first.

How to fix: check the spelling, then declare the action in permissions.actions or register it (catalog).

HTTP 400 · thrown by @better-iam/server

Example messages:

  • Unknown action …
  • Tenant actions must be {resourceType}:{verb} under a tenant-defined resource type
  • Register resource type … before its actions

INVALID_CIPHERTEXT

A key management ciphertext could not be decrypted.

keys.decrypt and reEncrypt refuse a ciphertext that is malformed, was modified, was produced by another key than the keyId you insisted on, or is presented with a different encryption context than it was encrypted with. The reasons are deliberately indistinguishable. A failure after the call was authorized is audited as a denied iam:kms:decrypt with reason: 'invalid-ciphertext'.

How to fix: pass exactly the encryption context the data was encrypted with (the same keys and values), and store ciphertexts unchanged.

HTTP 400 · thrown by @better-iam/server

Example messages:

  • The ciphertext was not produced by that key
  • The ciphertext is not valid
  • The ciphertext could not be decrypted with this key and encryption context

INVALID_CONFIG

The server, a storage adapter, or the CLI was given an invalid configuration.

It surfaces at startup: when you call betterIam(), create an adapter, or when a CLI command loads your configuration file. Examples are a missing database, a plain-HTTP baseURL outside localhost, an unsupported database URL, or email features enabled without sendEmail; the browser client raises it too (with status 0) for a bad baseURL or basePath.

How to fix: correct the option the message names (configuration).

HTTP 400 · thrown by @better-iam/adapter-libsql, @better-iam/adapter-postgres, @better-iam/adapter-sqlite, @better-iam/auth, @better-iam/cli, @better-iam/client, @better-iam/server

Example messages:

  • libSQL requires a url: :memory:, file:, libsql:, https:, or wss:
  • Unsupported libSQL URL scheme
  • busyTimeoutMs must be between 0 and 60000
  • A PostgreSQL connection string is required
  • Invalid PostgreSQL pool size or lock timeout
  • SQLite requires a filename or :memory:; URI filenames are unsupported

INVALID_CREDENTIAL

The credential id passed to a credentials method does not refer to an API key.

credentials.get, credentials.update, credentials.revoke, and credentials.rotate manage service-account API keys only, so the id of a user session or temporary credential is refused.

How to fix: pass an API key id from credentials.list; end user sessions with identities.revokeSessions instead.

HTTP 400 · thrown by @better-iam/server

Example messages:

  • Not an API key credential
  • Only API keys can rotate

INVALID_CSR

A certificate request could not be used.

pki.issueCertificate and requestCertificate need a PEM PKCS#10 request signed by its own key. The request is refused when it is not valid DER, its signature does not verify, it uses an unsupported algorithm (such as RSA-PSS), or its key is not ECDSA P-256/P-384, Ed25519 or RSA of 2048 to 8192 bits.

How to fix: build the request with createCertificateRequest from better-iam, or with OpenSSL (openssl req -new -key key.pem).

HTTP 400 · thrown by @better-iam/server

Example messages:

  • The certificate request is not valid DER
  • The requested key must be ECDSA P-256 or P-384, Ed25519, or RSA of 2048 to 8192 bits
  • The request names an unsupported kind of name
  • csr must be a PEM certificate request
  • The request carries an unreadable public key
  • The request is signed with an unsupported algorithm

INVALID_FILTER

A storage query was given an invalid filter, ordering, or pagination.

It comes from IamStore.find and findOrdered: the filter must be a plain JSON object, limit and offset nonnegative safe integers, the after cursor an id, and ordering needs a top-level field, asc or desc, and finite bounds. You meet it when a plugin or adapter works with the store directly.

How to fix: correct the query arguments (adapter contract).

HTTP 400 · thrown by @better-iam/core

Example messages:

  • Field names in SQL conditions must be identifiers
  • SQLite adapters do not evaluate JSON conditions
  • Filter must be a JSON object
  • Pagination must use nonnegative safe integers
  • The pagination cursor must be an id
  • Order options are required

INVALID_NONCE

The holder proof names no valid nonce.

Every proof must carry a fresh nonce from verifiableCredentials.nonce (or the issuer's nonce endpoint). Nonces work once, for five minutes, for one organization.

How to fix: fetch a new nonce, sign a new proof with it, and send the request again.

HTTP 400 · thrown by @better-iam/server

Example messages:

  • The proof needs a fresh nonce from verifiableCredentials.nonce

INVALID_POLICY

A policy document or access-package rule does not follow the policy grammar.

Policies, boundaries, ceilings, API key session policies, and package rules are validated when saved; the message says what is wrong, and for package rules it starts with the path of the failing clause.

How to fix: correct the document; analysis.lintPolicy checks one before you save it (policies).

HTTP 400 · thrown by @better-iam/core, @better-iam/server

Example messages:

  • …: …

INVALID_PROOF

The holder proof does not verify.

A proof is an openid4vci-proof+jwt signed with the holder key (ES256, ES384 or EdDSA) whose public half is in the jwk header, with the organization's issuer URL as aud and an iat within the last five minutes.

How to fix: sign the proof with the key the credential should be bound to, for the right issuer URL.

HTTP 400 · thrown by @better-iam/server

INVALID_RECORD

A record written to storage is not plain JSON or breaks a storage limit.

A stored record must be a JSON object whose collection name, id, tenantId, and uniqueKey are nonempty strings of at most 512 UTF-8 bytes, nested at most 64 levels, without symbols, cycles, or unpaired surrogates, and under one million characters. You meet it when a plugin or your own code writes to iam.store.

How to fix: store only plain JSON values within these limits (adapter contract).

HTTP 400 · thrown by @better-iam/core

Example messages:

  • … must be a nonempty string of at most 512 UTF-8 bytes
  • … cannot contain unpaired surrogates
  • Record nesting exceeds 64 levels
  • Records must contain JSON data only
  • Circular records are not supported
  • Records cannot contain symbols

INVALID_REQUEST

A policy evaluation input or an HTTP request target could not be understood.

evaluatePolicy from @better-iam/core needs an action, a resource, and a grants array (with optional boundaries and context), and the Node.js handler refuses a request URL it cannot parse.

How to fix: pass a complete evaluation input, or fix the client or proxy that produced the URL.

HTTP 400 · thrown by @better-iam/core, @better-iam/server

Example messages:

  • Invalid policy evaluation request
  • Invalid request target

INVALID_RESOURCE_TYPE

The resource type is unknown, reserved, or cannot be used this way.

resourceTypes.register refuses a name the platform already defines and a parent that is not an existing managed type. Registering resources, creating relationships, and policy documents need a known type, and only managed types hold registered resources; other types are resolved by your resolveResource callback.

How to fix: declare or register the type first, and check whether it is managed (resources and catalog).

HTTP 400 · thrown by @better-iam/server

Example messages:

  • Resource type name is reserved or already defined by the platform
  • Parent must be an existing managed resource type
  • Unknown resource type
  • This resource type is resolved by the application, not registered with IAM
  • Unknown resource type …

INVALID_SPONSOR

The sponsor of an AI agent or a guest is missing or is not an active person of the tenant.

Every agent needs a sponsor, an active and unexpired person of the same tenant who answers for it. agents.create makes the caller the sponsor only when the caller is a person in their own session, so an API key or a temporary credential must name one with sponsorId. agents.create and agents.update refuse a sponsor who is a service account or another agent, disabled, deleted, expired, or in another tenant.

Guests need one too, and a guest cannot sponsor another guest. guests.invite makes the caller the sponsor only when the caller is a person of the organization in their own session or API key, so a root administrator or a service account names one with sponsorId. guests.setSponsor and guests.resendInvitation refuse a sponsor who can no longer sponsor guests the same way.

How to fix: pass the sponsorId of an active member of the tenant who is not a guest (agents.create, guests.invite).

HTTP 400 · thrown by @better-iam/server

Example messages:

  • An agent sponsor must be an active person of the same tenant
  • Name the person accountable for the agent (sponsorId)
  • Name a sponsor (sponsorId): only an active member of the organization who is not a guest can sponsor guests
  • A guest’s sponsor must be an active member of the organization who is not a guest

INVALID_TICKET

The inference ticket given to inference.record is unknown, already used, expired, or its caller no longer exists.

An allowed inference.check returns a single-use ticket that is valid for one hour and names the caller, their session, and the model. An external gateway redeems it once with the call's token counts; a ticket of another tenant is refused too.

How to fix: meter each call once, within the hour, with the ticket its own check returned; when a ticket is lost, the call cannot be metered through record, so check again before the next call.

HTTP 400 · thrown by @better-iam/server

Example messages:

  • Unknown, used or expired inference ticket

INVARIANTS_BROKEN

check-invariants --fail-on-broken found invariants that are broken or cannot be evaluated.

The command prints the full run first, then fails so a CI job or scheduler notices.

How to fix: review the violations and errors in the output, then correct the access or the invariant (access invariants).

HTTP 400 · thrown by @better-iam/cli

Example messages:

  • … broken and … unevaluable invariant(s)

INVITATION_INVALID

The invitation link is invalid, expired, already used, or revoked.

tenants.acceptInvitation and identities.acceptInvitation refuse a token that matches no open invitation, an organization that is no longer pending, and an invitation whose inviter's grant authority was revoked after it was sent. guests.redeem also refuses a guest invitation whose inviter is inactive or can no longer grant what it grants, or whose sponsor can no longer sponsor guests, and guests.resendInvitation refuses to send such an invitation again.

How to fix: ask an administrator to send a new invitation with tenants.resendInvitation, identities.resendInvitation, or guests.resendInvitation. When a guest invitation's inviter lost their rights, revoke it and have someone who holds them invite the person again with guests.invite.

HTTP 400 · thrown by @better-iam/server

Example messages:

  • Invitation is invalid
  • The sponsor can no longer sponsor guests
  • …: revoke this invitation and invite the person again
  • Invitation authority revoked

LINKING_DISABLED

Account linking is turned off on this deployment.

links.create, and accepting an organization invitation with a link to an existing account, need onboarding: { mode: 'linked' } in the server options.

How to fix: enable linked onboarding in your configuration, or accept the invitation without linking.

HTTP 400 · thrown by @better-iam/server

Example messages:

  • Linked onboarding is disabled
  • Account linking disabled

MAX_DEPTH

Creating or moving this tenant would make the hierarchy deeper than allowed.

tenants.create and tenants.reparent enforce hierarchy.maxDepth, eight levels by default.

How to fix: attach the tenant higher in the tree, or raise maxDepth in your configuration if deeper nesting is intended.

HTTP 400 · thrown by @better-iam/server

Example messages:

  • Maximum tenant depth reached

MISSING_ENV

A CLI command needs an environment variable that is not set.

Commands that act as a member (config-export, config-plan, config-apply, analyze, report, check-invariants, mine-roles) read a session token or API key from BETTER_IAM_TOKEN; bootstrap and recover-root read BETTER_IAM_ROOT_EMAIL and BETTER_IAM_ROOT_PASSWORD. Secrets never go on the command line.

How to fix: export the variable the message names and run the command again.

HTTP 400 · thrown by @better-iam/cli

Example messages:

  • Set BETTER_IAM_ROOT_EMAIL and BETTER_IAM_ROOT_PASSWORD
  • Set BETTER_IAM_DATABASE_URL
  • Set BETTER_IAM_SECRET
  • Set BETTER_IAM_TOKEN to a session or API key

NOT_INITIALIZED

Root recovery was attempted before the platform was bootstrapped.

recoverRoot and better-iam recover-root add a root administrator to the existing root tenant, so one must exist.

How to fix: run bootstrap first (recovering root access).

HTTP 400 · thrown by @better-iam/server

Example messages:

  • Bootstrap first

OAUTH_CALLBACK

The OAuth sign-in callback arrived at a URL that does not match the connection's redirect URI.

The callback's origin and path must equal the connection's redirectUri, so a response meant for another route is never accepted.

How to fix: make the redirect URI registered with the provider, the connection's redirectUri, and the route that handles the callback identical (OAuth sign-in).

HTTP 400 · thrown by @better-iam/oauth

Example messages:

  • Unexpected callback URL.

OAUTH_STATE

The OAuth sign-in could not be matched to the browser that started it.

The callback needs the state parameter and the browser-binding cookie set when the sign-in began. State that is missing, expired (after ten minutes), already used, or from another browser is refused, which blocks login CSRF and replayed callbacks.

How to fix: start the sign-in again in the same browser, and check that the binding cookie reaches the callback route.

HTTP 400 · thrown by @better-iam/oauth

Example messages:

  • Missing login state.
  • Invalid, expired, or replayed login state.

PASSWORD_REUSED

The new password matches one of the person's recent passwords.

A tenant's authPolicy.passwordHistory refuses the last that many passwords when a password is changed or reset.

How to fix: ask the person to choose a password they have not used recently (password rules).

HTTP 400 · thrown by @better-iam/auth

Example messages:

  • Password must differ from your last … password…

PROTECTED_IDENTITY

A root administrator cannot be linked during organization onboarding.

Accepting an organization invitation with linked onboarding refuses a link credential that belongs to a root administrator, so platform identities stay separate from customer organizations.

How to fix: accept the invitation without linking, or link an ordinary account.

HTTP 400 · thrown by @better-iam/server

Example messages:

  • Root identities cannot link

RECONCILE_ATTENTION

reconcile --fail-on-attention found access-package rules that need attention.

The count covers package rules that failed or were suspended, runs stopped by the safety brake until someone confirms them, and tenants that could not be processed; the full result is printed first.

How to fix: review the listed packages, fix their rules, and confirm braked changes with reconcile --tenant ID --package ID --confirm (automatic assignment).

HTTP 400 · thrown by @better-iam/cli

Example messages:

  • … package rule problem(s) need attention

RESOURCE_RESOLVER_REQUIRED

An authorization check names an application resource type, but no resolveResource callback is configured.

Managed resource types are registered with IAM and read from its storage; for every other type Better IAM needs your resolveResource option to load the resource and its attributes.

How to fix: configure resolveResource, or declare the type as managed and register its resources with resources.register (resources and catalog).

HTTP 400 · thrown by @better-iam/server

Example messages:

  • Configure resolveResource for application resources

ROLE_CHAINING_DISABLED

A role cannot be assumed from a session that is itself an assumed role.

roles.assume refuses role sessions as the source, so temporary access can never be extended by chaining one role into another. A session token from sts.getSessionToken is accepted as a source, because it is the same identity with the same or fewer grants.

How to fix: assume the role from the original user session or API key (assumed roles).

HTTP 400 · thrown by @better-iam/server

Example messages:

  • Role chaining is disabled

ROLLBACK

A deliberate signal inside the storage conformance suite that discards a test transaction.

The suite throws it to prove that an adapter rolls back uncommitted writes; the API never returns it.

How to fix: nothing, unless a conformance check around it fails, which means your adapter did not roll the transaction back as required (conformance suite).

HTTP 400 · thrown by @better-iam/core

Example messages:

  • discard

SIGNAL_REJECTED

The Shared Signals receiver refused a security event token.

iam.signals.receive throws it as a SignalRejectedError whose err is the RFC 8935 error code: invalid_key (a key, algorithm, or signature problem), invalid_issuer, invalid_audience, or invalid_request (the token type, its age, its claims, its subject, or its size). The message says which check failed. The push endpoint answers the same refusal as 400 with { "err", "description" }, and polling reports it to the provider in setErrs, so providers never see this code itself. temporary: true means the source's keys could not be fetched: the same token may pass later, so pushes answer 503 and polled events stay unacknowledged.

How to fix: compare the token with the source (signals.getSource shows the refusal as lastError): its issuer and aliases, audiences, keys, algorithms, and requireTyp, then fix the source with signals.updateSource or the provider's configuration (verification). A temporary refusal needs no change once the provider's key set is reachable again.

HTTP 400 · thrown by @better-iam/server

SNAPSHOT_INVALID

The file given to store-import is not a valid Better IAM snapshot.

Every line must be a JSON object: a header naming the snapshot format and version, one record per line, and a trailer with counts. An unsupported version or content after the trailer is refused, and nothing is imported.

How to fix: import a file written by store-export, unmodified (snapshots).

HTTP 400 · thrown by @better-iam/core

Example messages:

  • Content after the trailer on line …
  • Line … is not JSON
  • Line … is not an object
  • Not a … version … snapshot
  • The trailer on line … has no counts
  • Line … is not a record entry

SNAPSHOT_TRUNCATED

The snapshot ended early, or its record counts do not match its trailer.

store-import checks the trailer that store-export writes last; a missing trailer or a count mismatch means the file was cut off or edited, so nothing is imported.

How to fix: export the snapshot again and copy it completely (snapshots).

HTTP 400 · thrown by @better-iam/core

Example messages:

  • The snapshot has no trailer; nothing was imported
  • Record counts do not match the snapshot trailer; nothing was imported

SOURCE_ADDRESS_UNKNOWN

The organization binds SSH certificates to the caller's address, and the server could not determine it.

With bindSourceAddress, every certificate carries a source-address option with the IP it was requested from. The IP comes from http.clientInfo, which must be configured to read it (behind a proxy you trust).

How to fix: configure http.clientInfo to return ip, or turn bindSourceAddress off.

HTTP 400 · thrown by @better-iam/server

Example messages:

  • This organization binds certificates to your address, which could not be determined

TOO_MANY_HOSTS

An SSH certificate would name more hosts or principals than allowed.

One certificate names at most ssh.maxHostsPerCertificate hosts (64 by default) and 256 principals.

How to fix: name the hosts you need with hosts (and logins) in ssh.issueCertificate.

HTTP 400 · thrown by @better-iam/server

Example messages:

  • The certificate would name … hosts; name at most … with hosts
  • Name fewer hosts or logins (at most 256 principals)

UNREADABLE_SECRETS

rotate-secrets found stored values that none of the configured secrets can open.

The command re-seals values encrypted with previousSecrets under the current secret and reports any it cannot open, which means the secret that sealed them is missing from the configuration.

How to fix: add that secret to previousSecrets and run the command again; keep old secrets until it passes (secrets).

HTTP 400 · thrown by @better-iam/cli

Example messages:

  • … stored value(s) open with no configured secret; add the secret that sealed them to previousSecrets

UNSUPPORTED_FILTER

A query plan's filter uses a condition the chosen target cannot express.

iam.planResources returns a filter with the policy engine's full semantics; filterToSql has no equivalent for IP address and array conditions, and filterToPrisma and filterToMongo lack some of dates, IP addresses, and wildcard patterns that are not a prefix, suffix or substring. Compiling refuses rather than returning a filter that would include or leave out the wrong rows. Planning itself refuses with this code when no filter can be exact, for example with security clearances: for an application type the scheme requires labels on, for any application type while a label in the tenant passes down to children, and for a statement that conditions on a classification label's resource keys (resource.classificationRank and the like).

How to fix: fetch candidate rows with a coarser query and keep those that pass filterMatches(plan.filter, row), which supports every filter exactly, or check each row with authorize (filters).

HTTP 400 · thrown by @better-iam/core, @better-iam/server

Example messages:

  • … conditions cannot be expressed in …; filter these resources with filterMatches or authorize
  • These policies are too complex to plan as a filter; check the resources with authorize
  • A policy compares a value with …, one of the resource’s own attributes, which a filter cannot express; check the resources with authorize
  • A policy has a condition on …, which the server derives for each resource rather than reading from the row; check the resources with authorize
  • Resources of this type must carry a classification label, which a filter cannot check for application resources; check them with authorize
  • Classification labels pass down to application resources through the parents their resolver reports, which a filter cannot check; check them with authorize

VERIFIED_EMAIL_REQUIRED

A first sign-in through an external provider did not come with a verified email address.

Enrolling a new account from an OAuth, OpenID Connect, or SAML sign-in needs an email the provider asserts as verified, because the account is created with that email already verified.

How to fix: have the person verify their email at the provider, or configure the connection to release a verified email; a person with an existing account can link the provider to it instead (first sign-in).

HTTP 400 · thrown by @better-iam/server

Example messages:

  • Federation enrollment needs a verified email

WEAK_PASSWORD

The new password does not meet the password rules.

Every password needs at least 12 characters. A tenant can add a longer minimum, required character classes, and a ban on the person's name or email; the built-in screen refuses common and keyboard-pattern passwords; and a custom passwordPolicy.check can add its own rule and message.

How to fix: show the message next to the password field and let the person choose a stronger password (password rules).

HTTP 400 · thrown by @better-iam/auth

Example messages:

  • Password must contain at least … characters in this organization
  • Password must mix at least … of: lowercase, uppercase, digits, symbols
  • Password must not contain your name or email address
  • Password is too common or predictable
  • Password must contain at least 12 characters

WEAK_TRUST_CONDITIONS

A web-identity trust must name exactly which workload may use it, and these conditions do not.

Web-identity federation lets tokens from an OpenID Connect provider, such as a CI platform, be exchanged for a role session. Its trust conditions must pin the token's subject with a StringEquals or StringLike entry on token.sub whose values are nonempty and do not start with a wildcard; otherwise every workload the provider issues tokens for could assume the role.

How to fix: add a token.sub condition naming the specific subject, for example one repository and branch (web-identity federation).

HTTP 400 · thrown by @better-iam/server

Example messages:

  • Web identity trusts must pin token.sub with StringEquals or StringLike (a wildcard only after two complete segments, such as repo:acme/*)

401 Unauthenticated

Better IAM could not establish who is calling, or the proof it was given is no longer good enough: the session expired or was revoked, the key is invalid, or a code was wrong. Send the person back through sign-in (or step-up) and retry.

CodeMeaning
CERTIFICATE_INVALIDiam.pki.verify refused a presented certificate.
DELEGATION_TOKEN_INVALIDA delegation token did not verify, or the delegation behind it has ended.
INVALID_ASSERTIONA stateless assertion failed verification.
INVALID_CHALLENGEA one-time link, code, or sign-in step is wrong, expired, or already used.
INVALID_CREDENTIALSThe email and password, or the current password, are wrong, or the account cannot sign in.
INVALID_MFAThe authenticator code or recovery code is wrong, expired, or already used.
INVALID_PASSKEYThe passkey response could not be verified or does not belong to this account.
INVALID_TOKENThe OAuth access token, or its DPoP proof, is missing, invalid, or expired.
LOGIN_REQUIREDThe OAuth client asked for a fresh sign-in, and the person's session signed in too long ago.
OAUTH_PROFILEThe identity provider's answer did not identify the person.
SAML_INVALIDThe SAML response could not be accepted.
SESSION_EXPIRINGThe session asking for an SSH certificate ends within a minute.
SESSION_NETWORK_MISMATCHThe session is bound to the network it was signed in from and was used from a different one.
UNAUTHENTICATEDThe request has no valid credential, or its credential has expired or been revoked.

CERTIFICATE_INVALID

iam.pki.verify refused a presented certificate.

The certificate was not issued by this deployment (or differs from the stored one), is revoked, expired or not yet valid, is a CA certificate, does not allow the requested usage, or chains to an authority that is no longer active.

How to fix: present a current certificate issued by the tenant's authority, with its usage matching the side of the connection, and renew certificates before they expire.

HTTP 401 · thrown by @better-iam/server

Example messages:

  • No certificate was presented
  • The certificate cannot be read
  • The certificate was not issued here
  • The certificate has been revoked
  • A CA certificate cannot authenticate
  • The certificate is not for … use

DELEGATION_TOKEN_INVALID

A delegation token did not verify, or the delegation behind it has ended.

iam.a2a.verifyDelegationToken answers it (401) when the token is not a biam-delegation+jwt signed with the deployment's card keys, when its issuer, audience, organization, or times do not match, or when it lives longer than an hour. With live: true it also answers it when anything the token stands on has changed:

  • the delegation or one above it was revoked or has expired;
  • the person or an agent in the chain is no longer in good standing;
  • the acting agent's API key was revoked, or the agent no longer accepts delegation;
  • the audience left an agent's tokenAudiences;
  • a limit no longer allows a scope.

It also answers it when a replay check refuses a token seen before.

How to fix: the agent gets a new token with delegations.issueToken for the right audience; when the delegation has ended, the person grants a new one.

HTTP 401 · thrown by @better-iam/server

INVALID_ASSERTION

A stateless assertion failed verification.

verifyAssertion (and the Next.js edge and NestJS assertion helpers) refuses a token that is malformed, signed with another key, meant for another audience or issuer, expired, or not yet valid.

How to fix: issue a fresh assertion with assertions.issue for the right audience, and verify with every current key from iam.assertionKeys() while a secret rotates (stateless assertions).

HTTP 401 · thrown by @better-iam/server

INVALID_CHALLENGE

A one-time link, code, or sign-in step is wrong, expired, or already used.

It covers email verification, password-reset, and email-change links, passwordless and phone verification codes, the sign-in challenge between the password and the second factor, MFA enrollment, and passkey ceremonies. A passwordless code also fails when the email or phone changed after it was sent.

How to fix: start the step again to get a new link or code; retrying the same one fails the same way.

HTTP 401 · thrown by @better-iam/auth

Example messages:

  • Email has changed
  • Invalid verification code
  • Challenge is invalid or expired
  • MFA enrollment is invalid or expired
  • Invalid passkey challenge
  • Sign-in challenge is invalid or expired

INVALID_CREDENTIALS

The email and password, or the current password, are wrong, or the account cannot sign in.

auth.signIn gives the same answer for an unknown email, a wrong password, and a disabled account, so responses never reveal which accounts exist; auth.changePassword and auth.reauthenticate use it for a wrong password. Failed attempts count toward rate limits and are recorded.

How to fix: show a generic "email or password is incorrect" message on the form and let the person retry or reset their password.

HTTP 401 · thrown by @better-iam/auth

Example messages:

  • Invalid current password
  • Invalid password
  • Invalid email or password

INVALID_MFA

The authenticator code or recovery code is wrong, expired, or already used.

Authenticator (TOTP) codes must be six digits and each time step is accepted once, so a replayed code fails; emailed codes expire, and each recovery code works once. Failures count toward the sign-in rate limit.

How to fix: enter a fresh code from the authenticator app or email, or an unused recovery code (MFA).

HTTP 401 · thrown by @better-iam/auth

Example messages:

  • Invalid MFA code
  • Invalid or previously used MFA code
  • Invalid recovery code

INVALID_PASSKEY

The passkey response could not be verified or does not belong to this account.

Registration and sign-in responses are checked against the stored challenge, the relying party, user verification, and the registered credential; a passkey registered for another identity or organization is refused.

How to fix: start the ceremony again and use a passkey registered for this account in this organization (passkeys).

HTTP 401 · thrown by @better-iam/auth

Example messages:

  • Passkey registration verification failed
  • Passkey verification failed
  • Passkey is not registered for this organization
  • Passkey user handle does not match this identity
  • Passkey does not belong to this tenant identity
  • Passkey authentication verification failed

INVALID_TOKEN

The OAuth access token, or its DPoP proof, is missing, invalid, or expired.

Resource servers built with createAccessTokenVerifier or createResourceGuard refuse a missing or expired token, a replayed or mismatched DPoP proof, and a sender-constrained token sent without its proof, and answer with a WWW-Authenticate challenge naming invalid_token. The browser client also raises it, before sending, for a configured bearer token with an invalid format.

How to fix: obtain a new access token (refresh it or sign in again) and send it with the right scheme (resource servers).

HTTP 401 · thrown by @better-iam/client, @better-iam/oauth, @better-iam/server

Example messages:

  • Invalid bearer token format
  • Invalid or expired host token

LOGIN_REQUIRED

The OAuth client asked for a fresh sign-in, and the person's session signed in too long ago.

The OAuth provider's completeInteraction honors the client's prompt=login (sign in again now) and max_age (a sign-in at most this many seconds old). With prompt=login the session must have signed in after the authorization request started; with max_age its sign-in must be younger than the limit. Otherwise consent is not recorded and the call answers 401.

How to fix: send the person through sign-in again on the interaction page, then submit the consent step with the new session (OAuth provider).

HTTP 401 · thrown by @better-iam/oauth

Example messages:

  • The application asked for a fresh sign-in: sign in again, then continue.

OAUTH_PROFILE

The identity provider's answer did not identify the person.

The sign-in fails when the profile request fails or returns something unusable, when the profile or OIDC ID token names no subject, or when a Microsoft ID token names no directory.

How to fix: retry the sign-in; if it keeps failing, check the connection's provider settings, scopes, and any mapProfile function (providers).

HTTP 401 · thrown by @better-iam/oauth

Example messages:

  • The Microsoft ID token names no directory.
  • Provider profile request failed.
  • Provider subject is missing.
  • Provider profile is invalid.
  • OIDC subject is missing.

SAML_INVALID

The SAML response could not be accepted.

Better IAM rejects responses that are unsafe or malformed XML, fail the signature, issuer, audience, destination, recipient, or age checks, were already used, are unsolicited when the connection does not allow IdP-initiated sign-in, or lack a required encrypted assertion. The assertion consumer service answers every such failure the same way, so an attacker learns nothing from it.

How to fix: start the sign-in again from your application; if it keeps failing, compare the identity provider's settings (entity ID, ACS URL, certificates, encryption) with the connection (SAML).

HTTP 400, 401, 413, 415 · thrown by @better-iam/saml

Example messages:

  • Unsafe SAML document.
  • SAML response binding is invalid.
  • An encrypted assertion is required.
  • SAML subject recipient is invalid.
  • An unsolicited response cannot answer a request.
  • Invalid SAML response.

SESSION_EXPIRING

The session asking for an SSH certificate ends within a minute.

A certificate never outlives the session that requested it.

How to fix: sign in again, then request the certificate.

HTTP 401 · thrown by @better-iam/server

Example messages:

  • This session or the access that allows it ends within a minute; renew it first

SESSION_NETWORK_MISMATCH

The session is bound to the network it was signed in from and was used from a different one.

A tenant's authPolicy.bindSessionsToIp makes a user session usable only from the client IP it was issued from, so a stolen session cookie or token does not work elsewhere; the refused attempt is recorded. Requests without a recorded client IP are not judged.

How to fix: sign in again from the current network; the browser client calls its onUnauthenticated hook for this code (binding sessions to their network).

HTTP 401 · thrown by @better-iam/auth, @better-iam/server

Example messages:

  • This session can only be used from the network it was signed in from

UNAUTHENTICATED

The request has no valid credential, or its credential has expired or been revoked.

It covers a missing or malformed session cookie or bearer token, an expired or idle session, a revoked session or API key, a disabled or expired identity, a role session or session token whose source was revoked, and an impersonation session whose administrator's session ended.

How to fix: send the person to sign in again; the browser client calls its onUnauthenticated hook for this code, and framework integrations treat the request as signed out (sessions).

HTTP 401 · thrown by @better-iam/auth, @better-iam/middleware, @better-iam/nestjs, @better-iam/nuxt, @better-iam/server

Example messages:

  • Original session is no longer valid
  • Invalid authentication
  • Impersonation has ended
  • Invalid credentials
  • Invalid or missing credentials
  • Invalid or expired credentials

403 Forbidden

The caller is known, but this action is not allowed for them: no role or policy grants it, a deny or boundary blocks it, or a tenant policy (MFA, network, sign-in method) refuses the request. Retrying will not help until access or context changes.

CodeMeaning
ACCESS_DENIEDThe caller is signed in but is not allowed to perform this action.
CATALOG_LOCKEDOrganizations cannot register their own resource types or actions on this deployment.
CSRFA protocol endpoint that acts on a signed-in session was called without a trusted Origin or the X-Better-IAM header.
CSRF_REJECTEDThe request did not carry the headers that prove it came from your application rather than another site.
DELEGATION_INACTIVEThe delegation can no longer be used to open delegated sessions.
DELEGATION_NOT_ALLOWEDThe AI agent does not accept delegation.
EMAIL_UNVERIFIEDThe account's email address is not verified, and this deployment requires verification before sign-in.
FEATURE_DISABLEDThe feature this call needs is turned off or not configured on this deployment or for this organization.
FORBIDDENSelf-registration was attempted in the root tenant, where only administrators create accounts.
GRANT_AUTHORITY_REQUIREDThe caller has permission for the action but holds no active grant authority to issue access under.
GUEST_NOT_ALLOWEDThe organization's cross-tenant access settings, or those of the person's own organization, do not admit this person as a guest.
HOST_MISMATCHThe request arrived on one organization's sign-in address but names or authenticates as another organization.
IMPERSONATION_RESTRICTEDThe operation is not available while an administrator is impersonating a member.
INSUFFICIENT_SCOPEThe OAuth access token is valid but lacks a scope the resource requires.
INVALID_LINKThe identities cannot be linked, or the link or linking proof is no longer valid.
INVALID_SESSIONOAuth consent was attempted with a credential that is not a person's signed-in session.
INVALID_TENANT_TREEThe organization's chain of parent tenants contains a cycle, so authentication refuses it.
IP_BLOCKEDRequests from this network are blocked for the organization or for the whole platform.
IP_NOT_ALLOWEDThe organization only allows sign-in from certain networks, and this address is not one of them.
METHOD_NOT_ALLOWEDThe organization does not permit this sign-in method, or the HTTP method is not supported on this route.
MFA_NOT_ENROLLEDThe operation needs a second factor, but the person has not enrolled one.
MFA_REQUIREDThe session has not completed multi-factor authentication, and this action or organization requires it.
NAME_NOT_PERMITTEDA certificate would name something its authority may not certify.
OAUTH_TENANTThe person's Microsoft directory is not allowed to sign in through this connection.
PASSWORD_EXPIREDThe password is correct but older than the organization's maximum password age.
PROTECTED_OPERATIONAn OAuth client was about to be stored outside the authenticated registration path.
PROTECTED_RESOURCEThe target is a protected owner role or policy, or belongs to a higher grant authority, and cannot be changed this way.
RECENT_AUTH_REQUIREDThe operation is sensitive, and the session was not authenticated recently enough.
RESOURCE_MISMATCHYour resolveResource callback returned nothing, or a resource that does not match the one being authorized.
ROOT_SSH_RESTRICTEDA platform root administrator asked for an SSH certificate into another organization.
SECURITY_KEY_REQUIREDThe organization only certifies hardware security keys for SSH.
SELF_REVIEWA reviewer tried to certify their own access in an access review.
TENANT_INACTIVEThe organization, or one of its parent tenants, is suspended, pending, or deleted.
TENANT_MISMATCHThe signed-in person belongs to a different organization than the OAuth client asking for consent.
TENANT_UNAVAILABLEThe organization is not active, so nobody can sign in to it.
UNTRUSTED_ORIGINThe request came from a browser origin that the deployment does not trust.
WEB_IDENTITY_REJECTEDThe external token was not accepted by sts.assumeRoleWithWebIdentity, for a reason the response deliberately does not name.

ACCESS_DENIED

The caller is signed in but is not allowed to perform this action.

Every API operation checks an iam:* permission (for example iam:groups:update) and, when no role or policy grants it or a deny or boundary blocks it, records a deny audit event and fails with this code; iam.require and the framework guards throw it for your own actions too. It also covers specific refusals: approving your own access request, granting a role beyond your grant authority, changing a binding issued under a higher authority, cancelling someone else's request, or acting from a session that is not an ordinary session of the tenant.

How to fix: ask an administrator for a role that grants the action. To see why a decision was made, use policies.simulate or accessPaths.find.

HTTP 400, 403 · thrown by @better-iam/cli, @better-iam/nestjs, @better-iam/server

Example messages:

  • Not allowed${decision.reason ?
  • Access denied
  • Access paths are for an ordinary session of the tenant
  • A request cannot be approved by its requester
  • Cannot grant role …
  • Only the requester can cancel a request

CATALOG_LOCKED

Organizations cannot register their own resource types or actions on this deployment.

resourceTypes.register and actions.register need permissions.mode: 'tenant-defined' in the server options; in catalog mode only your configuration defines the catalog.

How to fix: declare the types and actions under permissions in your configuration, or switch to tenant-defined mode if organizations should define their own (catalog).

HTTP 403 · thrown by @better-iam/server

Example messages:

  • Tenant-defined resource types are disabled
  • Tenant-defined actions are disabled

CSRF

A protocol endpoint that acts on a signed-in session was called without a trusted Origin or the X-Better-IAM header.

The OAuth and SAML sign-in handlers start account linking only for a POST with an Origin from trustedOrigins, the X-Better-IAM: 1 header, and a JSON content type, and the OAuth provider's completeInteraction requires a trusted Origin on its consent POST. This stops other sites from starting these flows with the person's cookies.

How to fix: send the request with fetch from your own origin (the browser sets Origin), include the header, and list the origin in trustedOrigins (CSRF and Origin checks).

HTTP 403 · thrown by @better-iam/oauth, @better-iam/saml

Example messages:

  • Linking requires a trusted Origin and X-Better-IAM header.
  • A trusted Origin is required.

CSRF_REJECTED

The request did not carry the headers that prove it came from your application rather than another site.

API calls must be POST requests with Content-Type: application/json and X-Better-IAM: 1, and a request that carries cookies must also send an Origin header. The SCIM administration handler additionally rejects cross-origin requests, and the Next.js, NestJS, and middleware integrations apply the same cookie rule to your own routes.

How to fix: call the API through the browser client, which sets both headers, or add them yourself (CSRF and Origin checks).

HTTP 403 · thrown by @better-iam/middleware, @better-iam/next, @better-iam/scim, @better-iam/server

Example messages:

  • Cookie requests require Origin
  • JSON requests require X-Better-IAM: 1.
  • Cross-origin requests are rejected.
  • JSON and X-Better-IAM header required

DELEGATION_INACTIVE

The delegation can no longer be used to open delegated sessions.

delegations.assume refuses a delegation that was denied or revoked, a request that lapsed undecided, a delegation past its end, and one whose person is no longer an active, unexpired member of the tenant.

How to fix: have the person grant a new delegation, or ask for one with delegations.request; delegations.get shows the status and whether it expired.

HTTP 403 · thrown by @better-iam/server

Example messages:

  • This delegation is no longer active
  • A delegation above this one has ended
  • The person behind this delegation is not active

DELEGATION_NOT_ALLOWED

The AI agent does not accept delegation.

Its profile sets delegable: false, so delegations.grant, delegations.request, delegations.approve, and delegations.assume refuse it, and its existing delegated sessions are refused (as UNAUTHENTICATED) until delegation is turned back on.

How to fix: an administrator sets delegable: true with agents.update.

It also answers delegation limits that are not the agent's switch: a hand-off the person did not allow (delegations.handoff), and a delegation token for an audience outside the tokenAudiences of an agent in the chain, or for a scope some limit on the session does not allow outright (delegations.issueToken). Name an allowed audience or narrower scopes, or have an administrator add the audience to the agents' profiles.

HTTP 403 · thrown by @better-iam/server

Example messages:

  • This agent does not accept delegation
  • The person did not allow this delegation to be handed on
  • An agent cannot receive a hand-off from its own delegation chain
  • The person did not allow hand-offs to this agent
  • Agent … may not present delegations to …

EMAIL_UNVERIFIED

The account's email address is not verified, and this deployment requires verification before sign-in.

With authentication.requireEmailVerification (on by default when self-registration is enabled), sign-in, session issue, and every later use of a session refuse unverified identities.

How to fix: have the person open the verification link from their email (auth.verifyEmail), or send a new one with auth.requestEmailVerification (email verification).

HTTP 403 · thrown by @better-iam/auth

Example messages:

  • Verify your email before signing in

FEATURE_DISABLED

The feature this call needs is turned off or not configured on this deployment or for this organization.

Examples: self-registration without signUpEnabled, password sign-in or recovery with emailPassword: false or no sendEmail, a passwordless channel or passkeys that are not configured, passkey sign-in for an account without a passkey, emailed MFA codes the sign-in does not offer, a delivery kind without its callback, impersonation in a tenant whose policy does not set allowImpersonation, session JWTs without sts.jwt, custom hostnames without hosts.customHostnames, passkeys on a custom hostname outside the passkey domain (the RP ID), the inference API group and iam.inference without the inference option, and the clearances API group and iam.clearances.sendReminders without the clearances option.

How to fix: enable the option in your configuration or the tenant's authentication policy, or hide the feature in your UI while it is off.

HTTP 400, 403 · thrown by @better-iam/auth, @better-iam/server

Example messages:

  • … delivery is not configured
  • Delivery callback unavailable
  • Email delivery is not configured
  • Password recovery is unavailable
  • Password authentication is disabled
  • Impersonation is not enabled for this organization

FORBIDDEN

Self-registration was attempted in the root tenant, where only administrators create accounts.

auth.signUp refuses the platform's root tenant, because anyone who registered there would join the tenant that operates the whole platform.

How to fix: sign people up in an organization tenant, and create root identities with bootstrap, recover-root, or an administrator.

HTTP 403 · thrown by @better-iam/auth, @better-iam/server

Example messages:

  • Root identity provisioning requires an administrator

GRANT_AUTHORITY_REQUIRED

The caller has permission for the action but holds no active grant authority to issue access under.

Operations that grant access or create grantable records (bindings, roles, policies, group memberships, invitations, organizations, API keys) record the grant authority they were issued under. A non-root administrator needs one that is not revoked and whose delegation chain is intact.

How to fix: ask a root administrator, or someone holding a broader authority, to delegate one with authorities.create.

HTTP 403 · thrown by @better-iam/server

Example messages:

  • An active delegated grant authority is required

GUEST_NOT_ALLOWED

The organization's cross-tenant access settings, or those of the person's own organization, do not admit this person as a guest.

guests.invite, guests.resendInvitation, and guests.redeem check who may join as a guest. The host refuses an address at a blocked domain, outside its allowed domains, from an organization it lists as a refused partner, or any guest while allowGuests is off. When another organization verified the address's domain, that organization's outbound settings must allow its people to join the host too; its refusal only says that the person's organization does not allow it. An address at one of the host's own verified domains is refused as well, because that person is a member. Either side may change its settings after an invitation is sent, so redemption can fail for an invitation that was admitted.

How to fix: for the host's own domain, invite the person as a member with identities.invite. Otherwise an administrator of the host adjusts its inbound settings with guests.configure, or the person's organization lists the host as an outbound partner.

HTTP 403 · thrown by @better-iam/server

Example messages:

  • … is a verified domain of this organization: invite the person as a member
  • The person’s organization does not allow its members to join this organization as guests

HOST_MISMATCH

The request arrived on one organization's sign-in address but names or authenticates as another organization.

With organization addresses (hosts), a request on acme.signin.example.com or Acme's custom hostname is pinned to Acme: a sign-in call naming another tenantId, a session or API key of another organization, and a page on one organization's address calling the API on another's are all refused. Root administrators sign in at the deployment's own address, not an organization's.

How to fix: send the person to their own organization's address (its signInUrl from tenants.lookup), or leave tenantId out and let the address decide (sign-in addresses).

HTTP 403 · thrown by @better-iam/auth, @better-iam/server

Example messages:

  • This address belongs to another organization
  • This page and this address belong to different organizations

IMPERSONATION_RESTRICTED

The operation is not available while an administrator is impersonating a member.

An impersonation ("view as") session never counts as recently authenticated, so every operation that needs recent authentication refuses it. Decisions made in the member's name are refused too: approving requests or activations, reviewing access, accepting agreements, granting OAuth consent, requesting packages, assuming roles, change previews, and starting another impersonation.

How to fix: end the impersonation and act from your own session (impersonation).

HTTP 403 · thrown by @better-iam/auth, @better-iam/oauth, @better-iam/server

Example messages:

  • An impersonated session cannot be re-authenticated
  • Only a person acting through their own session can impersonate
  • This operation is unavailable while impersonating a member
  • Consent cannot be granted while impersonating a member.
  • Access paths are not available while impersonating
  • Requests cannot be decided while impersonating

INSUFFICIENT_SCOPE

The OAuth access token is valid but lacks a scope the resource requires.

Resource servers built with createAccessTokenVerifier or createResourceGuard check the scopes they were configured with and answer with a WWW-Authenticate challenge naming insufficient_scope; the message lists the missing scopes.

How to fix: have the client request the missing scopes in a new authorization, and make sure it is allowed to (resource servers).

HTTP 403 · thrown by @better-iam/oauth

Example messages:

  • Missing scope: ….

The identities cannot be linked, or the link or linking proof is no longer valid.

links.create links only ordinary user identities of two different tenants, and linked onboarding needs an ordinary user session. links.switch refuses a link that no longer holds, and a federated linking callback refuses a proof whose session or identity changed.

How to fix: link from two signed-in user sessions in different tenants, and start a linking flow again if its session ended.

HTTP 400, 403 · thrown by @better-iam/server

Example messages:

  • Invalid verified account-linking proof
  • Linked onboarding requires an ordinary user session
  • Only separate ordinary tenant identities can link
  • Only ordinary user identities can link
  • Invalid identity link

INVALID_SESSION

OAuth consent was attempted with a credential that is not a person's signed-in session.

The OAuth provider's completeInteraction requires a user session, so API keys and temporary credentials cannot grant consent on anyone's behalf.

How to fix: sign the person in to the client's organization and pass their session cookie or token (OAuth provider).

HTTP 403 · thrown by @better-iam/oauth

Example messages:

  • A user session is required.

INVALID_TENANT_TREE

The organization's chain of parent tenants contains a cycle, so authentication refuses it.

Before sign-in and on every session use, the authentication service walks the tenant and its ancestors; a tenant that is its own ancestor can only come from data changed outside Better IAM. The Next.js integration treats it as a signed-out request.

How to fix: repair the tenants' parentId values (for example from a backup), then sign in again.

HTTP 403 · thrown by @better-iam/auth

Example messages:

  • Invalid tenant hierarchy

IP_BLOCKED

Requests from this network are blocked for the organization or for the whole platform.

Administrators block networks with security.blockNetwork. A blocked address is refused before any credential is checked (so no rate-limit counter moves), and existing sessions from it stop working at their next use.

How to fix: connect from another network, or ask an administrator to lift the block with security.unblockNetwork or wait for it to expire (network blocks).

HTTP 403 · thrown by @better-iam/auth

Example messages:

  • Sign-in from this network is blocked

IP_NOT_ALLOWED

The organization only allows sign-in from certain networks, and this address is not one of them.

A tenant's authPolicy.allowedIpRanges is checked when a session is issued and on every later use, including role sessions derived from it; requests without a known client address are not judged.

How to fix: connect from an allowed network (for example the company VPN), or ask an administrator to add the range (IP allowlist).

HTTP 403 · thrown by @better-iam/auth

Example messages:

  • Sign-in from this network is not permitted for this organization

METHOD_NOT_ALLOWED

The organization does not permit this sign-in method, or the HTTP method is not supported on this route.

With status 403 it comes from a tenant's authPolicy.allowedMethods, for example a tenant that allows only federated sign-in refusing a password. With status 405 an API, SCIM, or webhook route was called with an unsupported HTTP method; API calls are POST.

How to fix: offer the person a method the organization allows (restricting sign-in methods), or send the request as POST.

HTTP 403, 405 · thrown by @better-iam/auth, @better-iam/scim, @better-iam/server

Example messages:

  • This sign-in method is not permitted for this organization
  • Use POST.
  • Use POST
  • Method is not supported

MFA_NOT_ENROLLED

The operation needs a second factor, but the person has not enrolled one.

auth.verifyMfa refuses a code when the person has no authenticator app and no emailed code was requested for this sign-in. Regenerating recovery codes, and an MFA step-up on an existing credential (such as a temporary credential requested with an MFA code), need an enrolled authenticator app.

How to fix: enroll a factor first with auth.beginMfa and auth.confirmMfa (MFA).

HTTP 403 · thrown by @better-iam/auth, @better-iam/server

Example messages:

  • MFA is not enrolled
  • Only a signed-in person can verify MFA

MFA_REQUIRED

The session has not completed multi-factor authentication, and this action or organization requires it.

It is raised when the deployment or tenant requires MFA and a session without it is issued or used (for example after the organization turned the requirement on), when activating an eligible role whose rules require an MFA-verified session, when disabling MFA or regenerating recovery codes, when impersonating a member who needs MFA, and by framework step-up guards.

How to fix: send the person through the second-factor step and retry; framework integrations treat it as a signed-out request (step-up).

HTTP 403 · thrown by @better-iam/auth, @better-iam/server

Example messages:

  • Multi-factor authentication is required
  • Complete multi-factor authentication before impersonating a member who requires it
  • Use an existing MFA factor before changing enrollment
  • MFA is required by this tenant
  • Activation requires an MFA-verified session
  • Sign in with multi-factor authentication to get an SSH certificate

NAME_NOT_PERMITTED

A certificate would name something its authority may not certify.

Every name on a certificate must fit the name constraints (permitted) of the issuing authority and every authority above it, and a SPIFFE ID must be in the authority's trust domain. This holds for everyone, owners and root administrators included.

How to fix: request names within the constraints, or issue from an authority whose constraints cover them.

HTTP 403 · thrown by @better-iam/server

Example messages:

  • A requested name is outside the authority's name constraints
  • This authority issues SPIFFE IDs only in …
  • spiffe://…/…/… names an identity; it is issued only to that identity (requestCertificate)

OAUTH_TENANT

The person's Microsoft directory is not allowed to sign in through this connection.

A Microsoft connection with allowedMicrosoftTenants accepts ID tokens only from those directory ids.

How to fix: add the directory id to the connection's list if it should be accepted, or sign in with an account from an allowed directory.

HTTP 403 · thrown by @better-iam/oauth

Example messages:

  • This Microsoft directory may not sign in here.

PASSWORD_EXPIRED

The password is correct but older than the organization's maximum password age.

A tenant's authPolicy.passwordMaxAgeDays makes sign-in refuse a password that was not changed within that many days.

How to fix: send the person through password reset (auth.requestPasswordReset, then auth.resetPassword) (password reset).

HTTP 403 · thrown by @better-iam/auth

Example messages:

  • Your password has expired; reset it to continue

PROTECTED_OPERATION

An OAuth client was about to be stored outside the authenticated registration path.

Better IAM's OAuth provider stores clients only through issuer.registerClient(), which checks the caller's permission, or through a dynamic registration admitted for the same tenant; any other attempt to write a client is refused.

How to fix: register clients with registerClient (OAuth provider).

HTTP 403 · thrown by @better-iam/oauth

Example messages:

  • Use authenticated registerClient().

PROTECTED_RESOURCE

The target is a protected owner role or policy, or belongs to a higher grant authority, and cannot be changed this way.

Owner roles cannot be requested, approved, bound, packaged, inherited, or assumed through trust, and protected roles and the owner policy cannot be edited or deleted; ownership moves only through identities.setOwner. Roles and policies created under a superior authority can be edited only by that authority. Lifecycle workflows fail a run with it when a step that takes access away, or sets attributes or an expiry, reaches an owner or a root administrator: workflows never disable, delete, sign out, strip, or edit them.

How to fix: use owner transfer for ownership, and ask the administrator whose authority created the role or policy to make the change.

HTTP 400, 403 · thrown by @better-iam/server

Example messages:

  • Owner roles cannot be requested
  • Owner roles cannot be granted this way
  • Use owner transfer for protected roles
  • Use owner transfer
  • Protected roles cannot be packaged
  • Owner policy is protected

RECENT_AUTH_REQUIRED

The operation is sensitive, and the session was not authenticated recently enough.

Password, email, factor, session, credential, ownership, and policy changes, impersonation, and similar operations need a session established within authentication.recentAuthenticationMs (five minutes by default). Temporary credentials such as assumed-role sessions and session tokens never qualify, and OAuth or SAML account linking needs a sign-in within the last five minutes.

How to fix: reauthenticate (for example auth.reauthenticate with the password, completing MFA if asked) and retry (recent authentication).

HTTP 403 · thrown by @better-iam/auth, @better-iam/oauth, @better-iam/saml

Example messages:

  • Temporary credentials cannot perform this operation; use a signed-in session
  • Reauthenticate to perform this operation
  • Linking requires recent authentication to a non-root identity in this tenant.

RESOURCE_MISMATCH

Your resolveResource callback returned nothing, or a resource that does not match the one being authorized.

For application resource types, Better IAM asks your callback for the resource and requires the returned tenantId, type, and id to equal the request's, so a faulty lookup can never authorize one tenant's request with another tenant's record. The request is refused instead.

How to fix: return exactly the requested resource from resolveResource (resources and catalog).

HTTP 403 · thrown by @better-iam/server

Example messages:

  • Resource ownership mismatch

ROOT_SSH_RESTRICTED

A platform root administrator asked for an SSH certificate into another organization.

The root override never opens an organization's servers: SSH access there needs a role in that organization, like anyone else's. Only the root tenant's own hosts are the root administrators'.

How to fix: grant the person ssh:login through a role in the organization.

HTTP 403 · thrown by @better-iam/server

Example messages:

  • Platform administrators need a role in this organization to open its hosts

SECURITY_KEY_REQUIRED

The organization only certifies hardware security keys for SSH.

With requireSecurityKey in the SSH settings, ssh.issueCertificate accepts only FIDO keys (sk-ssh-ed25519@openssh.com, sk-ecdsa-sha2-nistp256@openssh.com).

How to fix: create one with ssh-keygen -t ed25519-sk and request the certificate for it.

HTTP 403 · thrown by @better-iam/server

Example messages:

  • This organization certifies only hardware security keys (ed25519-sk or ecdsa-sk)

SELF_REVIEW

A reviewer tried to certify their own access in an access review.

Certification campaigns refuse a reviewer's decision on an item that grants the reviewer access, directly or through one of their groups.

How to fix: leave the item to another reviewer of the campaign (certifications).

HTTP 403 · thrown by @better-iam/server

Example messages:

  • Reviewers cannot certify their own access; ask another reviewer

TENANT_INACTIVE

The organization, or one of its parent tenants, is suspended, pending, or deleted.

Every API call re-checks the caller's tenant ancestry, so suspending an organization stops its sessions at their next request. Creating identities or invitations, opening access requests, moving a tenant under an inactive parent, and the OAuth, SAML, SCIM, and shared-signals endpoints refuse inactive tenants too; framework integrations treat it as a signed-out request.

How to fix: reactivate the organization and its parents with tenants.setStatus, or finish onboarding a pending one.

HTTP 400, 403 · thrown by @better-iam/oauth, @better-iam/saml, @better-iam/scim, @better-iam/server

Example messages:

  • Tenant is unavailable.
  • Tenant unavailable.
  • Tenant must be active
  • The tenant is not active
  • Tenant inactive
  • This organization is not active

TENANT_MISMATCH

The signed-in person belongs to a different organization than the OAuth client asking for consent.

The OAuth provider's completeInteraction accepts consent only from a user session in the client's tenant.

How to fix: sign the person in to the client's organization before the consent step (OAuth provider).

HTTP 403 · thrown by @better-iam/oauth

Example messages:

  • Sign in to the client tenant before continuing.

TENANT_UNAVAILABLE

The organization is not active, so nobody can sign in to it.

Authentication checks the tenant and every ancestor before sign-in, sign-up, and recovery and on every session use; a missing, suspended, pending, or deleted tenant anywhere in the chain is refused. Framework integrations treat it as a signed-out request.

How to fix: check that the person chose the right organization, and ask the platform operator to reactivate it if it was suspended.

HTTP 403 · thrown by @better-iam/auth

Example messages:

  • Tenant is not active

UNTRUSTED_ORIGIN

The request came from a browser origin that the deployment does not trust.

A request with an Origin header must name an exact origin from trustedOrigins (the origin of baseURL is always included); the Next.js and middleware integrations apply the same rule to cookie requests on your routes.

How to fix: add the origin (scheme, host, and port) to trustedOrigins in your configuration.

HTTP 403 · thrown by @better-iam/middleware, @better-iam/next, @better-iam/server

Example messages:

  • Origin is not trusted

WEB_IDENTITY_REJECTED

The external token was not accepted by sts.assumeRoleWithWebIdentity, for a reason the response deliberately does not name.

The public exchange answers every refusal that depends on stored state or on the token with this same 403 body: an unknown or revoked trust, a disabled provider, a bad signature, issuer, audience, type, or lifetime, a token that is expired or too old, unmet trust conditions, a missing source identity claim, a token already redeemed, an inactive service account, a revoked authority, or an inactive tenant. Keeping them identical stops callers from discovering which trusts, roles, and providers exist.

How to fix: an administrator runs trust.evaluateWebIdentity with the same token, which reports the reason and each failing condition, and checks the audit log for role:assumed-with-web-identity denials, whose metadata carries the reason.

HTTP 403 · thrown by @better-iam/server

Example messages:

  • The web identity token was not accepted

404 Not found

The record does not exist in this tenant. Better IAM also answers 404 for records in other tenants, so responses never reveal what exists elsewhere.

CodeMeaning
NOT_FOUNDThe record does not exist in this tenant.
OAUTH_CLIENTThe OAuth client is unknown, revoked, or belongs to an organization that is not active.
TENANT_NOT_FOUNDAn identity was about to be created in a tenant that does not exist.

NOT_FOUND

The record does not exist in this tenant.

Better IAM also answers 404 for records that belong to another tenant, so responses never reveal what exists elsewhere. Deleted identities, unregistered managed resources, unknown routes, and the public tenants.lookup and domains.discover calls for unknown or inactive organizations answer the same way.

How to fix: check the id and the tenantId you passed, and refresh lists that may be stale.

HTTP 400, 404 · thrown by @better-iam/auth, @better-iam/cli, @better-iam/core, @better-iam/oauth, @better-iam/projects, @better-iam/saml, @better-iam/scim, @better-iam/server

Example messages:

  • Passkey not found
  • Device not found
  • Session not found
  • No single identity in … has the email …
  • No API group named …
  • No tenant …

OAUTH_CLIENT

The OAuth client is unknown, revoked, or belongs to an organization that is not active.

When Better IAM acts as an OAuth provider, it refuses authorization, consent, and client changes for such clients.

How to fix: check the client_id, register a new client if it was revoked, or reactivate the organization (OAuth provider).

HTTP 404 · thrown by @better-iam/oauth

Example messages:

  • Client is unavailable.

TENANT_NOT_FOUND

An identity was about to be created in a tenant that does not exist.

It is a safety check inside the low-level identity creation that sign-up, invitations, federation, and root recovery share. Those flows check the tenant first and normally answer TENANT_UNAVAILABLE or NOT_FOUND instead.

How to fix: check the tenantId you passed.

HTTP 404 · thrown by @better-iam/auth

Example messages:

  • Tenant does not exist

409 Conflict

The request conflicts with the current state: a duplicate name or email, a version that changed since it was read, or a one-time token that was already used. Reload the current state and decide again.

CodeMeaning
ACCESS_EXPIRINGThe access that allows this verifiable credential ends within a minute.
ACCOUNT_LINK_CONFLICTThe external account you are linking is already linked to a different account in this tenant.
ACCOUNT_LINK_REQUIREDA first sign-in through an external provider used an email that already belongs to an account in this tenant, so the accounts must be linked explicitly.
ALREADY_INITIALIZEDBootstrap was run on a database that already has a root tenant.
ARCHIVE_CONFLICTThe audit archive already holds a file for this batch of events with different contents.
AUTHORITY_UNAVAILABLEA certificate authority cannot sign: it, or an authority above it, is disabled, revoked or expired.
BILLING_PERIOD_CLOSEDThe month has already been invoiced for this billing account, so its usage and prices can no longer change.
CHECKOUT_REQUIREDA vault secret is handed out only through check-outs, so it cannot be revealed directly.
CLAIM_UNAVAILABLEA verifiable credential type requires a claim this person has no value for.
CONFLICTThe request conflicts with a record that already exists or with the record's current state.
DATABASE_IN_USECode running inside a transaction tried to use the same SQLite or libSQL database through a second adapter instance.
DELEGATION_EXISTSAn AI agent and a person already have a pending request or an active delegation between them.
DELEGATION_PENDINGThe person has not approved this delegation yet.
DOMAIN_TAKENAnother organization has already verified this email domain.
FEATURE_LOCKEDA tenant tried to choose its own value for a feature flag (features.setOverride) that the flag's owners decide for it.
HOST_KEY_CHANGEDThe host key sent with an SSH host's renewal token is not the key the host enrolled with.
HOST_KEY_IN_USEThe SSH host key is another enrolled host's key.
HOST_NAME_TAKENAnother SSH host of the organization already uses this name or address.
HOSTNAME_TAKENAnother organization has already verified this custom hostname.
IDENTITY_EXISTSAn identity with this email address already exists in the tenant.
IDENTITY_INACTIVECredentials are issued to active members only.
INVALID_IDENTITYThe identity involved is not in a state that allows this operation.
INVALID_TRANSITIONThe record is not in a state that allows this step.
INVARIANT_VIOLATIONThe change would break an enforced access invariant, so it was rolled back.
KEY_MANAGEDA key management key belongs to another module.
KEY_STATE_INVALIDA key management key is not in a state that allows the call.
LAST_AUTHENTICATORDeleting this passkey would leave the account with no way to sign in.
LAST_OWNERThe operation would leave the organization without an active owner.
LAST_ROOT_ADMINThe operation would leave the platform without an active root administrator.
LDAP_BASE_TAKENAnother organization of this deployment already publishes its LDAP directory under this base DN.
LEGAL_HOLDThe person or subject is under a legal hold, so they cannot be erased or deleted.
LIMIT_EXCEEDEDThe tenant has reached a limit on how many of these records it may have.
METER_ARCHIVEDThe billing meter is archived and accepts no new usage.
MFA_ALREADY_ENABLEDThe person already has an authenticator app enrolled.
OWNER_INACTIVEThe owner of a lifecycle workflow run is no longer active, so the run cannot go on.
PASSKEY_EXISTSThis passkey is already registered.
PHONE_EXISTSAnother person in this organization has already verified this phone number.
RESOURCE_IN_USEThe record is still referenced by other records, so it cannot be deleted or changed this way.
SAME_DATABASEThe source and target of a store copy are the same database.
SECRET_CHECKED_OUTSomeone already holds an exclusive check-out of this vault secret, or you hold one yourself.
SECRET_PENDING_DELETIONThe vault secret is scheduled for deletion, so it cannot be read, changed, or leased.
SLUG_TAKENAnother organization already uses this slug.
SOD_CONFLICTThe change would give one person a combination of roles that a separation-of-duties rule forbids.
SSH_NOT_CONFIGUREDThe organization has no SSH certificate authority yet.
STORE_NOT_EMPTYThe target database of an import or copy already holds records.
TEAM_MANAGEDThe group belongs to a team, and its members come from the team.
TRANSACTION_ABORTEDA database operation failed inside a transaction, so the whole transaction was rolled back.
TYPE_DISABLEDThe credential type is disabled.
VERSION_CONFLICTThe record changed since you read it, so your change or acceptance was refused.
VERSION_DISABLEDThe version is disabled, so it cannot be revealed or made current.

ACCESS_EXPIRING

The access that allows this verifiable credential ends within a minute.

A self-service credential never outlives the grant that allowed vc:request: an expiring binding or group membership, a just-in-time activation, an access window, or the assumed role or temporary credentials the request came from.

How to fix: renew the access (activate the eligible role again, or ask for the binding to be extended), then request the credential.

HTTP 409 · thrown by @better-iam/server

Example messages:

  • The access that allows this credential ends within a minute

The external account you are linking is already linked to a different account in this tenant.

It happens at the callback of an OAuth or SAML linking flow when the provider identity (provider, issuer, and subject) already belongs to another identity. A provider identity links to one account at a time, and links never move silently.

How to fix: sign in with that provider to reach the account it already belongs to (link an existing account).

HTTP 409 · thrown by @better-iam/server

Example messages:

  • External identity is already linked to another account

A first sign-in through an external provider used an email that already belongs to an account in this tenant, so the accounts must be linked explicitly.

Better IAM never merges accounts by email, because anyone who could set that address at some provider could otherwise take over the account.

How to fix: sign in to the existing account (password, passkey, or another linked provider), reauthenticate, and start the provider's linking flow from that session; later sign-ins with the provider then reach that account (link an existing account).

HTTP 409 · thrown by @better-iam/server

Example messages:

  • Authenticate the existing account before linking this provider

ALREADY_INITIALIZED

Bootstrap was run on a database that already has a root tenant.

bootstrap and the better-iam bootstrap command create the root tenant and the first root administrator exactly once.

How to fix: nothing needs creating. Sign in with an existing root administrator, or run recover-root if you lost access to all of them (recovering root access).

HTTP 409 · thrown by @better-iam/server

Example messages:

  • Root already exists

ARCHIVE_CONFLICT

The audit archive already holds a file for this batch of events with different contents.

The JSONL archive sink writes each batch once, named by tenant and sequence range, and never replaces an existing file. A different batch under the same name means two deployments share one archive directory, or the database's audit chain diverged from what was archived earlier (for example after restoring an old backup). archiveAudit reports it per tenant in its failed list, and audit-archive then fails with AUDIT_ARCHIVE_FAILED.

How to fix: keep the existing file as evidence, give each deployment its own archive directory, and investigate why the chain changed (continuous audit archiving).

HTTP 409 · thrown by @better-iam/server

Example messages:

  • …/… already holds different events

AUTHORITY_UNAVAILABLE

A certificate authority cannot sign: it, or an authority above it, is disabled, revoked or expired.

pki.issueCertificate, requestCertificate and createAuthority (for a parent) refuse. A revoked authority cannot be changed at all.

How to fix: re-enable a disabled authority with updateAuthority, or issue from another authority. A revoked or expired authority has to be replaced.

HTTP 409 · thrown by @better-iam/server

Example messages:

  • A revoked authority cannot change
  • The certificate authority … is …
  • The certificate authority … has expired

BILLING_PERIOD_CLOSED

The month has already been invoiced for this billing account, so its usage and prices can no longer change.

billing.record (and iam.billing.record) refuse usage whose occurredAt falls in a month with a statement for the account, and billing.setPrice refuses an effectiveFrom that reaches back into an invoiced month.

How to fix: record late usage in the current month, or have a root administrator void the statement (billing.voidStatement), record the correction, and close the month again (billing).

HTTP 409 · thrown by @better-iam/server

Example messages:

  • A period from effectiveFrom on has already been invoiced; invoiced periods keep their prices
  • … has already been invoiced; leave period out to bill the next invoice
  • … has already been invoiced for this billing account

CHECKOUT_REQUIRED

A vault secret is handed out only through check-outs, so it cannot be revealed directly.

Secrets with a check-out policy that has required (shared privileged credentials) refuse vault.reveal, so every use is time-limited, attributed and, when the policy says so, explained. The holder of a live check-out may reveal the version they checked out.

How to fix: call vault.checkout (with a reason when the policy asks for one) and checkin when done.

HTTP 409 · thrown by @better-iam/server

Example messages:

  • This secret is handed out only through check-outs (vault.checkout)
  • A check-out reveals only the version it handed out

CLAIM_UNAVAILABLE

A verifiable credential type requires a claim this person has no value for.

Claims marked required in the credential type are never left out, so issuing is refused instead (for example, a title taken from an identity attribute that was never set).

How to fix: set the value on the person (such as the identity attribute), or make the claim optional with verifiableCredentials.updateType.

HTTP 409 · thrown by @better-iam/server

Example messages:

  • This person has no value for the claim …

CONFLICT

The request conflicts with a record that already exists or with the record's current state.

Typical causes are a duplicate (an agreement, invariant, access package, resource type, or action with the same name, a resource or domain that is already registered or claimed, a binding or group membership that already exists, an identical pending request), acting on a certification campaign that is closed, and reusing an invitation that was consumed or revoked. The storage layer also raises it for any duplicate id or tenant-unique key and for an attempt to move a record to another tenant.

How to fix: reload the current state and decide again; for a duplicate, use the existing record or choose another name.

HTTP 409 · thrown by @better-iam/core, @better-iam/oauth, @better-iam/projects, @better-iam/saml, @better-iam/scim, @better-iam/server

Example messages:

  • Record tenant cannot be changed
  • A record with this identifier or tenant key already exists
  • Client ID already exists; tenant binding is immutable.
  • A project with this name already exists
  • A SAML connection with this ID already exists.
  • The connection changed while it was being updated; try again.

DATABASE_IN_USE

Code running inside a transaction tried to use the same SQLite or libSQL database through a second adapter instance.

The adapters refuse it instead of waiting forever for a lock their own caller holds. Copying a database onto itself is the usual cause, which copyStore reports as SAME_DATABASE.

How to fix: inside a transaction, read and write through the transaction's store, and share one adapter instance per database file.

HTTP 409 · thrown by @better-iam/adapter-libsql, @better-iam/adapter-sqlite

Example messages:

  • This call chain already holds this database through another adapter instance

DELEGATION_EXISTS

An AI agent and a person already have a pending request or an active delegation between them.

One live delegation links an agent and a person at a time. delegations.grant refuses a new one while the agent's request waits for the person's decision or while the person already delegates to the agent, and delegations.request refuses a second request.

How to fix: approve or deny the pending request, or revoke the existing delegation before granting a new one (delegations).

HTTP 409 · thrown by @better-iam/server

Example messages:

  • A request or delegation between this agent and person already exists

DELEGATION_PENDING

The person has not approved this delegation yet.

delegations.assume answers it for a request the person is still deciding; a request waits up to seven days.

How to fix: poll delegations.get until status becomes active (or denied), then call assume again.

HTTP 409 · thrown by @better-iam/server

Example messages:

  • The person has not approved this delegation yet

DOMAIN_TAKEN

Another organization has already verified this email domain.

A verified domain belongs to exactly one tenant, so domains.add and domains.verify refuse it once another tenant verified it first.

How to fix: if the domain is yours, the other organization must release it with domains.delete before you can verify it.

HTTP 409 · thrown by @better-iam/server

Example messages:

  • Another organization verified this domain

FEATURE_LOCKED

A tenant tried to choose its own value for a feature flag (features.setOverride) that the flag's owners decide for it.

The flag does not allow tenants to choose (tenantOverridable is off), or a locked target set by the tenant that defines the flag covers this tenant or one of its ancestors. Withdrawing an earlier choice (value: null) is always allowed.

How to fix: ask the flag's managers (root administrators for a platform flag) to allow overrides or to lift the lock. features.list shows overridable and locked for each flag.

HTTP 409 · thrown by @better-iam/server

Example messages:

  • This flag does not let tenants choose their own value
  • A locked target decides this flag for the tenant

HOST_KEY_CHANGED

The host key sent with an SSH host's renewal token is not the key the host enrolled with.

ssh.syncHost never moves a host to another key, so a stolen renewal token cannot produce a host certificate for an attacker's key.

How to fix: after rebuilding a server, re-enroll it: get a new join token with ssh.resetJoinToken and run better-iam ssh-host-enroll.

HTTP 409 · thrown by @better-iam/server

Example messages:

  • This host key differs from the enrolled one; re-enroll the host with a new join token

HOST_KEY_IN_USE

The SSH host key is another enrolled host's key.

Each host's key identifies it to clients, and revoking a host publishes its key as revoked, so two hosts never share one (a cloned image, or a key copied from another server).

How to fix: generate a fresh host key on the server (ssh-keygen -A after removing the copied keys), then enroll.

HTTP 409 · thrown by @better-iam/server

Example messages:

  • Host … already uses this key

HOST_NAME_TAKEN

Another SSH host of the organization already uses this name or address.

A host certificate vouches for every name and address of its host, so each belongs to one host only.

How to fix: remove the address from the other host with ssh.updateHost, or use another name.

HTTP 409 · thrown by @better-iam/server

Example messages:

  • Host … already uses …

HOSTNAME_TAKEN

Another organization has already verified this custom hostname.

A verified hostname belongs to exactly one organization, so hostnames.add and hostnames.verify refuse it once another organization verified it first.

How to fix: if the hostname is yours, the other organization must release it with hostnames.delete before you can verify it.

HTTP 409 · thrown by @better-iam/server

Example messages:

  • Another organization verified this hostname

IDENTITY_EXISTS

An identity with this email address already exists in the tenant.

Emails are unique within a tenant (another tenant may have its own identity with the same address), so sign-up, identities.create, identities.createMany, identities.invite, and email changes refuse a duplicate.

How to fix: use the existing identity or another address; a person who already has an account should sign in or reset their password instead of signing up again.

HTTP 409 · thrown by @better-iam/auth, @better-iam/server

Example messages:

  • Email is already in use in this tenant
  • An identity with this email already exists in this tenant

IDENTITY_INACTIVE

Credentials are issued to active members only.

The person the credential or offer is for is disabled, deleted, past their scheduled account expiry, or not a member of this organization.

How to fix: reactivate the account first, or offer the credential to someone else.

HTTP 409 · thrown by @better-iam/server

Example messages:

  • Credentials are issued to active members only

INVALID_IDENTITY

The identity involved is not in a state that allows this operation.

accessRequests.approve refuses a request whose requester is no longer active, and credentials.create issues API keys only to active, unexpired service accounts and to AI agents in good standing. delegations.grant and delegations.approve (409) refuse an agent that is not in good standing: suspended, expired, or with a sponsor who is no longer an active person.

How to fix: re-enable the identity (or extend its expiry) first, or issue the key to a service account rather than a person. For an agent, check agents.standing, then resume it or name an active sponsor with agents.update.

HTTP 400, 409 · thrown by @better-iam/server

Example messages:

  • Requester is not active
  • The agent is not in good standing
  • API keys require an active service account
  • API keys require an agent in good standing
  • Web-identity trusts require an active service account

INVALID_TRANSITION

The record is not in a state that allows this step.

Examples: deciding or cancelling an access request, activation, or package request that is no longer pending; activating a binding that is not eligible or that nobody can approve; re-enabling an expired identity or service account before extending expiresAt; changing a deleted tenant or making a status change its lifecycle does not allow; pinging or redelivering through a paused webhook; and ending a rule-based package assignment by hand.

How to fix: reload the record, check its current status, and take a step that status allows.

HTTP 400, 409 · thrown by @better-iam/projects, @better-iam/server

Example messages:

  • Project is not active
  • Project is not archived
  • Request is …
  • Extend or clear expiresAt before resuming an expired agent
  • Deleted tenants cannot be updated
  • The credit is already revoked

INVARIANT_VIOLATION

The change would break an enforced access invariant, so it was rolled back.

Access-changing operations (role, policy, binding, group, package, relationship, and configuration changes, among others) re-evaluate the tenant's invariants in enforce mode inside their transaction. They refuse a change that creates a new violation or makes an invariant impossible to evaluate (for example by deleting the group it names); the message names the invariant and the person affected.

How to fix: preview the change with impact.preview, then adjust the change or the invariant (access invariants).

HTTP 409 · thrown by @better-iam/server

Example messages:

  • Access invariant "…" could no longer be evaluated (…); change or delete the invariant first
  • Access invariant "…": … would … … on …/…

KEY_MANAGED

A key management key belongs to another module.

Keys a certificate authority signs with (managedBy: 'pki') and keys a data protection profile encrypts with (managedBy: 'protection') are used only by that module. The keys API refuses to encrypt, decrypt, sign, generate data keys or MACs with them, or grant them, so nobody can sign a certificate body or open a value the module did not decide on. Binding such a key to another authority or profile is refused too.

How to fix: use a key of your own, or go through the module (pki.issueCertificate, protection.detokenize). The key can still be disabled or scheduled for deletion, which stops the module.

HTTP 409 · thrown by @better-iam/server

KEY_STATE_INVALID

A key management key is not in a state that allows the call.

Cryptographic calls (keys.encrypt, decrypt, sign, verify, the MAC and token calls) need an enabled key; a disabled key or a key pending deletion refuses them. State changes are refused when they do not apply, such as enabling a key pending deletion or rotating a disabled one.

How to fix: enable the key, or cancelDeletion first (the key then comes back disabled).

HTTP 409 · thrown by @better-iam/server

Example messages:

  • A key that is … cannot be …

LAST_AUTHENTICATOR

Deleting this passkey would leave the account with no way to sign in.

auth.deletePasskey refuses the last passkey when the account has no password and no verified email or phone that the deployment's passwordless sign-in could use.

How to fix: add another sign-in method first (a password or a second passkey), then delete the old passkey.

HTTP 409 · thrown by @better-iam/auth

Example messages:

  • Configure another login method before removing the last passkey

LAST_OWNER

The operation would leave the organization without an active owner.

Deleting, disabling, offboarding, or setting an expiry on the last active owner, or removing their owner flag with identities.setOwner, is refused.

How to fix: make someone else an owner with identities.setOwner first, then retry.

HTTP 409 · thrown by @better-iam/server

Example messages:

  • The last owner without an expiry cannot be given one
  • Cannot remove the final active owner

LAST_ROOT_ADMIN

The operation would leave the platform without an active root administrator.

Deleting, disabling, or offboarding the last root administrator, or removing their root flag with root.setAdministrator, is refused so the platform always keeps a way in.

How to fix: grant root to another person with root.setAdministrator first.

HTTP 409 · thrown by @better-iam/server

Example messages:

  • Cannot remove the final root administrator

LDAP_BASE_TAKEN

Another organization of this deployment already publishes its LDAP directory under this base DN.

The LDAP gateway finds the organization of a bind or search by its base DN, so each base DN belongs to one organization.

How to fix: choose a base DN of your own, such as your domain (dc=acme,dc=com).

HTTP 409 · thrown by @better-iam/server

Example messages:

  • Another organization publishes its directory under this base DN

The person or subject is under a legal hold, so they cannot be erased or deleted.

A legal hold keeps a subject's data while litigation or an investigation needs it. While it is live, fulfilling an erasure request is refused, and so is every deletion of the person's account, because identity deletion checks for holds itself: both identities.delete and an erasure request stop here. Offboarding and SCIM deprovisioning only disable an account, so they still work and keep the data.

How to fix: release the hold with privacy.releaseHold once it is no longer needed, or wait for its expiresAt. To refuse an erasure request because the data must be kept, reject it with reason exempt (privacy guide).

HTTP 409 · thrown by @better-iam/server

Example messages:

  • This subject is under a legal hold; release it or reject the request as exempt
  • This person is under a legal hold; release the hold before deleting them
  • People in this organization are under a legal hold; release the holds before deleting it

LIMIT_EXCEEDED

The tenant has reached a limit on how many of these records it may have.

Root administrators set plan limits per tenant with tenants.setLimits (members, service accounts, groups, roles, policies, registered resources, and webhooks), and every creation path checks them, including invitations, sign-up, federation, and SCIM. Fixed caps also apply: 50 webhooks, 50 agreements, and 100 invariants per tenant, and 5,000 bindings per certification campaign.

How to fix: remove records you no longer need, ask the platform operator to raise the limit, or narrow the campaign by role or subject type; tenants.usage shows the current counts.

HTTP 409 · thrown by @better-iam/auth, @better-iam/scim, @better-iam/server

Example messages:

  • This tenant has reached its member limit (…)
  • This tenant has reached its … limit (…).
  • At most … agreements
  • At most … apps
  • At most … assignments per app; assign groups instead
  • A tenant can define at most … meters

METER_ARCHIVED

The billing meter is archived and accepts no new usage.

Archiving (billing.updateMeter with archived: true) keeps a meter's history and prices but stops billing.record and iam.billing.record from adding to it.

How to fix: record on the meter that replaced it, or restore the meter with archived: false (billing).

HTTP 409 · thrown by @better-iam/server

Example messages:

  • This meter no longer accepts usage

MFA_ALREADY_ENABLED

The person already has an authenticator app enrolled.

auth.beginMfa starts a new authenticator enrollment only when none is active.

How to fix: to replace the authenticator, disable MFA with auth.disableMfa where the tenant allows it, then enroll again (managing factors).

HTTP 409 · thrown by @better-iam/auth

Example messages:

  • MFA is already enabled

OWNER_INACTIVE

The owner of a lifecycle workflow run is no longer active, so the run cannot go on.

A workflow run uses the rights of the workflow's owner when it started (or of whoever last retried it), and every step checks first that this owner is still active and unexpired. A step reached after the owner was disabled, offboarded, deleted, or passed their expiresAt fails its run with this code (recorded on the run and as workflow:run:fail); no call returns it. New runs fail the same way until someone takes the workflow over.

How to fix: an administrator who holds the steps' permissions retries the failed runs with workflows.retryRun, which makes them the runs' owner, or cancels them, and saves the workflow with workflows.update so new runs start with an active owner (lifecycle workflows).

HTTP 409 · thrown by @better-iam/server

Example messages:

  • The owner of these steps is no longer active; cancel the run, or save the workflow again and start a new one

PASSKEY_EXISTS

This passkey is already registered.

A passkey credential can be registered once for this relying party, even across organizations.

How to fix: sign in with the existing passkey, or create a new passkey on the device and register that one.

HTTP 409 · thrown by @better-iam/auth

Example messages:

  • Passkey is already registered

PHONE_EXISTS

Another person in this organization has already verified this phone number.

auth.confirmPhoneVerification keeps verified phone numbers unique within a tenant, so SMS codes and SMS sign-in reach exactly one account.

How to fix: verify a different number.

HTTP 409 · thrown by @better-iam/auth

Example messages:

  • Phone is already verified by another identity

RESOURCE_IN_USE

The record is still referenced by other records, so it cannot be deleted or changed this way.

Examples: a role that other roles inherit or packages include, a policy still attached to a role, a group that packages grant or that approves requests, a resource type with registered resources, relationships, or child types, a resource with child resources, a package that is still assigned, an action still used by policies and roles, a classification scheme change that would re-rank a level (or bring a removed one back at another rank), remove a level or compartment a clearance or label uses, or make a label invalid (clearances.updateScheme), moving a tenant that holds classification labels or live clearances out from under its scheme (tenants.reparent), and deleting a resource whose inherited classification label the scheme in force cannot read (resources.delete).

How to fix: remove or repoint the references the message names, then retry.

HTTP 400, 409 · thrown by @better-iam/server

Example messages:

  • This meter has recorded usage; archive it instead
  • Remove relationships before dropping a relation
  • Delete registered resources first
  • Delete relationships first
  • Other resource types use this type as their parent
  • Levels keep their rank: labels, including those your application asserts, may use any level

SAME_DATABASE

The source and target of a store copy are the same database.

store-copy and copyStore would otherwise read and overwrite one database in the same run.

How to fix: point --target-config at the configuration of a different, empty database (snapshots).

HTTP 409 · thrown by @better-iam/core

Example messages:

  • The source and target are the same database

SECRET_CHECKED_OUT

Someone already holds an exclusive check-out of this vault secret, or you hold one yourself.

An exclusive check-out policy hands a shared credential to one holder at a time; a caller who already holds a check-out of the secret gets this code too rather than a second check-out.

How to fix: wait until the holder checks it in or the check-out expires (vault.get shows the holder and when), renew your own with renewLease, or have an administrator end the other check-out with revokeLease.

HTTP 409 · thrown by @better-iam/server

Example messages:

  • You already hold a check-out of this secret; check it in or renew it
  • Someone else has this secret checked out; try again when it is checked in

SECRET_PENDING_DELETION

The vault secret is scheduled for deletion, so it cannot be read, changed, or leased.

vault.delete keeps a secret for a recovery window of 7 to 30 days before iam.vault.purgeDeleted removes it; meanwhile only get, restore, and an immediate delete work, and the name stays taken.

How to fix: restore the secret if it is still needed.

HTTP 409 · thrown by @better-iam/server

Example messages:

  • This secret is scheduled for deletion; restore it first
  • This secret is already scheduled for deletion
  • This secret is scheduled for deletion

SLUG_TAKEN

Another organization already uses this slug.

Slugs are globally unique aliases that sign-in screens use to find an organization; tenants.create, tenants.setSlug, and bootstrap claim them.

How to fix: choose a different slug.

HTTP 409 · thrown by @better-iam/server

Example messages:

  • This slug is already in use

SOD_CONFLICT

The change would give one person a combination of roles that a separation-of-duties rule forbids.

Operations that grant roles (bindings, group membership, identity creation, access-request and package approvals, package assignments, and configuration apply) check the tenant's prevent rules inside their transaction and roll back when they create a new conflict. Conflicts that already existed never block unrelated work.

How to fix: remove one of the conflicting roles from the person first, or grant a different role (separation of duties).

HTTP 409 · thrown by @better-iam/server

Example messages:

  • Separation of duties (…): one person cannot hold …

SSH_NOT_CONFIGURED

The organization has no SSH certificate authority yet.

How to fix: create the authorities with ssh.setup (before registering hosts).

HTTP 409 · thrown by @better-iam/server

Example messages:

  • Set up the SSH certificate authority first (ssh.setup)

STORE_NOT_EMPTY

The target database of an import or copy already holds records.

store-import and store-copy load a whole deployment in one transaction, and only into an empty, migrated database, so two deployments never mix.

How to fix: point the target configuration at a new, empty database (snapshots).

HTTP 409 · thrown by @better-iam/core

Example messages:

  • The target store already holds records (…); import into an empty, migrated database
  • The target store already holds records

TEAM_MANAGED

The group belongs to a team, and its members come from the team.

Every team owns a backing group (team:{slug}) holding the members of the team and of the teams below it. Only the teams API changes who is in it: groups.addMember, groups.updateMember, groups.removeMember, and groups.delete refuse it, and access packages, invitations, and onboarding flows cannot name it. Lifecycle workflow group steps cannot name it either.

How to fix: add or remove people with teams.addMember / teams.removeMember, or delete the team with teams.delete. Binding roles to the backing group with bindings.create is how a team gets access, and is allowed.

HTTP 409 · thrown by @better-iam/server

Example messages:

  • … belongs to a team; add people to the team instead
  • This group belongs to a team; manage its members with the teams API

TRANSACTION_ABORTED

A database operation failed inside a transaction, so the whole transaction was rolled back.

Once a statement fails, the adapters mark the transaction as doomed: catching the error and continuing does not save it, and the transaction ends with this code instead of committing partial work. You meet it in plugins or custom code that swallow storage errors inside a transaction.

How to fix: let storage errors propagate, or check for the condition before writing (adapter contract).

HTTP 409 · thrown by @better-iam/adapter-libsql, @better-iam/adapter-postgres, @better-iam/adapter-sqlite

Example messages:

  • A nested database operation failed; transaction rolled back

TYPE_DISABLED

The credential type is disabled.

Disabled types issue no new credentials and are left out of the issuer metadata. Credentials issued earlier are not affected.

How to fix: enable the type with verifiableCredentials.updateType.

HTTP 409 · thrown by @better-iam/server

Example messages:

  • This credential type is disabled

VERSION_CONFLICT

The record changed since you read it, so your change or acceptance was refused.

policies.update takes the version you edited and refuses it when someone saved a newer one, so edits never overwrite each other. agreements.accept refuses a version that is no longer current, so nobody accepts terms they have not seen.

How to fix: reload the record, show the current version, and let the person decide or accept again.

HTTP 409 · thrown by @better-iam/server

Example messages:

  • The agreement changed; review the current version before accepting
  • The scheme changed; reload it first
  • Policy version changed
  • The purpose changed; review the current version before deciding

VERSION_DISABLED

The version is disabled, so it cannot be revealed or made current.

vault.setVersionState disables a version without erasing it, for example while an old credential is being retired.

How to fix: enable it again with setVersionState, or use another version.

HTTP 409 · thrown by @better-iam/server

Example messages:

  • Only enabled versions can become current
  • This version is disabled

429 Rate limited

Too many attempts in a short time. The error carries retryAfterMs and the HTTP response a Retry-After header; wait that long before retrying.

CodeMeaning
QUOTA_EXCEEDEDThe caller's usage plan does not allow this much use of the meter right now.
RATE_LIMITEDThere were too many attempts in a short time, so the caller must wait before trying again.
TOO_MANY_REQUESTSThe person already has too many pending access requests.

QUOTA_EXCEEDED

The caller's usage plan does not allow this much use of the meter right now.

iam.quotas.enforce refuses a call that would go over the plan's throttle (its token bucket is empty) or one of its period limits (the day's or month's allowance is spent), counting nothing. The error carries retryAfterMs, when the bucket has refilled enough or the window starts over, and the HTTP response a Retry-After header. It has no retryAfterMs when the call's cost is larger than the plan could ever allow. The plan that applies is the one assigned to the API key, the agent, the person or one of their groups, else the organization's default plan for the meter (quotas).

How to fix: wait for Retry-After and retry; show the caller their allowance with quotas.status, or have an administrator assign a larger plan with quotas.assign.

HTTP 429 · thrown by @better-iam/server

RATE_LIMITED

There were too many attempts in a short time, so the caller must wait before trying again.

Sign-in, sign-up, recovery, verification, and MFA flows count attempts per person (and per client IP when ipAttempts is set) within a window from authentication.rateLimits, 15 minutes by default, which a tenant's authPolicy.maxAttempts can tighten. The error carries retryAfterMs, the window length and so the longest you may need to wait, and the HTTP response a Retry-After header.

How to fix: wait and tell the person when to try again; an administrator can clear a locked-out person's counters with identities.unlock (failed attempts and lockouts).

HTTP 429 · thrown by @better-iam/auth, @better-iam/server

Example messages:

  • Too many attempts; try again later
  • The controls were evaluated less than a minute ago
  • A key rotates on demand at most ten times a day

TOO_MANY_REQUESTS

The person already has too many pending access requests.

accessRequests.create allows 20 open requests per requester so reviewers are not flooded. Unlike RATE_LIMITED, waiting alone does not help.

How to fix: wait for pending requests to be decided, or cancel ones no longer needed with accessRequests.cancel, then open the new one.

HTTP 429 · thrown by @better-iam/server

Example messages:

  • Resolve pending requests before opening more

402 HTTP 402

CodeMeaning
SPEND_LIMIT_REACHEDAn enforced spend budget that covers this usage is already spent (status 402).

SPEND_LIMIT_REACHED

An enforced spend budget that covers this usage is already spent (status 402).

billing.record and iam.billing.record with enforceBudgets: true check the enforced budgets that cover the usage: tenant budgets up the tree, the person's own, and their teams' and department's. Budget standings may lag recorded usage by up to 30 seconds.

How to fix: raise or disable the budget (billing.updateBudget), wait for the next budget window, or ask the budget's owners; billing.check tells callers in advance (billing).

HTTP 402 · thrown by @better-iam/server

Example messages:

  • The budget “…” is spent

410 HTTP 410

CodeMeaning
VERSION_DESTROYEDThe version's value was destroyed (HTTP 410 Gone).

VERSION_DESTROYED

The version's value was destroyed (HTTP 410 Gone).

vault.destroyVersion erases a version's value for good and keeps its record as history, so it can never be revealed or made current again.

How to fix: use another version; vault.listVersions lists them with their state.

HTTP 410 · thrown by @better-iam/server

Example messages:

  • This version was destroyed

413 HTTP 413

CodeMeaning
INVALID_INPUTA field in the request is missing, has the wrong type, or is out of range.
PAYLOAD_TOO_LARGEThe request body is larger than the endpoint accepts.

INVALID_INPUT

A field in the request is missing, has the wrong type, or is out of range.

It is the general validation error of every package: an empty name, a malformed email or id, a limit outside its range, a body that is not a JSON object, or options that cannot be combined; the message names the field. A few handlers use it with status 413 for an oversized body.

How to fix: correct the input; sending the same request again fails the same way.

HTTP 400, 413 · thrown by @better-iam/auth, @better-iam/cli, @better-iam/client, @better-iam/core, @better-iam/middleware, @better-iam/next, @better-iam/oauth, @better-iam/projects, @better-iam/saml, @better-iam/scim, @better-iam/server

Example messages:

  • Unknown credential token type
  • limit must be between 1 and 1000
  • Only active people with an email can reset a password
  • A guest’s email address cannot change: ask the organization to invite the new address
  • limit must be a positive integer
  • name must not be empty

PAYLOAD_TOO_LARGE

The request body is larger than the endpoint accepts.

API calls and the SCIM administration handler accept up to 64 KiB of JSON, and the Next.js webhook handler accepts 1 MiB by default.

How to fix: send less per call, for example by splitting a batch into several requests.

HTTP 413 · thrown by @better-iam/scim, @better-iam/server

Example messages:

  • Request body is too large.
  • Request body too large

421 HTTP 421

CodeMeaning
WRONG_REGIONThe organization is served by another region's deployment (HTTP 421 Misdirected Request).

WRONG_REGION

The organization is served by another region's deployment (HTTP 421 Misdirected Request).

In a multi-region deployment (regions), each organization has a home region, and sign-in for it is served only there: tenants.lookup, domains.discover, public sign-in calls naming its tenant, and requests on its address answer this code everywhere else. An address that names the wrong region (acme.signin.eu-west-1.example.com for an organization homed in us-east-1) gets it too. The error carries region and, when one can be built, location: the organization's sign-in URL in its own region (IamClientError.region and .location in the client).

How to fix: redirect the person to location. See sign-in addresses and regions.

HTTP 421 · thrown by @better-iam/server

500 HTTP 500

CodeMeaning
INTERNAL_ERRORSomething failed on the server that Better IAM does not describe to the caller.
INVALID_HIERARCHYThe tenant hierarchy does not allow this parent and child, or the stored hierarchy is broken.
INVALID_SEALED_VALUEA stored secret could not be decrypted with the configured secrets.
KEY_MATERIAL_UNAVAILABLEA key management key's material, or a value it protects, could not be opened.
SCHEMA_VERSIONThe database was created by an incompatible version of Better IAM.
STORAGE_CORRUPTA stored record could not be decoded.
STORAGE_ERRORA database operation failed for a reason other than a conflict or a busy database.
STORE_CLOSEDThe storage adapter was used after it was closed.
TRANSACTION_ACTIVEA storage adapter was closed from inside one of its own transactions.
TRANSACTION_CLOSEDA transaction handle was used after its transaction finished.
TRANSACTION_REQUIREDA write was attempted outside a transaction.

INTERNAL_ERROR

Something failed on the server that Better IAM does not describe to the caller.

Over HTTP, any exception that is not an IamError (a bug, or one of your callbacks such as resolveResource throwing) is answered with this code and a generic message so internals never leak; direct server calls throw the original error instead. The NestJS middleware and the SCIM handlers do the same, and accessPaths.find and impact.preview use it if their read-only simulation ends abnormally.

How to fix: retry once, then find the request in your logs or http spans by its request ID and report it (observability).

HTTP 500 · thrown by @better-iam/server

Example messages:

  • Access paths did not complete
  • Impact preview did not complete
  • The CA certificate is malformed

INVALID_HIERARCHY

The tenant hierarchy does not allow this parent and child, or the stored hierarchy is broken.

tenants.create and tenants.reparent refuse a child type that the parent's type does not allow in hierarchy.types, and a move under the tenant's own subtree. With status 500 it means a stored ancestry has a cycle or exceeds the maximum depth, which points to data changed outside Better IAM.

How to fix: choose a permitted parent or adjust hierarchy in your configuration; for the 500 case, repair the tenants' parentId values (tenants and identities).

HTTP 400, 500 · thrown by @better-iam/server

Example messages:

  • Child type is not permitted
  • Child type is not permitted under the new parent
  • Cannot move a tenant under its own subtree
  • Invalid tenant hierarchy
  • The authority chain is broken

INVALID_SEALED_VALUE

A stored secret could not be decrypted with the configured secrets.

Better IAM seals sensitive values at rest (webhook signing secrets, queued delivery payloads, downstream SCIM tokens, and others) with the deployment secret. When that secret changed and the old one is not in previousSecrets, or the stored value was damaged, the value cannot be opened.

How to fix: add the secret that sealed the value to previousSecrets, then run rotate-secrets to re-seal everything under the current secret (secrets).

HTTP 400, 500 · thrown by @better-iam/auth, @better-iam/scim, @better-iam/server

Example messages:

  • Invalid sealed value
  • Outbox payload is not sealed
  • The stored downstream token cannot be opened.
  • Webhook payload is incomplete
  • The secret value cannot be opened

KEY_MATERIAL_UNAVAILABLE

A key management key's material, or a value it protects, could not be opened.

Key material is sealed under the deployment secret. This error means none of the configured secrets (secret and previousSecrets) opens it: the secret that sealed it was removed before iam.rotateSecrets() re-sealed everything. Data protection also reports a stored value that fails authentication (altered in storage) this way.

How to fix: put the old secret back in previousSecrets, run iam.rotateSecrets() until it reports done, and only then remove it. A stored value that was altered cannot be recovered: restore it from a backup or delete the token.

HTTP 500 · thrown by @better-iam/server

Example messages:

  • The key material cannot be opened with the configured secrets
  • A stored value cannot be opened

SCHEMA_VERSION

The database was created by an incompatible version of Better IAM.

The SQL adapters record a schema version and refuse to run against a database whose version they do not support.

How to fix: run the Better IAM version that created the database, or restore a backup made for this version (database operations).

HTTP 500 · thrown by @better-iam/core

Example messages:

  • Unsupported database schema version

STORAGE_CORRUPT

A stored record could not be decoded.

The adapters refuse rows whose JSON or escaped values are invalid, or whose columns disagree with the record's own id, tenant, or key, which points to data changed outside Better IAM or damaged storage.

How to fix: restore the affected data from a backup and find what wrote to the IAM tables directly (database operations).

HTTP 500 · thrown by @better-iam/core

Example messages:

  • Invalid escaped value
  • Invalid escaped keys
  • Invalid stored JSON
  • Record metadata mismatch

STORAGE_ERROR

A database operation failed for a reason other than a conflict or a busy database.

Adapters map driver errors to this code with a generic message, so SQL text and record data never reach responses; lost connections, missing tables, and full disks end up here.

How to fix: check the database's own logs and connectivity, and run better-iam migrate if the schema may be missing (database operations).

HTTP 500 · thrown by @better-iam/core

Example messages:

  • Database operation failed

STORE_CLOSED

The storage adapter was used after it was closed.

After close(), every read and transaction is refused; usually shutdown code closed the store while requests or jobs were still running.

How to fix: close the store only after in-flight work finishes, and create a new adapter if you need the database again.

HTTP 500 · thrown by @better-iam/adapter-libsql, @better-iam/adapter-postgres, @better-iam/adapter-sqlite

Example messages:

  • Database adapter is closed

TRANSACTION_ACTIVE

A storage adapter was closed from inside one of its own transactions.

How to fix: close the store only after the transaction's callback has returned, typically at process shutdown.

HTTP 500 · thrown by @better-iam/adapter-libsql, @better-iam/adapter-postgres, @better-iam/adapter-sqlite

Example messages:

  • Cannot close an adapter inside a transaction

TRANSACTION_CLOSED

A transaction handle was used after its transaction finished.

The tx store passed to a transaction callback is valid only until the callback settles; keeping it for later, or not awaiting work started inside the callback, leads here.

How to fix: await every storage call inside the callback, and never keep tx beyond it.

HTTP 500 · thrown by @better-iam/adapter-libsql, @better-iam/adapter-postgres, @better-iam/adapter-sqlite

Example messages:

  • Transaction has already completed

TRANSACTION_REQUIRED

A write was attempted outside a transaction.

Every insert, put, and delete must run inside store.transaction(), so writes are serialized and related changes commit together.

How to fix: wrap the writes in store.transaction(async (tx) => { ... }) and write through tx (adapter contract).

HTTP 500 · thrown by @better-iam/adapter-libsql, @better-iam/adapter-postgres, @better-iam/adapter-sqlite

Example messages:

  • Database writes require transaction()

501 HTTP 501

CodeMeaning
NO_AUDIT_ARCHIVEAudit archiving was requested, but no archive destination is configured.
UNSUPPORTEDThe storage adapter cannot list its collections, which a snapshot or copy needs.

NO_AUDIT_ARCHIVE

Audit archiving was requested, but no archive destination is configured.

archiveAudit and the audit-archive command need the auditArchive option, for example createJsonlAuditArchive({ directory }).

How to fix: configure an archive sink (continuous audit archiving).

HTTP 501 · thrown by @better-iam/server

Example messages:

  • Configure auditArchive (for example createJsonlAuditArchive) to archive audit events

UNSUPPORTED

The storage adapter cannot list its collections, which a snapshot or copy needs.

store-export, store-copy, and exportStore and copyStore in code discover what to copy with the store's collections() method, which a custom adapter may not implement.

How to fix: implement collections() in the adapter, or pass the collections to copy explicitly to exportStore or copyStore (optional adapter methods).

HTTP 501 · thrown by @better-iam/core

Example messages:

  • This store cannot list its collections; pass the collections to copy explicitly
  • This adapter cannot list its collections

502 HTTP 502

CodeMeaning
ENGINE_FAILEDA dynamic secret's engine failed to issue or renew a credential.
OAUTH_DISCOVERYThe Microsoft Entra ID metadata for a sign-in connection could not be fetched or is not trustworthy.
ROTATION_FAILEDA vault secret's rotator failed to apply the new value.
WEBHOOK_REJECTEDA webhook endpoint answered a delivery with a non-success HTTP status.
WEBHOOK_UNREACHABLEA webhook delivery could not connect to the endpoint, or the endpoint did not answer in time.

ENGINE_FAILED

A dynamic secret's engine failed to issue or renew a credential.

vault.lease asks the engine configured on the deployment (vault.engines) to mint a credential for the caller; the engine threw, timed out (vault.callTimeoutMs, 30 seconds by default), or returned something that does not fit the secret's format. No lease is left behind, and credentials minted for a lease that was revoked meanwhile are revoked at once.

How to fix: check the system the engine talks to and the engine's logs, then call lease again. The lease's vault:lease audit event (outcome deny) carries the error.

HTTP 400, 502 · thrown by @better-iam/server

OAUTH_DISCOVERY

The Microsoft Entra ID metadata for a sign-in connection could not be fetched or is not trustworthy.

Before a Microsoft sign-in, Better IAM downloads the directory's OpenID configuration and checks that the issuer and every endpoint belong to the expected authority; a network failure or unexpected metadata stops the sign-in.

How to fix: retry later if Microsoft was unreachable; otherwise check the connection's directory settings (Microsoft Entra ID).

HTTP 502 · thrown by @better-iam/oauth

Example messages:

  • Microsoft discovery failed.
  • Unexpected Microsoft issuer.
  • Microsoft metadata is incomplete.
  • Microsoft endpoints must use the authority.

ROTATION_FAILED

A vault secret's rotator failed to apply the new value.

vault.rotate (and the scheduled iam.vault.rotateDue) stage a new version as pending and call the secret's rotator to apply it to the system it unlocks; the rotator threw or timed out. The current version stays current and the pending one is kept, so the next attempt passes the rotator the same value. The error is recorded on the secret (rotation.lastFailure, with secret values redacted) and in a vault:rotate audit event with outcome deny; scheduled retries back off from one hour to a day.

How to fix: fix what the rotator reported, then rotate again. Rotators must be idempotent.

HTTP 502 · thrown by @better-iam/server

WEBHOOK_REJECTED

A webhook endpoint answered a delivery with a non-success HTTP status.

Deliveries run from the outbox, so this code never reaches the call that caused the event: it is recorded as the delivery's last error, and the delivery is retried with growing delays until its attempts run out.

How to fix: make the endpoint return a 2xx status, read the recorded error with webhooks.listDeliveries, and use webhooks.redeliver once it is fixed (retries).

HTTP 502 · thrown by @better-iam/server

Example messages:

  • Webhook endpoint responded with status …

WEBHOOK_UNREACHABLE

A webhook delivery could not connect to the endpoint, or the endpoint did not answer in time.

Like WEBHOOK_REJECTED, it is recorded as the delivery's last error in the outbox and retried with growing delays. Every connection failure gets the same message (refused, unresolvable, or a private or reserved address the guarded transport refuses), so the delivery history cannot be used to map networks or ports; only a timeout says so.

How to fix: check that the URL resolves to a public address and the endpoint answers within the webhook timeout, then use webhooks.redeliver (retries).

HTTP 502 · thrown by @better-iam/server

503 HTTP 503

CodeMeaning
PASSWORD_CHECK_UNAVAILABLEThe breached-password service could not be reached, and the deployment refuses passwords it cannot check.
STORAGE_BUSYThe database was too busy to complete the operation in time.

PASSWORD_CHECK_UNAVAILABLE

The breached-password service could not be reached, and the deployment refuses passwords it cannot check.

pwnedPasswords({ failClosed: true }) rejects a new password when the breach lookup fails or times out; without failClosed, the password is accepted instead.

How to fix: retry shortly; if it persists, check the server's outbound network access (password screening).

HTTP 503 · thrown by @better-iam/auth

Example messages:

  • Password screening is unavailable

STORAGE_BUSY

The database was too busy to complete the operation in time.

Lock timeouts, serialization failures, deadlocks, cancelled statements, and busy or locked SQLite files are reported with this code, and the whole transaction is rolled back.

How to fix: retry the complete operation after a short pause; if it happens often, raise lockTimeoutMs (PostgreSQL) or busyTimeoutMs (SQLite, libSQL), or shorten long transactions such as large imports (storage adapters).

HTTP 503 · thrown by @better-iam/core

Example messages:

  • Database is busy; retry the complete operation

SCIM error types

The SCIM endpoints (@better-iam/scim) answer failures in the RFC 7644 error format: { "schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"], "status", "scimType", "detail" }. scimType carries the code, in the lowercase form the RFC defines, and status the HTTP status. Identity providers branch on scimType; the codes below are the ones Better IAM sends. Better IAM errors raised while provisioning (an ACCESS_DENIED, say) keep their uppercase code in scimType.

CodeMeaning
invalidFilterA SCIM filter could not be parsed.
invalidMethodThe HTTP method is not supported on this SCIM route (HTTP 405).
invalidPathA SCIM PATCH operation or bulk operation names a path Better IAM cannot apply.
invalidSyntaxThe SCIM request body is missing or malformed.
invalidValueA SCIM attribute or query parameter has a value Better IAM does not accept.
invalidVersThe If-Match version does not match the resource's current version (HTTP 412).
mutabilityA SCIM request tried to change something that cannot be changed.
noTargetA SCIM PATCH operation has nothing to act on.
notFoundThe SCIM resource, schema, or route does not exist (HTTP 404).
tooLargeThe SCIM request body exceeds the size limit (HTTP 413).
tooManyA SCIM bulk request has more operations than the server accepts (HTTP 413).
unauthorizedThe SCIM bearer token is invalid, or the connection's tenant is not active (HTTP 401).
uniquenessThe SCIM request would create a duplicate (HTTP 409).

invalidFilter

A SCIM filter could not be parsed.

The SCIM endpoints accept the RFC 7644 filter grammar (userName eq "ada@acme.test", and, or, not, pr, value paths in brackets) over the attributes they support. An unterminated string, an unknown operator or attribute, or trailing text after a complete filter is refused.

How to fix: correct the filter; the message names the token that failed. Identity providers usually send valid filters, so this often points at a hand-written test request.

HTTP 400 · thrown by @better-iam/scim

Example messages:

  • Unterminated string in filter.
  • Unexpected character "…" in filter.
  • Attribute name is missing after the schema URN.
  • Unsupported schema URN.
  • Invalid attribute path "…".
  • Unexpected trailing filter content.

invalidMethod

The HTTP method is not supported on this SCIM route (HTTP 405).

/.search and /Bulk accept POST only, and resource routes accept the methods RFC 7644 defines for them. Better IAM sends this code for 405 answers; it is not one of the RFC's own scimType values.

How to fix: use the method the route supports, such as POST /Users/.search instead of GET.

HTTP 405 · thrown by @better-iam/scim

Example messages:

  • /.search accepts POST only.
  • Method is not supported for this resource.
  • Bulk accepts POST only.

invalidPath

A SCIM PATCH operation or bulk operation names a path Better IAM cannot apply.

The attribute is unsupported or read-only, a value filter was used on a single-valued attribute, a multi-valued attribute was modified without a value filter, or a bulk operation's path is not a supported resource route.

How to fix: patch only the mutable attributes the SCIM page lists, using value filters (emails[type eq "work"].value) for one value of a multi-valued attribute.

HTTP 400 · thrown by @better-iam/scim

Example messages:

  • Invalid bulk operation path "…".
  • Unsupported PATCH attribute "…".
  • "…" is not a mutable … attribute.
  • Invalid PATCH path "…".
  • Value filters apply only to multi-valued attributes.
  • Use a value filter to modify one value of ….

invalidSyntax

The SCIM request body is missing or malformed.

A search or bulk request without its body, a bulk request with no operations or an unsupported method, a POST bulk operation without a bulkId, an operation without data, or a body that is not JSON. A request without a JSON content type is refused with HTTP 415.

How to fix: send Content-Type: application/scim+json (or application/json) and the body RFC 7644 defines for the operation.

HTTP 400, 415 · thrown by @better-iam/scim

Example messages:

  • A SearchRequest body is required.
  • A BulkRequest body is required.
  • Bulk requests require at least one operation.
  • Operation … has an unsupported method.
  • POST operations require a bulkId.
  • … operations require data.

invalidValue

A SCIM attribute or query parameter has a value Better IAM does not accept.

Examples: a sortOrder other than ascending or descending, both attributes and excludedAttributes, more than 50 attribute names, invalid pagination, a wrong type for an attribute, or a bulk failOnErrors that is not a positive integer. Circular bulkId references in a bulk request are refused with HTTP 409.

How to fix: correct the value the message names.

HTTP 400, 409 · thrown by @better-iam/scim

Example messages:

  • sortOrder must be ascending or descending.
  • attributes and excludedAttributes are mutually exclusive.
  • Attribute lists hold at most 50 attribute names.
  • Invalid pagination.
  • … must be a string.
  • failOnErrors must be a positive integer.

invalidVers

The If-Match version does not match the resource's current version (HTTP 412).

SCIM resources carry a weak ETag (W/"<version>"). A request with If-Match succeeds only while the resource is still at that version, so two provisioning runs never overwrite each other blindly.

How to fix: read the resource again, reapply the change, and retry with the new ETag (or send If-Match: *).

HTTP 412 · thrown by @better-iam/scim

Example messages:

  • Version conflict.

mutability

A SCIM request tried to change something that cannot be changed.

Removing a required attribute, supplying an immutable or unsupported attribute, or modifying an identity SCIM does not manage. Owners, root administrators, and deleted identities are refused with HTTP 403, so a misconfigured identity provider cannot lock administrators out or revive a deleted account.

How to fix: leave read-only attributes out of the request. Change protected identities in the console or through the API instead of SCIM.

HTTP 400, 403 · thrown by @better-iam/scim

Example messages:

  • Required attribute cannot be removed.
  • SCIM cannot modify a protected identity.
  • SCIM cannot modify a deleted identity.
  • An unsupported or immutable attribute was supplied.

noTarget

A SCIM PATCH operation has nothing to act on.

A value filter matched no values of the attribute, or a remove operation has no path.

How to fix: check the value filter against the resource's current values, and give every remove a path.

HTTP 400 · thrown by @better-iam/scim

Example messages:

  • The value filter matched no values.
  • remove requires a path.

notFound

The SCIM resource, schema, or route does not exist (HTTP 404).

The resource ID is unknown to this connection's tenant, the schema or resource type is not one Better IAM serves, or, on the administration routes, the connection or group does not exist. Better IAM sends this code for 404 answers; it is not one of the RFC's own scimType values.

How to fix: list the resources again; an identity provider that kept a deleted resource's ID should recreate it.

HTTP 404 · thrown by @better-iam/scim

Example messages:

  • Schema not found.
  • Unsupported SCIM resource.
  • Resource not found.
  • Connection not found.
  • Connection or group not found.
  • SCIM resource not found.

tooLarge

The SCIM request body exceeds the size limit (HTTP 413).

Better IAM sends this code for 413 answers; it is not one of the RFC's own scimType values.

How to fix: send fewer operations or smaller values per request.

HTTP 413 · thrown by @better-iam/scim

Example messages:

  • Request body is too large.

tooMany

A SCIM bulk request has more operations than the server accepts (HTTP 413).

The limit is advertised as bulk.maxOperations in /ServiceProviderConfig.

How to fix: split the bulk request into batches below the advertised limit.

HTTP 413 · thrown by @better-iam/scim

Example messages:

  • Bulk requests accept at most … operations.

unauthorized

The SCIM bearer token is invalid, or the connection's tenant is not active (HTTP 401).

Each SCIM connection has its own token; a revoked, rotated, or mistyped token, or a token for a suspended tenant, is refused. Better IAM sends this code for 401 answers; it is not one of the RFC's own scimType values.

How to fix: copy the connection's current token into the identity provider, or rotate it and update the provider.

HTTP 401 · thrown by @better-iam/scim

Example messages:

  • Invalid SCIM credential.
  • Inactive tenant.

uniqueness

The SCIM request would create a duplicate (HTTP 409).

The userName already exists in the tenant, or the user's email belongs to another identity. SCIM never takes over an existing account by email, so an identity provider cannot claim an account it did not create.

How to fix: filter for the existing user (userName eq "…") and update it instead of creating it. For an email that belongs to an account created outside SCIM, change one of the two addresses.

HTTP 409 · thrown by @better-iam/scim

Example messages:

  • userName already exists.
  • Email belongs to an existing identity; automatic linking is disabled.

Client and CLI codes

These codes never come from the server. The browser client (@better-iam/client) throws them as IamClientError when it cannot send a request or read the answer: status is 0, or the raw HTTP status for a response that is not a Better IAM envelope. The better-iam command line throws them as CliError (an IamError with an optional hint), prints CODE: message, and exits with status 1 (2 for the usage mistakes INVALID_ARGUMENT and INVALID_COMMAND).

CodeMeaning
CANCELLEDYou pressed Ctrl+C at a better-iam prompt, such as the password prompt of login.
COMMAND_FAILEDThe program that better-iam vault-run started exited with a non-zero status.
COMMAND_NOT_FOUNDThe program named after -- in better-iam vault-run does not exist on the PATH.
CONFIG_LOAD_FAILEDThis Node.js version cannot load a TypeScript configuration file (better-iam.config.ts).
CONFIG_NOT_FOUNDThe CLI found no configuration to run the command against.
HTTP_ERRORThe server answered with an error status, but the body was not a Better IAM error envelope.
INVALID_ARGUMENTA better-iam command received a flag it does not accept, a missing value, or a value out of range.
INVALID_COMMANDThe CLI does not know the command you typed.
INVALID_CREDENTIALS_FILEThe file where the CLI saves sessions exists but is not a Better IAM credentials file.
INVALID_PATHThe browser client was asked to call a route whose path is not plain relative segments.
INVALID_RESPONSEThe server's answer could not be read as a Better IAM response.
MFA_EMAIL_UNAVAILABLEbetter-iam login --email-code was used, but this organization does not send one-time sign-in codes by email.
MFA_ENROLLMENT_REQUIREDThe account must enroll an authenticator before it can sign in, and the CLI cannot enroll one.
MISSING_DEPENDENCYThe configuration needs an optional package that is not installed.
NO_CREDENTIALS_FILEThe CLI has nowhere to save the session: HOME, APPDATA, and XDG_CONFIG_HOME are all unset.
PROFILE_IN_USEbetter-iam login would replace a saved session that belongs to another deployment.
SESSION_EXPIREDThe session saved by better-iam login has expired.
SIGN_IN_INCOMPLETEbetter-iam login finished the sign-in steps but the server returned no session.
UNEXPECTED_RESPONSEThe server asked the SSH host commands to write a file outside /etc/ssh, so the CLI refused.
UNSUPPORTED_ENVIRONMENTThe runtime lacks something device proofs need.

CANCELLED

You pressed Ctrl+C at a better-iam prompt, such as the password prompt of login.

The command stops without changing anything.

How to fix: run the command again, or pass the value as a flag or environment variable so no prompt appears.

No HTTP status · thrown by @better-iam/cli as CliError

Example messages:

  • Cancelled

COMMAND_FAILED

The program that better-iam vault-run started exited with a non-zero status.

vault-run injects the secrets into the program's environment and then reports the program's own failure. The binary exits with the program's status, so scripts see the same result as if they had run it directly.

How to fix: read the program's own output above this line; the secrets were delivered.

No HTTP status · thrown by @better-iam/cli as CliError

Example messages:

  • … exited with status …

COMMAND_NOT_FOUND

The program named after -- in better-iam vault-run does not exist on the PATH.

How to fix: check the spelling of the command after --, or give its full path.

No HTTP status · thrown by @better-iam/cli as CliError

Example messages:

  • … was not found

CONFIG_LOAD_FAILED

This Node.js version cannot load a TypeScript configuration file (better-iam.config.ts).

The CLI imports the configuration with Node's built-in type stripping, which is on by default from Node.js 22.18.

How to fix: upgrade to Node.js 22.18 or later, or rename the file to better-iam.config.mjs and remove its type annotations.

No HTTP status · thrown by @better-iam/cli as CliError

Example messages:

  • Node.js … cannot load the TypeScript configuration …

CONFIG_NOT_FOUND

The CLI found no configuration to run the command against.

Commands that work on the database look for better-iam.config.mjs (or .js or .ts) in the current directory and its parents, unless --config or BETTER_IAM_CONFIG names one; a path given that way must exist. Without a file, the CLI can still build an instance from BETTER_IAM_DATABASE_URL and BETTER_IAM_SECRET. Commands that call a deployment over HTTP also accept --url or a profile saved by better-iam login.

How to fix: run better-iam init to create a configuration, pass --config PATH, or set the environment variables (CLI).

No HTTP status · thrown by @better-iam/cli as CliError

Example messages:

  • Configuration … does not exist
  • No … (or .js/.ts) in … or its parents

HTTP_ERROR

The server answered with an error status, but the body was not a Better IAM error envelope.

The browser client throws it when the response is JSON without an error object, which usually means a proxy, a gateway, or another app answered instead of Better IAM. status is the HTTP status of that response.

How to fix: check that baseURL and basePath point at the IAM handler, and that nothing in front of it rewrites error responses.

No HTTP status · thrown by @better-iam/client as IamClientError

Example messages:

  • The IAM server rejected the request

INVALID_ARGUMENT

A better-iam command received a flag it does not accept, a missing value, or a value out of range.

The message names the problem, for example a required --tenant or --output, a flag used with the wrong command, or a number outside its allowed range; the command stops before it changes anything.

How to fix: correct the flags using the command's usage line in the CLI reference.

No HTTP status · thrown by @better-iam/cli as CliError

INVALID_COMMAND

The CLI does not know the command you typed.

How to fix: run better-iam help for the list of commands, or check the CLI reference.

No HTTP status · thrown by @better-iam/cli as CliError

Example messages:

  • Unknown command: ….…

INVALID_CREDENTIALS_FILE

The file where the CLI saves sessions exists but is not a Better IAM credentials file.

The CLI keeps login sessions in one JSON file (BETTER_IAM_CREDENTIALS, or better-iam/credentials.json in your configuration directory). It refuses to overwrite a file it cannot read, in case the path points at something else.

How to fix: delete the file, or point BETTER_IAM_CREDENTIALS at another path, then run better-iam login.

No HTTP status · thrown by @better-iam/cli as CliError

Example messages:

  • … is not a Better IAM credentials file

INVALID_PATH

The browser client was asked to call a route whose path is not plain relative segments.

Typed calls (client.groups.list) always build valid paths; this comes from $request with a path that is empty, starts or ends with /, or has a segment that does not start with a letter, contains characters other than letters, digits, _, and -, or is a reserved name such as __proto__. The client refuses it before sending anything (status 0).

How to fix: pass the route relative to the base path, such as $request('myPlugin/run', input).

No HTTP status · thrown by @better-iam/client as IamClientError

Example messages:

  • IAM routes must use plain relative path segments

INVALID_RESPONSE

The server's answer could not be read as a Better IAM response.

The browser client throws it when the body is not JSON, is not an object, has an error that lacks a code or message, or has neither data nor error. status is the HTTP status of that response. It usually means the request reached something other than the IAM handler, such as a single-page app's HTML fallback.

How to fix: check baseURL and basePath, and that the IAM handler is mounted before any catch-all route.

No HTTP status · thrown by @better-iam/client as IamClientError

Example messages:

  • The IAM server returned an invalid response
  • The IAM server returned an invalid error response

MFA_EMAIL_UNAVAILABLE

better-iam login --email-code was used, but this organization does not send one-time sign-in codes by email.

How to fix: run better-iam login without --email-code and enter a code from your authenticator app, or use a recovery code.

No HTTP status · thrown by @better-iam/cli as CliError

Example messages:

  • This organization does not offer emailed codes

MFA_ENROLLMENT_REQUIRED

The account must enroll an authenticator before it can sign in, and the CLI cannot enroll one.

The organization requires MFA and the account has no factor yet, and the CLI does not enroll factors.

How to fix: enroll a factor in the console, then run better-iam login again. When the message says so, pass --email-code to receive a one-time code instead.

No HTTP status · thrown by @better-iam/cli as CliError

Example messages:

  • This account must enroll an authenticator before it can sign in

MISSING_DEPENDENCY

The configuration needs an optional package that is not installed.

A libsql://, file:, http(s)://, or ws(s):// database URL in BETTER_IAM_DATABASE_URL needs @better-iam/adapter-libsql, which the CLI loads only when such a URL is used.

How to fix: install it next to better-iam (npm install @better-iam/adapter-libsql).

No HTTP status · thrown by @better-iam/cli as CliError

Example messages:

  • A libSQL database URL needs @better-iam/adapter-libsql

NO_CREDENTIALS_FILE

The CLI has nowhere to save the session: HOME, APPDATA, and XDG_CONFIG_HOME are all unset.

This happens in minimal containers and some CI runners.

How to fix: set BETTER_IAM_CREDENTIALS to a writable file path, or skip login and pass BETTER_IAM_TOKEN to each command.

No HTTP status · thrown by @better-iam/cli as CliError

Example messages:

  • No place to save sessions: HOME, APPDATA, and XDG_CONFIG_HOME are all unset

PROFILE_IN_USE

better-iam login would replace a saved session that belongs to another deployment.

The current profile already holds a session for a different deployment: another --url, another configuration file, or the environment configuration. The CLI refuses before signing in, so a new session never silently replaces one you still use.

How to fix: pass --profile NAME to save the new session under another name, or run better-iam logout first.

No HTTP status · thrown by @better-iam/cli as CliError

Example messages:

  • Profile … holds a session for …

SESSION_EXPIRED

The session saved by better-iam login has expired.

The CLI checks the saved expiry before calling the deployment, so the command fails without a network round trip.

How to fix: run better-iam login again (with --profile NAME for a named profile), or pass a fresh BETTER_IAM_TOKEN.

No HTTP status · thrown by @better-iam/cli as CliError

Example messages:

  • The session saved as profile … has expired

SIGN_IN_INCOMPLETE

better-iam login finished the sign-in steps but the server returned no session.

The server accepted the credentials (and the MFA code, when one was asked for) but its answer carried no session token, so the CLI has nothing to save.

How to fix: sign in once in the console to see whether the account has a step left to complete, then run better-iam login again.

No HTTP status · thrown by @better-iam/cli as CliError

Example messages:

  • Sign-in did not return a session

UNEXPECTED_RESPONSE

The server asked the SSH host commands to write a file outside /etc/ssh, so the CLI refused.

better-iam ssh-host-enroll and ssh-host-sync write the host certificate and trust files the server names, but only below /etc/ssh. A path elsewhere, or one containing .., means the response cannot be trusted.

How to fix: check that --url points at your Better IAM deployment, and report the response if it does.

No HTTP status · thrown by @better-iam/cli as CliError

Example messages:

  • The server named a file outside /etc/ssh: …

UNSUPPORTED_ENVIRONMENT

The runtime lacks something device proofs need.

createDeviceProver needs WebCrypto (crypto.subtle), which browsers offer only in secure contexts (HTTPS or localhost) and Node.js from version 22. indexedDbKeyStore needs IndexedDB, and fails the same way when the database cannot be opened or refuses a write (private browsing, storage blocked).

How to fix: serve the app over HTTPS, and outside browsers pass store: memoryKeyStore() or your own DeviceKeyStore.

No HTTP status · thrown by @better-iam/client as IamClientError

Example messages:

  • Device proofs need WebCrypto (crypto.subtle): a secure context (HTTPS or localhost) in browsers, Node 22 or later on servers
  • IndexedDB is not available here; use memoryKeyStore() or your own DeviceKeyStore outside browsers
  • The device key database could not be opened
  • The device key database refused the change
  • WebCrypto returned an ECDSA signature that is not in the raw r||s form

Decision reasons

A decision is returned, not thrown: { allowed, reason, matched }. evaluatePolicy from @better-iam/core and the administrator simulation APIs (policies.simulate, policies.whoCan, accessPaths.find) report the reason; public responses turn every denial into ACCESS_DENIED. Lowercase reasons come from the policy evaluator, uppercase ones from the server steps around it. How decisions work shows the step that produces each.

ReasonOutcomeMeaning
evaluation-limitDeniedMatching the policies exceeded the evaluator's work budget, so the request is refused as if denied.
explicit-denyDeniedA Deny statement matched, in a grant path or a global boundary. A deny always wins over any allow.
no-grantDeniedThe policy evaluator (evaluatePolicy) found no Allow statement that matches. The server reports this as NO_APPLICABLE_GRANT.
boundary-denyDeniedA global boundary has no Allow that matches, so the request is outside the ceiling even though a grant allows it.
allowedAllowedA grant allowed the request and nothing refused it.
ROOT_OVERRIDEAllowedA root administrator in an MFA user session; allowed without evaluating policies.
TENANT_INACTIVEDeniedThe tenant, or one of its ancestors, is suspended or deleted.
TENANT_MISMATCHDeniedThe session belongs to another tenant than the one the request acts in.
DELEGATION_REVOKEDDeniedA delegated session's delegation or agent is revoked, expired, or no longer active.
CREDENTIAL_AUTHORITY_REVOKEDDeniedThe authority behind the credential is gone: an API key's issuing authority, a hand-off's issuer, or a web-identity provider's registrar was revoked.
NO_APPLICABLE_GRANTDeniedNo grant path allowed the request within its ceilings (the server's form of no-grant).
UNKNOWN_ACTIONDeniedThe action is not in the tenant's permission catalog.
IMPERSONATOR_DENIEDDeniedThe member being impersonated is allowed, but the administrator impersonating them is not.
CLEARANCE_REQUIREDDeniedA party of the session lacks a clearance that dominates the resource's classification label.
CONFIRMATION_REQUIREDDeniedThe delegation holds this action until the person confirms it, so the agent must ask first.

Was this page helpful?

Better IAM is created by Sean Filimon

Last updated

On this page

Handling errors in a UICLEARANCE_REQUIRED400 Bad requestAUDIT_ARCHIVE_FAILEDAUDIT_ARCHIVE_INVALIDAUDIT_CHAIN_BROKENBREACHED_PASSWORDCONFIG_DRIFTCONFIG_EXISTSCONFIG_WARNINGSconfigurationCONFIRMATION_INVALIDCREDENTIAL_CHAINING_DISABLEDDELIVERY_REQUIREDDOCTOR_FINDINGSDOMAIN_NOT_ALLOWEDFINDINGSHOST_OUTSIDE_PATTERNSHOSTNAME_NOT_ALLOWEDINVALID_ACTIONINVALID_CIPHERTEXTINVALID_CONFIGINVALID_CREDENTIALINVALID_CSRINVALID_FILTERINVALID_NONCEINVALID_POLICYINVALID_PROOFINVALID_RECORDINVALID_REQUESTINVALID_RESOURCE_TYPEINVALID_SPONSORINVALID_TICKETINVARIANTS_BROKENINVITATION_INVALIDLINKING_DISABLEDMAX_DEPTHMISSING_ENVNOT_INITIALIZEDOAUTH_CALLBACKOAUTH_STATEPASSWORD_REUSEDPROTECTED_IDENTITYRECONCILE_ATTENTIONRESOURCE_RESOLVER_REQUIREDROLE_CHAINING_DISABLEDROLLBACKSIGNAL_REJECTEDSNAPSHOT_INVALIDSNAPSHOT_TRUNCATEDSOURCE_ADDRESS_UNKNOWNTOO_MANY_HOSTSUNREADABLE_SECRETSUNSUPPORTED_FILTERVERIFIED_EMAIL_REQUIREDWEAK_PASSWORDWEAK_TRUST_CONDITIONS401 UnauthenticatedCERTIFICATE_INVALIDDELEGATION_TOKEN_INVALIDINVALID_ASSERTIONINVALID_CHALLENGEINVALID_CREDENTIALSINVALID_MFAINVALID_PASSKEYINVALID_TOKENLOGIN_REQUIREDOAUTH_PROFILESAML_INVALIDSESSION_EXPIRINGSESSION_NETWORK_MISMATCHUNAUTHENTICATED403 ForbiddenACCESS_DENIEDCATALOG_LOCKEDCSRFCSRF_REJECTEDDELEGATION_INACTIVEDELEGATION_NOT_ALLOWEDEMAIL_UNVERIFIEDFEATURE_DISABLEDFORBIDDENGRANT_AUTHORITY_REQUIREDGUEST_NOT_ALLOWEDHOST_MISMATCHIMPERSONATION_RESTRICTEDINSUFFICIENT_SCOPEINVALID_LINKINVALID_SESSIONINVALID_TENANT_TREEIP_BLOCKEDIP_NOT_ALLOWEDMETHOD_NOT_ALLOWEDMFA_NOT_ENROLLEDMFA_REQUIREDNAME_NOT_PERMITTEDOAUTH_TENANTPASSWORD_EXPIREDPROTECTED_OPERATIONPROTECTED_RESOURCERECENT_AUTH_REQUIREDRESOURCE_MISMATCHROOT_SSH_RESTRICTEDSECURITY_KEY_REQUIREDSELF_REVIEWTENANT_INACTIVETENANT_MISMATCHTENANT_UNAVAILABLEUNTRUSTED_ORIGINWEB_IDENTITY_REJECTED404 Not foundNOT_FOUNDOAUTH_CLIENTTENANT_NOT_FOUND409 ConflictACCESS_EXPIRINGACCOUNT_LINK_CONFLICTACCOUNT_LINK_REQUIREDALREADY_INITIALIZEDARCHIVE_CONFLICTAUTHORITY_UNAVAILABLEBILLING_PERIOD_CLOSEDCHECKOUT_REQUIREDCLAIM_UNAVAILABLECONFLICTDATABASE_IN_USEDELEGATION_EXISTSDELEGATION_PENDINGDOMAIN_TAKENFEATURE_LOCKEDHOST_KEY_CHANGEDHOST_KEY_IN_USEHOST_NAME_TAKENHOSTNAME_TAKENIDENTITY_EXISTSIDENTITY_INACTIVEINVALID_IDENTITYINVALID_TRANSITIONINVARIANT_VIOLATIONKEY_MANAGEDKEY_STATE_INVALIDLAST_AUTHENTICATORLAST_OWNERLAST_ROOT_ADMINLDAP_BASE_TAKENLEGAL_HOLDLIMIT_EXCEEDEDMETER_ARCHIVEDMFA_ALREADY_ENABLEDOWNER_INACTIVEPASSKEY_EXISTSPHONE_EXISTSRESOURCE_IN_USESAME_DATABASESECRET_CHECKED_OUTSECRET_PENDING_DELETIONSLUG_TAKENSOD_CONFLICTSSH_NOT_CONFIGUREDSTORE_NOT_EMPTYTEAM_MANAGEDTRANSACTION_ABORTEDTYPE_DISABLEDVERSION_CONFLICTVERSION_DISABLED429 Rate limitedQUOTA_EXCEEDEDRATE_LIMITEDTOO_MANY_REQUESTS402 HTTP 402SPEND_LIMIT_REACHED410 HTTP 410VERSION_DESTROYED413 HTTP 413INVALID_INPUTPAYLOAD_TOO_LARGE421 HTTP 421WRONG_REGION500 HTTP 500INTERNAL_ERRORINVALID_HIERARCHYINVALID_SEALED_VALUEKEY_MATERIAL_UNAVAILABLESCHEMA_VERSIONSTORAGE_CORRUPTSTORAGE_ERRORSTORE_CLOSEDTRANSACTION_ACTIVETRANSACTION_CLOSEDTRANSACTION_REQUIRED501 HTTP 501NO_AUDIT_ARCHIVEUNSUPPORTED502 HTTP 502ENGINE_FAILEDOAUTH_DISCOVERYROTATION_FAILEDWEBHOOK_REJECTEDWEBHOOK_UNREACHABLE503 HTTP 503PASSWORD_CHECK_UNAVAILABLESTORAGE_BUSYSCIM error typesinvalidFilterinvalidMethodinvalidPathinvalidSyntaxinvalidValueinvalidVersmutabilitynoTargetnotFoundtooLargetooManyunauthorizeduniquenessClient and CLI codesCANCELLEDCOMMAND_FAILEDCOMMAND_NOT_FOUNDCONFIG_LOAD_FAILEDCONFIG_NOT_FOUNDHTTP_ERRORINVALID_ARGUMENTINVALID_COMMANDINVALID_CREDENTIALS_FILEINVALID_PATHINVALID_RESPONSEMFA_EMAIL_UNAVAILABLEMFA_ENROLLMENT_REQUIREDMISSING_DEPENDENCYNO_CREDENTIALS_FILEPROFILE_IN_USESESSION_EXPIREDSIGN_IN_INCOMPLETEUNEXPECTED_RESPONSEUNSUPPORTED_ENVIRONMENTDecision reasons