BetterIAM
Server API

auth

The auth group signs people in and lets them manage their own account security: passwords, one-time codes, passkeys, MFA, sessions, and recovery.

The auth group signs people in and lets them manage their own account security: passwords, one-time codes, passkeys, MFA, sessions, and recovery. It is what your login page, your account settings page, and the links in your recovery emails call.

Unlike the other groups, these methods are not checked against iam:* permissions. Each one is either public, because the caller is still proving who they are (sign-up, sign-in, MFA challenges, email links), or it acts only on the caller's own session and account. No method takes another person's ID: administrators manage other people's accounts through identities (revokeSessions, requestPasswordReset, unlock) and set the rules with tenants.setAuthPolicy.

Changes are recorded as auth:* audit events with the person as actor and resource, and with impersonatorId when an administrator acted through impersonation. They are written only when something happens, never as denials. The one kind of failure that is recorded is auth:signin:fail: an attempt that named a real, active account with a wrong password, code, or recovery code. The events land in the tenant's audit log, fan out to webhooks like every other event, and people read their own with listSecurityEvents.

Over HTTP every method is POST {basePath}/auth/{method}. Methods that issue a session set the session cookie, and a remembered-device token travels in a cookie of its own (see HTTP behaviour). The HTTP handler records the client's IP address and user agent for you. When you call these methods in-process, pass { headers: request.headers } as the credential of an authenticated method, or wrap a public call in iam.auth.withClient({ ip, userAgent }, fn), so network rules, per-address rate limits, and the sign-in record see the real client.

Sign-in flow and MFA challenges

Every way of signing in ends the same way. Once the first factor checks out (a password, a passwordless code or link, or a federated assertion), Better IAM decides whether the person also needs a second factor. MFA is required for root administrators, for anyone with an authenticator enrolled, and for people the tenant policy (requireMfa, or requireMfaForOwners for owners) or the deployment's requireMfa callback covers. The result, a SignInResult, has one of two shapes:

  • A session: { token, session }. The token is the bearer secret, returned once (only its hash is stored); session is the stored session without its secret-derived fields.
  • An MFA challenge: { mfaRequired: true, challenge, enrollmentRequired, emailCodeAvailable?, passkeyAvailable? }. The challenge is a single-use token, valid for five minutes, proving that the first factor passed. It is not a session: it only unlocks the calls below.
The challenge saysNext call
enrollmentRequired: false (an authenticator is enrolled)verifyMfa with a code from the authenticator, or recoverMfa with a recovery code
enrollmentRequired: true (nothing enrolled)beginMfa, then confirmMfa, to enroll an authenticator on the spot
emailCodeAvailable: truerequestMfaCode, then verifyMfa with the emailed code
passkeyAvailable: truebeginPasskeyMfa, then finishPasskeyMfa

A passkey sign-in (finishPasskeyAuthentication) skips this step: a user-verified passkey counts as both factors, so it always returns a session.

Remember this device. verifyMfa, confirmMfa, and finishPasskeyMfa accept rememberDevice: true and then also return a deviceToken with its deviceExpiresAt (together an MfaSessionResult). Passing that token to signIn or finishPasswordless later satisfies the MFA requirement from that browser; an unknown or expired token simply leads to the normal challenge. The HTTP handler keeps the token in the better-iam.device cookie and adds it to those two calls for you. The deployment's trustedDeviceLifetimeMs (30 days by default) and the tenant's trustedDeviceDays cap how long a device is remembered, either can turn the feature off, and root administrators are never remembered.

Issuing the session. Before a session is issued, the person must still be active in an active tenant, must have verified their email when the deployment requires it (EMAIL_UNVERIFIED), and must be connecting from a network the tenant accepts (IP_NOT_ALLOWED, IP_BLOCKED). The new session then:

  • records the sign-in method (password, passwordless-email, passwordless-sms, passkey, or federated), which policies see as principal.authMethod, and the client's IP address, user agent, and label;
  • carries the person's previous sign-in and the failed attempts since then as session.previousSignIn, for a "last sign-in" notice, and restarts that count;
  • ends the person's oldest sessions when the tenant caps concurrent sessions with maxSessions;
  • queues a new-sign-in email when the client is unfamiliar and sign-in notifications are on (never for a remembered device);
  • is audited as auth:session:create with the method and client, plus auth:device:trust when a device was remembered.

Failed attempts. A wrong password, authenticator or emailed code, or recovery code for a real, active account is recorded after the refusal as auth:signin:fail, with a reason of password, mfa, or recovery-code. It counts toward the next session's previousSignIn, and with the deployment's failedSignInAlerts set, the person receives one sign-in-failures email when the streak reaches that number. Unknown addresses, disabled accounts, and rate-limited attempts are never recorded, so the trail cannot be used to discover accounts. The sign-in methods and MFA guides walk through each flow.

Rate limits and tenant policy

Every public flow except beginMfa, and every authenticated flow that checks a password or code or sends a message, counts the attempt before it looks at any credential:

  1. Network blocks. A client address covered by a live security.blockNetwork block, of the tenant or platform-wide, is refused with IP_BLOCKED before anything else, and no counter moves.
  2. Per-address counter. When the deployment sets rateLimits.ipAttempts, all of a tenant's flows share one counter per client address (IPv6 addresses are counted per /64), which stops password spraying across many accounts.
  3. Per-subject counter. Each flow counts against its own subject: the email address, phone number, or account it names, or the single-use token being redeemed.
TierDefault attempts per 15-minute windowUsed by
Ordinary10 (rateLimits.attempts)signIn, reauthenticate, changePassword, beginPasskeyAuthentication with an email, and redeeming emailed tokens (verifyEmail, resetPassword, confirmEmailChange)
Sensitive5 (rateLimits.sensitiveAttempts)signUp, every call that sends an email or SMS, checking passwordless and phone codes, confirmMfa, every step that answers an MFA challenge, and finishing a passkey sign-in
Discovery10 times the ordinary limit, per client addressbeginPasskeyAuthentication without an email

Every attempt counts, successful or not. Second-factor steps count twice, per challenge and per person, because each correct password mints a new challenge with a fresh budget. A tenant policy's maxAttempts lowers the per-subject limits for that tenant (it can never raise them). An exhausted counter fails the call with RATE_LIMITED (429). The error carries retryAfterMs, the HTTP response adds a Retry-After header, and the browser client exposes it as IamClientError.retryAfterMs. Counters are stored in the IAM database unless you supply rateLimits.limiter, and identities.unlock clears a person's counters after a burst of failures.

A tenant's authentication policy can only tighten what the deployment allows. Its effects on these methods:

Policy fieldEffect
allowedMethodsA sign-in method outside the list fails with METHOD_NOT_ALLOWED before any credential is examined, so the answer never reveals whether a password was right: password (signIn, reauthenticate), passwordless-email and passwordless-sms (startPasswordless, finishPasswordless), passkey (beginPasskeyAuthentication, finishPasskeyAuthentication).
requireMfa, requireMfaForOwnersSign-ins return an MFA challenge, and people without a factor enroll on the spot. Sessions that did not pass MFA stop working with MFA_REQUIRED, and disableMfa is refused.
mfaEmailCodesOffers emailed codes (emailCodeAvailable) to people with no authenticator.
trustedDeviceDaysShortens "remember this device"; 0 turns it off.
allowedIpRangesSessions are issued and used only from these networks; anything else fails with IP_NOT_ALLOWED. Needs recorded client addresses.
bindSessionsToIpA session works only from the address it was issued from; elsewhere it is refused with SESSION_NETWORK_MISMATCH (401) and recorded as auth:session:mismatch.
sessionLifetimeMs, sessionIdleTimeoutMs, maxSessionsShorter sessions, and a cap on concurrent sessions per person (the oldest ends).
maxAttemptsLower rate limits, as above.
minPasswordLength, passwordMinClasses, passwordRejectPersonalInfo, passwordHistory, passwordMaxAgeDaysChecked by signUp, resetPassword, and changePassword (WEAK_PASSWORD, PASSWORD_REUSED); an expired password fails signIn with PASSWORD_EXPIRED until it is reset. The deployment's screening adds BREACHED_PASSWORD, and PASSWORD_CHECK_UNAVAILABLE when a fail-closed breach check cannot answer.
notifyNewSignInEmails people about sessions from unfamiliar clients.

A tenant that does not exist, is suspended, or sits under a suspended parent fails every flow with TENANT_UNAVAILABLE. Features the deployment has not enabled fail with FEATURE_DISABLED: sign-up without signUpEnabled, password flows when emailPassword is false, a passwordless channel without passwordlessEmail or passwordlessSms, passkeys without passkeys, and anything that sends an email or SMS without sendEmail or sendSms.

Sessions and recent authentication

Methods whose permission is "the caller's own session" act only on the person behind the credential you pass: { token } with a session token, or { headers } with the incoming request's headers, which carry the session cookie or an Authorization: Bearer token. Only a person's session works here. API keys and assumed-role sessions fail with UNAUTHENTICATED, as does a session that has expired, idled out, or been revoked. Each call re-checks the session too: it fails with MFA_REQUIRED when the person now needs MFA and the session did not pass it, and with IP_NOT_ALLOWED or IP_BLOCKED when its network is no longer accepted.

Methods that change how someone signs in, or that end sessions, also need recent authentication: the session must have been established within the deployment's recentAuthenticationMs (five minutes by default). An older session fails with RECENT_AUTH_REQUIRED; call reauthenticate, or sign in again, and retry with the new session. An impersonation session never qualifies, whatever its age, and fails with IMPERSONATION_RESTRICTED, so an administrator viewing as a member cannot change the member's password, factors, devices, or sessions. See sessions.

Some changes end every session of the person, including the one that made the call, and also forget their remembered devices and pending challenges (sign-in challenges and emailed links): changePassword, resetPassword, confirmEmailChange, confirmMfa, disableMfa, and deletePasskey. That way, a password, address, or factor change always cuts off anyone who was using the old one. confirmMfa returns a fresh session; after the others, the person signs in again.

Methods40
Serveriam.api.auth
Clientclient.auth
HTTPPOST /api/iam/auth/*
MethodWhat it doesAccess
beginMfaStarts enrolling a TOTP authenticator and returns its secret and an otpauth:// URI to show as a QR code.Public
beginPasskeyAuthenticationStarts a passkey sign-in and returns WebAuthn request options plus the challengeId to finish it with.Public
beginPasskeyMfaStarts answering an MFA challenge with a registered passkey instead of a code, returning WebAuthn request options bound to that sign-in.Public
beginPasskeyRegistrationReturns WebAuthn creation options for adding a passkey to the caller's account.Credential
changePasswordReplaces the caller's password after checking the current one, and signs the person out everywhere.Credential
confirmEmailChangeCompletes an email change from the link sent to the new address, then signs the person out everywhere.Public
confirmMfaChecks the first code from a newly enrolled authenticator, turns MFA on, and returns a fresh session with ten recovery codes.Public
confirmPhoneVerificationChecks the six-digit SMS code from startPhoneVerification and saves the number as the caller's verified phone.Credential
deletePasskeyRemoves one of the caller's passkeys and signs the person out everywhere.Credential
disableMfaRemoves the caller's authenticator and recovery codes, and signs the person out everywhere.Credential
finishPasskeyAuthenticationVerifies a passkey assertion and signs the person in with a session that has already passed MFA.Public
finishPasskeyMfaVerifies a passkey assertion for a pending sign-in and issues the session, optionally remembering the device.Public
finishPasskeyRegistrationVerifies the browser's registration response and saves the new passkey on the caller's account.Credential
finishPasswordlessRedeems a magic-link token or one-time code and signs the person in, or returns an MFA challenge.Public
getSessionReturns the caller's identity, their session, and the session limits in force, so a client can warn before an idle sign-out.Credential
listPasskeysLists the caller's passkeys, newest first, without key material.Credential
listSecurityEventsReturns the caller's own authentication trail, newest first: sign-ins, failed attempts, sign-outs, and changes to passwords, addresses, factors, passkeys, and devices.Credential
listSessionsLists the caller's unexpired sessions in this tenant, marking the one making the call with current: true.Credential
listTrustedDevicesLists the caller's remembered devices that have not expired, most recently used first, without their tokens.Credential
mfaStatusSummarizes what the caller has set up: whether an authenticator is enabled, how many recovery codes remain, how many passkeys and remembered devices they have, and whether this session passed MFA.Credential
reauthenticateChecks the caller's password again and returns a new session that counts as recently authenticated, or an MFA challenge.Credential
recoverMfaAnswers an MFA challenge with a single-use recovery code when the person has lost their authenticator.Public
regenerateRecoveryCodesReplaces all of the caller's recovery codes with ten new ones.Credential
renamePasskeyChanges the label of one of the caller's passkeys.Credential
requestEmailChangeEmails a confirmation link to the new address the caller wants to move to.Credential
requestEmailVerificationEmails a new verification link to an address that has not been verified yet.Public
requestMfaCodeEmails a six-digit one-time code that answers the given MFA challenge, for people with no authenticator.Public
requestPasswordResetEmails a password-reset link to a person who has forgotten their password.Public
resetPasswordSets a new password with the token from a password-reset email and ends every session of the person.Public
revokeOtherSessionsEnds every other session of the caller in this tenant ("sign out everywhere else") and returns how many ended.Credential
revokeSessionEnds one of the caller's sessions, such as one left open on a shared computer.Credential
revokeTrustedDeviceForgets one remembered device, so its next sign-in asks for MFA again.Credential
revokeTrustedDevicesForgets every remembered device of the caller and returns how many were removed.Credential
signInChecks an email address and password and returns a session, or an MFA challenge when a second factor is needed.Public
signOutEnds the caller's current session.Credential
signUpRegisters a new person in a tenant with an email address and password, when self-registration is enabled.Public
startPasswordlessSends a magic link or a one-time code for signing in without a password.Public
startPhoneVerificationTexts a six-digit code to a phone number the caller wants to verify.Credential
verifyEmailMarks an email address verified using the token from a verification email.Public
verifyMfaAnswers an MFA challenge with a code from the person's authenticator, or an emailed code, and issues the session.Public

beginMfa

Starts enrolling a TOTP authenticator and returns its secret and an otpauth:// URI to show as a QR code.

POST/api/iam/auth/beginMfa
client.auth.beginMfa()Public

Used inTyped client,Quickstart,Multi-factor authentication

  • Permission: None: public during sign-in, with the { tenantId, challenge } of an mfaRequired result; or the caller's own session, authenticated recently.
  • Audited as: not audited; confirmMfa records auth:mfa:enable.
  • Errors: INVALID_CHALLENGE when the sign-in challenge is invalid or expired; MFA_REQUIRED when a sign-in challenge is used by someone who already has an authenticator (they must use it instead); MFA_ALREADY_ENABLED (409) when a signed-in caller already has one; RECENT_AUTH_REQUIRED for an older session.

The enrollment stays pending for ten minutes and is bound to the credential that started it, so confirmMfa must present the same sign-in challenge or the same session. Calling beginMfa again replaces a pending enrollment with a new secret. Over HTTP, send { tenantId, challenge } in the body during sign-in, or {} with a session.

// signIn returned { mfaRequired: true, enrollmentRequired: true, challenge }.
const { secret, uri } = await iam.api.auth.beginMfa({ tenantId, challenge: result.challenge });
// Render `uri` as a QR code and show `secret` for manual entry, then call confirmMfa.
Returns

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/beginMfa" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{}'
Signature
iam.api.auth.beginMfa(
  credential: MfaCredential,
): Promise<{ secret: string; uri: string }>

beginPasskeyAuthentication

Starts a passkey sign-in and returns WebAuthn request options plus the challengeId to finish it with.

POST/api/iam/auth/beginPasskeyAuthentication
client.auth.beginPasskeyAuthentication()Public

Used inTyped client,Passkeys

  • Permission: None: public.
  • Audited as: not audited; finishPasskeyAuthentication records the session.
  • Errors: FEATURE_DISABLED when passkeys are not configured; METHOD_NOT_ALLOWED when the tenant does not allow passkey; INVALID_CREDENTIALS when email names no active person in the tenant; RATE_LIMITED.

Without email, the options name no credential: the browser's passkey picker or autofill offers any discoverable passkey it holds for your site, and finishPasskeyAuthentication finds the account from the credential itself. That mode is rate limited per client address with ten times the ordinary allowance, because a login page starts one on every visit. With email, the options list that person's passkeys. The challenge is valid for five minutes, so refresh an autofill request that waits longer. See passkeys.

Input

Prop

Type

Returns

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/beginPasskeyAuthentication" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "tenantId": "<tenantId>"
}'
Signature
iam.api.auth.beginPasskeyAuthentication(
  input: { tenantId: string; email?: string },
): Promise<{
  challengeId: string;
  options: Awaited<ReturnType<typeof generateAuthenticationOptions>>;
}>

beginPasskeyMfa

Starts answering an MFA challenge with a registered passkey instead of a code, returning WebAuthn request options bound to that sign-in.

POST/api/iam/auth/beginPasskeyMfa
client.auth.beginPasskeyMfa()Public

Used inTyped client,Multi-factor authentication,Passkeys

  • Permission: None: public, with the challenge of an mfaRequired result that offered passkeyAvailable.
  • Audited as: not audited; finishPasskeyMfa records the session.
  • Errors: FEATURE_DISABLED when passkeys are not configured or the person has none registered; INVALID_CHALLENGE when the sign-in challenge is invalid or expired; RATE_LIMITED (counted per challenge and per person).

The passkey challenge it returns is valid for five minutes, and the sign-in challenge must still be open when you call finishPasskeyMfa.

Input

Prop

Type

Returns

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/beginPasskeyMfa" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "tenantId": "<tenantId>",
  "challenge": "<challenge>"
}'
Signature
iam.api.auth.beginPasskeyMfa(
  input: { tenantId: string; challenge: string },
): Promise<{
  challengeId: string;
  options: Awaited<ReturnType<typeof generateAuthenticationOptions>>;
}>

beginPasskeyRegistration

Returns WebAuthn creation options for adding a passkey to the caller's account.

POST/api/iam/auth/beginPasskeyRegistration
client.auth.beginPasskeyRegistration()Credential

Used inTyped client,Passkeys

  • Permission: The caller's own session, authenticated recently.
  • Audited as: not audited; finishPasskeyRegistration records auth:passkey:create.
  • Errors: FEATURE_DISABLED when passkeys are not configured; RECENT_AUTH_REQUIRED; IMPERSONATION_RESTRICTED.

The options require a discoverable credential and user verification, request no attestation, and exclude the passkeys the person already has. The challenge is bound to this session and valid for five minutes.

// In the browser, with the typed client.
import { startRegistration } from 'better-iam/client/passkeys';

const { challengeId, options } = await client.auth.beginPasskeyRegistration();
const response = await startRegistration({ optionsJSON: options });
await client.auth.finishPasskeyRegistration({ challengeId, response, name: 'Work laptop' });
Returns

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/beginPasskeyRegistration" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{}'
Signature
iam.api.auth.beginPasskeyRegistration(
  credentials: CredentialInput,
): Promise<{
  challengeId: string;
  options: Awaited<ReturnType<typeof generateRegistrationOptions>>;
}>

changePassword

Replaces the caller's password after checking the current one, and signs the person out everywhere.

POST/api/iam/auth/changePassword
client.auth.changePassword()Credential

Used inVerification and recovery

  • Permission: The caller's own session, authenticated recently.
  • Audited as: auth:password:change; a wrong current password is recorded as auth:signin:fail.
  • Errors: INVALID_CREDENTIALS when currentPassword is wrong or the account has no password; WEAK_PASSWORD, BREACHED_PASSWORD, or PASSWORD_REUSED when the new password fails the deployment's screening or the tenant's rules; RECENT_AUTH_REQUIRED; IMPERSONATION_RESTRICTED; RATE_LIMITED.

Every session ends, including the one that made the call, along with remembered devices and pending challenges, so the person signs in again with the new password. A wrong current password counts as a failed attempt on the account, because someone holding a stolen session may be guessing it. People who have no password set one through requestPasswordReset and resetPassword instead.

Input

Prop

Type

Returns

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/changePassword" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "currentPassword": "<currentPassword>",
  "password": "<password>"
}'
Signature
iam.api.auth.changePassword(
  credentials: CredentialInput,
  input: { currentPassword: string; password: string },
): Promise<{ success: true }>

confirmEmailChange

Completes an email change from the link sent to the new address, then signs the person out everywhere.

POST/api/iam/auth/confirmEmailChange
client.auth.confirmEmailChange()Public

Used inVerification and recovery

  • Permission: None: public (the token from the email-change email is the proof).
  • Audited as: auth:email:change.
  • Errors: INVALID_CHALLENGE when the token is invalid, already used, or older than ten minutes; UNAUTHENTICATED when the session that requested the change has ended; IDENTITY_EXISTS (409) when another person in the tenant now has the address; RATE_LIMITED.

The new address is marked verified, since following the link proves the person receives mail there. Tying the confirmation to the requesting session means a change requested from a session that has since been revoked cannot complete. Every session, remembered device, and pending challenge of the person then ends.

Input

Prop

Type

Returns

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/confirmEmailChange" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "tenantId": "<tenantId>",
  "token": "<token>"
}'
Signature
iam.api.auth.confirmEmailChange(
  input: { tenantId: string; token: string },
): Promise<{ success: true }>

confirmMfa

Checks the first code from a newly enrolled authenticator, turns MFA on, and returns a fresh session with ten recovery codes.

POST/api/iam/auth/confirmMfa
client.auth.confirmMfa()Public

Used inTyped client,Quickstart,Multi-factor authentication

  • Permission: None: public with the sign-in challenge (credential: { tenantId, challenge }), or the caller's own session, authenticated recently; either way, the same credential that called beginMfa.
  • Audited as: auth:mfa:enable, then auth:session:create (and auth:device:trust with rememberDevice).
  • Errors: INVALID_CHALLENGE when there is no pending enrollment for this credential or it is older than ten minutes; INVALID_MFA for a wrong or already-used code; MFA_REQUIRED when a sign-in challenge is used by someone who already has an authenticator; RECENT_AUTH_REQUIRED; RATE_LIMITED.

Show the recovery codes once and ask the person to store them somewhere safe: they are returned only here and by regenerateRecoveryCodes, and each works once with recoverMfa. Enabling MFA ends every existing session, remembered device, and pending challenge of the person, so the returned session is the only one left. It has passed MFA and keeps the original sign-in method; over HTTP it replaces the session cookie.

const { token, recoveryCodes } = await iam.api.auth.confirmMfa({
  credential: { tenantId, challenge: result.challenge },
  code: '492039',
  rememberDevice: true,
});
Input

Prop

Type

Returns

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/confirmMfa" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "credential": {},
  "code": "<code>"
}'
Signature
iam.api.auth.confirmMfa(
  input: { credential: MfaCredential; code: string; rememberDevice?: boolean },
): Promise<SessionResult & { deviceToken?: string; deviceExpiresAt?: number } & {
  recoveryCodes: string[];
}>

confirmPhoneVerification

Checks the six-digit SMS code from startPhoneVerification and saves the number as the caller's verified phone.

POST/api/iam/auth/confirmPhoneVerification
client.auth.confirmPhoneVerification()Credential

Used inVerification and recovery

  • Permission: The caller's own session, authenticated recently; the same session that started the verification.
  • Audited as: auth:phone:verify.
  • Errors: INVALID_CHALLENGE when the code is wrong or expired, or was sent to another session or another number; PHONE_EXISTS (409) when another person in the tenant has already verified the number; INVALID_INPUT when phone is not in E.164 format; RECENT_AUTH_REQUIRED; RATE_LIMITED.

A verified phone lets the person sign in with SMS codes when the deployment enables passwordlessSms.

Input

Prop

Type

Returns

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/confirmPhoneVerification" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "phone": "<phone>",
  "code": "<code>"
}'
Signature
iam.api.auth.confirmPhoneVerification(
  credentials: CredentialInput,
  input: { phone: string; code: string },
): Promise<{ success: true }>

deletePasskey

Removes one of the caller's passkeys and signs the person out everywhere.

POST/api/iam/auth/deletePasskey
client.auth.deletePasskey()Credential

Used inPasskeys

  • Permission: The caller's own session, authenticated recently.
  • Audited as: auth:passkey:delete.
  • Errors: NOT_FOUND when the passkey is not one of the caller's; LAST_AUTHENTICATOR (409) when it is the last passkey and the person has no other way to sign in; RECENT_AUTH_REQUIRED; IMPERSONATION_RESTRICTED.

"Another way to sign in" means a password, or a verified email address or phone number with passwordless sign-in enabled for that channel. Every session, remembered device, and pending challenge ends, so no session opened with the removed passkey outlives it.

Input

Prop

Type

Returns

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/deletePasskey" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "id": "<id>"
}'
Signature
iam.api.auth.deletePasskey(
  credentials: CredentialInput,
  input: { id: string },
): Promise<{ success: true }>

disableMfa

Removes the caller's authenticator and recovery codes, and signs the person out everywhere.

POST/api/iam/auth/disableMfa
client.auth.disableMfa()Credential
  • Permission: The caller's own session, authenticated recently and signed in with MFA.
  • Audited as: auth:mfa:disable.
  • Errors: MFA_REQUIRED when the session did not pass MFA, when the person is a root administrator, or when the tenant policy or the deployment's requireMfa callback requires MFA for them; RECENT_AUTH_REQUIRED; IMPERSONATION_RESTRICTED.

Registered passkeys are kept. Every session, remembered device, and pending challenge of the person ends.

Returns

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/disableMfa" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{}'
Signature
iam.api.auth.disableMfa(
  credentials: CredentialInput,
): Promise<{ success: true }>

finishPasskeyAuthentication

Verifies a passkey assertion and signs the person in with a session that has already passed MFA.

POST/api/iam/auth/finishPasskeyAuthentication
client.auth.finishPasskeyAuthentication()Public

Used inTyped client,Passkeys

  • Permission: None: public.
  • Audited as: auth:session:create with method passkey.
  • Errors: INVALID_CHALLENGE when challengeId is invalid or expired; INVALID_PASSKEY when the passkey is not registered to an account in this tenant, its user handle does not match, or verification fails; INVALID_INPUT when response is missing; METHOD_NOT_ALLOWED; FEATURE_DISABLED; EMAIL_UNVERIFIED, IP_NOT_ALLOWED, or IP_BLOCKED from the session checks; RATE_LIMITED.

The server checks the challenge, origin, relying-party ID, user verification, signature, and signature counter, then records the passkey's lastUsedAt. Because the passkey proves possession and user verification together, the result is always a session (SessionResult), never an MFA challenge. Over HTTP it sets the session cookie.

// In the browser, with the typed client.
import { startAuthentication } from 'better-iam/client/passkeys';

const { challengeId, options } = await client.auth.beginPasskeyAuthentication({ tenantId });
const response = await startAuthentication({ optionsJSON: options });
await client.auth.finishPasskeyAuthentication({ tenantId, challengeId, response });
Input

Prop

Type

Returns

A SessionResult object:

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/finishPasskeyAuthentication" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "tenantId": "<tenantId>",
  "challengeId": "<challengeId>",
  "response": {}
}'
Signature
iam.api.auth.finishPasskeyAuthentication(
  input: {
    tenantId: string;
    challengeId: string;
    response: AuthenticationResponseJSON;
  },
): Promise<SessionResult>

finishPasskeyMfa

Verifies a passkey assertion for a pending sign-in and issues the session, optionally remembering the device.

POST/api/iam/auth/finishPasskeyMfa
client.auth.finishPasskeyMfa()Public

Used inTyped client,Multi-factor authentication,Passkeys

  • Permission: None: public, with the challengeId from beginPasskeyMfa.
  • Audited as: auth:session:create (and auth:device:trust with rememberDevice).
  • Errors: INVALID_CHALLENGE when the passkey challenge, or the sign-in challenge it belongs to, is invalid or expired; INVALID_PASSKEY when the passkey is not the person's or verification fails; INVALID_INPUT when response is missing; RATE_LIMITED (counted per challenge and per person).

Both challenges are consumed together, and the session keeps the method of the first factor, such as password. The result is an MfaSessionResult: the session, plus deviceToken and deviceExpiresAt when you asked to remember the device and the policy allows it.

Input

Prop

Type

Returns

A MfaSessionResult object:

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/finishPasskeyMfa" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "tenantId": "<tenantId>",
  "challengeId": "<challengeId>",
  "response": {}
}'
Signature
iam.api.auth.finishPasskeyMfa(
  input: {
    tenantId: string;
    challengeId: string;
    response: AuthenticationResponseJSON;
    rememberDevice?: boolean;
  },
): Promise<MfaSessionResult>

finishPasskeyRegistration

Verifies the browser's registration response and saves the new passkey on the caller's account.

POST/api/iam/auth/finishPasskeyRegistration
client.auth.finishPasskeyRegistration()Credential

Used inTyped client,Passkeys

  • Permission: The caller's own session, authenticated recently; the same session that called beginPasskeyRegistration.
  • Audited as: auth:passkey:create, with the passkey's name.
  • Errors: INVALID_CHALLENGE when the challenge is expired or belongs to another session; INVALID_PASSKEY when verification fails; PASSKEY_EXISTS (409) when the credential is already registered anywhere in the installation; INVALID_INPUT for an empty name or one over 64 characters; RECENT_AUTH_REQUIRED.

name is optional. Without it, the passkey is labeled from what the authenticator reports about itself: "This device", "Phone", "Security key", or "Passkey". Returns the new passkey's id and name.

Input

Prop

Type

Returns

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/finishPasskeyRegistration" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "challengeId": "<challengeId>",
  "response": {}
}'
Signature
iam.api.auth.finishPasskeyRegistration(
  credentials: CredentialInput,
  input: { challengeId: string; response: RegistrationResponseJSON; name?: string },
): Promise<{ id: string; name: string }>

finishPasswordless

Redeems a magic-link token or one-time code and signs the person in, or returns an MFA challenge.

POST/api/iam/auth/finishPasswordless
client.auth.finishPasswordless()Public

Used inSign-in methods

  • Permission: None: public.
  • Audited as: auth:session:create when a session is issued.
  • Errors: INVALID_CHALLENGE when the token is wrong, already used, older than five minutes, or issued for another destination, or when the person's address or phone changed since it was sent; METHOD_NOT_ALLOWED; FEATURE_DISABLED when the channel has been turned off; RATE_LIMITED (counted per destination).

Pass the same destination the message went to: a value starting with + is read as a phone number, anything else as an email address. Signing in by email also marks the address verified. The result is a SignInResult (see the sign-in flow); a deviceToken from "remember this device" satisfies MFA, and the HTTP handler adds it from the device cookie.

Input

Prop

Type

Returns

One of SessionResult | MfaRequired.

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/finishPasswordless" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "tenantId": "<tenantId>",
  "destination": "<destination>",
  "token": "<token>"
}'
Signature
iam.api.auth.finishPasswordless(
  input: { tenantId: string; destination: string; token: string; deviceToken?: string },
): Promise<SignInResult>

getSession

Returns the caller's identity, their session, and the session limits in force, so a client can warn before an idle sign-out.

POST/api/iam/auth/getSession
client.auth.getSession()Credential

Used inTyped client,Nuxt,React,Sessions

  • Permission: The caller's own session.
  • Audited as: not audited.
  • Errors: UNAUTHENTICATED when the session is missing, expired, idle, or revoked.

limits holds the tenant's lifetimeMs and idleTimeoutMs, idleExpiresAt (when the session lapses if nothing uses it again, never later than its absolute expiry), and now, the server's clock, so a client can correct for its own clock skew. Like every authenticated call, it counts as activity, which makes it the natural target for a "Stay signed in" button. The identity comes back without its password hash.

Returns

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/getSession" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{}'
Signature
iam.api.auth.getSession(
  credentials: CredentialInput,
): Promise<{
  identity: SafeIdentity;
  session: SafeSession;
  limits: {
    lifetimeMs: number;
    idleTimeoutMs: number;
    idleExpiresAt: number;
    now: number;
  };
}>

listPasskeys

Lists the caller's passkeys, newest first, without key material.

POST/api/iam/auth/listPasskeys
client.auth.listPasskeys()Credential

Used inPasskeys

  • Permission: The caller's own session.
  • Audited as: not audited.

Each entry has its id, name, createdAt, lastUsedAt (the last sign-in or MFA answer), deviceType (singleDevice, or multiDevice for a synced passkey), backedUp, transports, and, when the authenticator reports one, the aaguid that identifies its model.

Returns

An array of SafePasskey.

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/listPasskeys" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{}'
Signature
iam.api.auth.listPasskeys(
  credentials: CredentialInput,
): Promise<SafePasskey[]>

listSecurityEvents

Returns the caller's own authentication trail, newest first: sign-ins, failed attempts, sign-outs, and changes to passwords, addresses, factors, passkeys, and devices.

POST/api/iam/auth/listSecurityEvents
client.auth.listSecurityEvents()Credential
  • Permission: The caller's own session.
  • Audited as: not audited.
  • Errors: INVALID_INPUT when limit is not an integer from 1 to 200.

It returns up to limit (50 by default) of the auth:* audit events the person was the actor of in this tenant. Each event carries impersonatorId when an administrator acted through impersonation, sequence (its position in the audit chain), and metadata such as the client's ip and userAgent, the sign-in method, or a failure's reason. Use it for an account page's recent activity; the tenant's full log is audit.list, which needs iam:audit:read.

Input

An optional object:

Prop

Type

Returns

An array of object.

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/listSecurityEvents" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{}'
Signature
iam.api.auth.listSecurityEvents(
  credentials: CredentialInput,
  input?: { limit?: number } | undefined,
): Promise<{
  id: string;
  action: string;
  timestamp: number;
  impersonatorId?: string;
  sequence?: number;
  metadata?: Record<string, Json>;
}[]>

listSessions

Lists the caller's unexpired sessions in this tenant, marking the one making the call with current: true.

POST/api/iam/auth/listSessions
client.auth.listSessions()Credential

Used inSessions,Sign-in and devices

  • Permission: The caller's own session.
  • Audited as: not audited.

Each session shows its sign-in method, its client details, and its timestamps, never its token. Sessions an administrator opened as the person through impersonation appear too, with impersonatorId, so people can see them and end them with revokeSession.

Returns

An array of SafeSession & object.

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/listSessions" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{}'
Signature
iam.api.auth.listSessions(
  credentials: CredentialInput,
): Promise<(SafeSession & { current: boolean })[]>

listTrustedDevices

Lists the caller's remembered devices that have not expired, most recently used first, without their tokens.

POST/api/iam/auth/listTrustedDevices
client.auth.listTrustedDevices()Credential

Used inMulti-factor authentication,Sign-in and devices

  • Permission: The caller's own session.
  • Audited as: not audited.

Each device records when it was remembered, when it expires, when it last vouched for a sign-in (lastUsedAt), and the client (IP address, user agent, label) that completed MFA.

Returns

An array of SafeTrustedDevice.

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/listTrustedDevices" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{}'
Signature
iam.api.auth.listTrustedDevices(
  credentials: CredentialInput,
): Promise<SafeTrustedDevice[]>

mfaStatus

Summarizes what the caller has set up: whether an authenticator is enabled, how many recovery codes remain, how many passkeys and remembered devices they have, and whether this session passed MFA.

POST/api/iam/auth/mfaStatus
client.auth.mfaStatus()Credential
  • Permission: The caller's own session.
  • Audited as: not audited.

Use it to drive an account security page, for example to prompt for new recovery codes when recoveryCodesRemaining runs low.

Returns

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/mfaStatus" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{}'
Signature
iam.api.auth.mfaStatus(
  credentials: CredentialInput,
): Promise<{
  enabled: boolean;
  recoveryCodesRemaining: number;
  passkeys: number;
  trustedDevices: number;
  sessionMfa: boolean;
}>

reauthenticate

Checks the caller's password again and returns a new session that counts as recently authenticated, or an MFA challenge.

POST/api/iam/auth/reauthenticate
client.auth.reauthenticate()Credential

Used inSessions

  • Permission: The caller's own session, but not an impersonation session.
  • Audited as: auth:session:create for the new session; a wrong password is recorded as auth:signin:fail.
  • Errors: INVALID_CREDENTIALS for a wrong password or an account without one; METHOD_NOT_ALLOWED when the tenant does not allow password; IMPERSONATION_RESTRICTED; RATE_LIMITED.

Call it when an operation answers RECENT_AUTH_REQUIRED, then retry with the new session. It is a full sign-in: when the person needs MFA the result is an mfaRequired challenge (a remembered device does not skip it here), and the session comes from verifyMfa or another second-factor call. The old session stays valid; over HTTP the cookie switches to the new one. People without a password get a recent session by signing in again, for example with a passkey.

const result = await iam.api.auth.reauthenticate({ headers: request.headers }, { password });
Input

Prop

Type

Returns

One of SessionResult | MfaRequired.

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/reauthenticate" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "password": "<password>"
}'
Signature
iam.api.auth.reauthenticate(
  credentials: CredentialInput,
  input: { password: string },
): Promise<SignInResult>

recoverMfa

Answers an MFA challenge with a single-use recovery code when the person has lost their authenticator.

POST/api/iam/auth/recoverMfa
client.auth.recoverMfa()Public

Used inMulti-factor authentication

  • Permission: None: public, with the challenge of an mfaRequired result.
  • Audited as: auth:mfa:recover, then auth:session:create; a wrong code is recorded as auth:signin:fail.
  • Errors: INVALID_MFA when the code is wrong or already used, or the person has no authenticator enrolled; INVALID_CHALLENGE when the challenge is invalid or expired; RATE_LIMITED (counted per challenge and per person).

The code is used up. The authenticator stays enrolled, so once signed in the person should check mfaStatus and replace their codes with regenerateRecoveryCodes. This call cannot remember the device.

Input

Prop

Type

Returns

A SessionResult object:

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/recoverMfa" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "tenantId": "<tenantId>",
  "challenge": "<challenge>",
  "code": "<code>"
}'
Signature
iam.api.auth.recoverMfa(
  input: { tenantId: string; challenge: string; code: string },
): Promise<SessionResult>

regenerateRecoveryCodes

Replaces all of the caller's recovery codes with ten new ones.

POST/api/iam/auth/regenerateRecoveryCodes
client.auth.regenerateRecoveryCodes()Credential
  • Permission: The caller's own session, authenticated recently and signed in with MFA.
  • Audited as: auth:mfa:recovery-codes.
  • Errors: MFA_REQUIRED when the session did not pass MFA; MFA_NOT_ENROLLED when no authenticator is enabled; RECENT_AUTH_REQUIRED; IMPERSONATION_RESTRICTED.

The old codes stop working at once. Show the new ones one time: only their hashes are stored, so they cannot be read back.

Returns

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/regenerateRecoveryCodes" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{}'
Signature
iam.api.auth.regenerateRecoveryCodes(
  credentials: CredentialInput,
): Promise<{ recoveryCodes: string[] }>

renamePasskey

Changes the label of one of the caller's passkeys.

POST/api/iam/auth/renamePasskey
client.auth.renamePasskey()Credential

Used inPasskeys

  • Permission: The caller's own session.
  • Audited as: auth:passkey:rename, with the new name.
  • Errors: NOT_FOUND when the passkey is not one of the caller's; INVALID_INPUT for an empty name or one over 64 characters.

Unlike the other passkey changes, renaming does not need recent authentication.

Input

Prop

Type

Returns

A SafePasskey object:

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/renamePasskey" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "id": "<id>",
  "name": "<name>"
}'
Signature
iam.api.auth.renamePasskey(
  credentials: CredentialInput,
  input: { id: string; name: string },
): Promise<SafePasskey>

requestEmailChange

Emails a confirmation link to the new address the caller wants to move to.

POST/api/iam/auth/requestEmailChange
client.auth.requestEmailChange()Credential

Used inVerification and recovery

  • Permission: The caller's own session, authenticated recently.
  • Audited as: not audited; confirmEmailChange records auth:email:change.
  • Errors: INVALID_INPUT for an invalid address; FEATURE_DISABLED when email delivery is not configured; RECENT_AUTH_REQUIRED; IMPERSONATION_RESTRICTED; RATE_LIMITED.

The address does not change until someone with access to the new mailbox follows the email-change link, within ten minutes and while this session is still alive. Whether another person already uses the address is checked at confirmation. Administrators change an address directly with identities.update.

Input

Prop

Type

Returns

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/requestEmailChange" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "email": "<email>"
}'
Signature
iam.api.auth.requestEmailChange(
  credentials: CredentialInput,
  input: { email: string },
): Promise<{ success: true }>

requestEmailVerification

Emails a new verification link to an address that has not been verified yet.

POST/api/iam/auth/requestEmailVerification
client.auth.requestEmailVerification()Public

Used inVerification and recovery

  • Permission: None: public.
  • Audited as: not audited; verifyEmail records auth:email:verify.
  • Errors: FEATURE_DISABLED when email delivery is not configured; RATE_LIMITED.

It always succeeds, so it never reveals whether an account exists: the verify-email message, valid for 24 hours, goes out only when an active person in the tenant has that address unverified. Use it for a "resend verification email" button.

Input

Prop

Type

Returns

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/requestEmailVerification" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "tenantId": "<tenantId>",
  "email": "<email>"
}'
Signature
iam.api.auth.requestEmailVerification(
  input: { tenantId: string; email: string },
): Promise<{ success: true }>

requestMfaCode

Emails a six-digit one-time code that answers the given MFA challenge, for people with no authenticator.

POST/api/iam/auth/requestMfaCode
client.auth.requestMfaCode()Public

Used inMulti-factor authentication

  • Permission: None: public, with the challenge of a sign-in that offered emailCodeAvailable.
  • Audited as: not audited.
  • Errors: FEATURE_DISABLED when emailed codes were not offered for this sign-in or the person has no verified address; INVALID_CHALLENGE when the challenge is invalid or expired; RATE_LIMITED (counted per challenge and per person).

The code works once with verifyMfa until the returned expiresAt, which never outlives the sign-in challenge. Requesting again replaces the code. Emailed codes are offered only to people with nothing enrolled and a verified address, never to root administrators, and only when the tenant's or the deployment's mfaEmailCodes allows them.

Input

Prop

Type

Returns

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/requestMfaCode" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "tenantId": "<tenantId>",
  "challenge": "<challenge>"
}'
Signature
iam.api.auth.requestMfaCode(
  input: { tenantId: string; challenge: string },
): Promise<{ success: true; expiresAt: number }>

requestPasswordReset

Emails a password-reset link to a person who has forgotten their password.

POST/api/iam/auth/requestPasswordReset
client.auth.requestPasswordReset()Public

Used inVerification and recovery

  • Permission: None: public.
  • Audited as: not audited; resetPassword records auth:password:reset.
  • Errors: FEATURE_DISABLED when email delivery is not configured or password sign-in is turned off; RATE_LIMITED.

It always succeeds, so it never reveals whether an account exists. The password-reset message goes out only to an active person whose address is verified, and its token is valid for ten minutes. Administrators can send the same email for a member with identities.requestPasswordReset. See recovery.

Input

Prop

Type

Returns

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/requestPasswordReset" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "tenantId": "<tenantId>",
  "email": "<email>"
}'
Signature
iam.api.auth.requestPasswordReset(
  input: { tenantId: string; email: string },
): Promise<{ success: true }>

resetPassword

Sets a new password with the token from a password-reset email and ends every session of the person.

POST/api/iam/auth/resetPassword
client.auth.resetPassword()Public

Used inVerification and recovery

  • Permission: None: public (the token is the proof).
  • Audited as: auth:password:reset.
  • Errors: INVALID_CHALLENGE when the token is invalid, already used, or expired; WEAK_PASSWORD, BREACHED_PASSWORD, or PASSWORD_REUSED when the new password fails the deployment's screening or the tenant's rules; FEATURE_DISABLED when password sign-in is turned off; RATE_LIMITED.

Recovery never signs the person in and never removes MFA: they sign in afterwards with the new password and their second factor. Every session, remembered device, and pending challenge ends, including any other reset links. The new password also restarts the tenant's password-age clock (passwordMaxAgeDays), which is how someone whose password expired gets back in.

Input

Prop

Type

Returns

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/resetPassword" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "tenantId": "<tenantId>",
  "token": "<token>",
  "password": "<password>"
}'
Signature
iam.api.auth.resetPassword(
  input: { tenantId: string; token: string; password: string },
): Promise<{ success: true }>

revokeOtherSessions

Ends every other session of the caller in this tenant ("sign out everywhere else") and returns how many ended.

POST/api/iam/auth/revokeOtherSessions
client.auth.revokeOtherSessions()Credential

Used inSessions,Sign-in and devices

  • Permission: The caller's own session, authenticated recently.
  • Audited as: auth:session:revoke-others.
  • Errors: RECENT_AUTH_REQUIRED; IMPERSONATION_RESTRICTED.

This includes sessions an administrator opened as the person through impersonation. Remembered devices are not affected, so a device that is still remembered can sign in again without MFA; use revokeTrustedDevices to forget those too.

Returns

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/revokeOtherSessions" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{}'
Signature
iam.api.auth.revokeOtherSessions(
  credentials: CredentialInput,
): Promise<{ revoked: number }>

revokeSession

Ends one of the caller's sessions, such as one left open on a shared computer.

POST/api/iam/auth/revokeSession
client.auth.revokeSession()Credential

Used inSessions

  • Permission: The caller's own session, authenticated recently.
  • Audited as: auth:session:revoke.
  • Errors: NOT_FOUND when the session is not one of the caller's; RECENT_AUTH_REQUIRED; IMPERSONATION_RESTRICTED.

Take the id from listSessions. Any impersonation sessions the person opened through the ended session end with it.

Input

Prop

Type

Returns

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/revokeSession" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "sessionId": "<sessionId>"
}'
Signature
iam.api.auth.revokeSession(
  credentials: CredentialInput,
  input: { sessionId: string },
): Promise<{ success: true }>

revokeTrustedDevice

Forgets one remembered device, so its next sign-in asks for MFA again.

POST/api/iam/auth/revokeTrustedDevice
client.auth.revokeTrustedDevice()Credential

Used inMulti-factor authentication,Sign-in and devices

  • Permission: The caller's own session, authenticated recently.
  • Audited as: auth:device:revoke.
  • Errors: NOT_FOUND when the device is not one of the caller's; RECENT_AUTH_REQUIRED; IMPERSONATION_RESTRICTED.

Sessions already issued on that device keep working; end them with revokeSession.

Input

Prop

Type

Returns

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/revokeTrustedDevice" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "deviceId": "<deviceId>"
}'
Signature
iam.api.auth.revokeTrustedDevice(
  credentials: CredentialInput,
  input: { deviceId: string },
): Promise<{ success: true }>

revokeTrustedDevices

Forgets every remembered device of the caller and returns how many were removed.

POST/api/iam/auth/revokeTrustedDevices
client.auth.revokeTrustedDevices()Credential

Used inSign-in and devices

  • Permission: The caller's own session, authenticated recently.
  • Audited as: auth:device:revoke, once, when at least one device was removed.
  • Errors: RECENT_AUTH_REQUIRED; IMPERSONATION_RESTRICTED.

Over HTTP it also clears the better-iam.device cookie in the calling browser. Existing sessions keep working.

Returns

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/revokeTrustedDevices" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{}'
Signature
iam.api.auth.revokeTrustedDevices(
  credentials: CredentialInput,
): Promise<{ revoked: number }>

signIn

Checks an email address and password and returns a session, or an MFA challenge when a second factor is needed.

POST/api/iam/auth/signIn
client.auth.signIn()Public

Used inTyped client,Express, Hono, and Fastify,Nuxt,React Router,React,SvelteKitand 8 more

  • Permission: None: public.
  • Audited as: auth:session:create when a session is issued; auth:signin:fail when a real, active account was given a wrong password.
  • Errors: INVALID_CREDENTIALS (401) for an unknown address, a wrong password, an inactive account, or an account without a password, all indistinguishable; EMAIL_UNVERIFIED when verification is required and still pending; PASSWORD_EXPIRED when the tenant's maximum password age has passed; METHOD_NOT_ALLOWED; TENANT_UNAVAILABLE; IP_NOT_ALLOWED or IP_BLOCKED; FEATURE_DISABLED when password sign-in is turned off; RATE_LIMITED.

The tenant is never inferred from the email address: you always pass tenantId. Unknown addresses go through the same password-hash work as real ones, so response times do not reveal which accounts exist. Pass deviceToken from an earlier "remember this device" to skip the second factor in that browser. See the sign-in flow for what to do with each result.

const result = await iam.auth.withClient({ ip, userAgent }, () =>
  iam.api.auth.signIn({ tenantId, email: 'ada@example.com', password }),
);
if ('mfaRequired' in result) {
  // Ask for a code, then: iam.api.auth.verifyMfa({ tenantId, challenge: result.challenge, code })
} else {
  // result.token is the bearer token; result.session describes the new session.
}
Input

Prop

Type

Returns

One of SessionResult | MfaRequired.

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/signIn" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "tenantId": "<tenantId>",
  "email": "<email>",
  "password": "<password>"
}'
Signature
iam.api.auth.signIn(
  input: { tenantId: string; email: string; password: string; deviceToken?: string },
): Promise<SignInResult>

signOut

Ends the caller's current session.

POST/api/iam/auth/signOut
client.auth.signOut()Credential

Used inSupport and privacy

  • Permission: The caller's own session.
  • Audited as: auth:session:revoke.
  • Errors: UNAUTHENTICATED when the session has already ended.

Impersonation sessions the person opened through this session end with it. Over HTTP the session cookie is cleared even when the sign-out is refused, for example because the session had already idled out. Remembered devices stay remembered; revokeTrustedDevices forgets them.

Returns

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/signOut" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{}'
Signature
iam.api.auth.signOut(
  credentials: CredentialInput,
): Promise<{ success: true }>

signUp

Registers a new person in a tenant with an email address and password, when self-registration is enabled.

POST/api/iam/auth/signUp
client.auth.signUp()Public

Used inSign-in methods

  • Permission: None: public.
  • Audited as: auth:identity:create.
  • Errors: FEATURE_DISABLED when signUpEnabled is off or password sign-in is turned off; FORBIDDEN for the root tenant, whose people only administrators create; IDENTITY_EXISTS (409) when the address is taken in this tenant; LIMIT_EXCEEDED when the tenant has reached its member limit; WEAK_PASSWORD or BREACHED_PASSWORD; TENANT_UNAVAILABLE; RATE_LIMITED.

Sign-up does not sign the person in. When email verification is required (the default once sign-up is enabled), it queues a verify-email message valid for 24 hours and returns verificationRequired: true, and signIn fails with EMAIL_UNVERIFIED until verifyEmail succeeds. The new person is never an owner or a root administrator, and signing up grants no access by itself: bind roles with bindings.create or add them to a group.

Input

Prop

Type

Returns

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/signUp" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "tenantId": "<tenantId>",
  "email": "<email>",
  "name": "<name>",
  "password": "<password>"
}'
Signature
iam.api.auth.signUp(
  input: { tenantId: string; email: string; name: string; password: string },
): Promise<{ identity: SafeIdentity; verificationRequired: boolean }>

startPasswordless

Sends a magic link or a one-time code for signing in without a password.

POST/api/iam/auth/startPasswordless
client.auth.startPasswordless()Public

Used inSign-in methods

  • Permission: None: public.
  • Audited as: not audited; finishPasswordless records the session.
  • Errors: INVALID_INPUT for an unknown channel or kind, a malformed destination, or SMS with kind: 'magic-link'; FEATURE_DISABLED when the channel is not enabled; METHOD_NOT_ALLOWED when the tenant does not allow it; RATE_LIMITED (counted per destination).

It always succeeds, whether or not the destination belongs to anyone, so it never reveals which accounts exist. A message goes out only to an active person with that email address, or with that phone number verified; SMS supports codes only. The magic-link token or six-digit code is valid for five minutes and works once, with finishPasswordless. The message uses the magic-link or code template, which your sendEmail or sendSms callback turns into an email or text.

await iam.api.auth.startPasswordless({
  tenantId,
  destination: 'ada@example.com',
  channel: 'email',
  kind: 'code',
});
// Later, with the code the person typed:
const result = await iam.api.auth.finishPasswordless({
  tenantId,
  destination: 'ada@example.com',
  token: '038514',
});
Input

Prop

Type

Returns

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/startPasswordless" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "tenantId": "<tenantId>",
  "destination": "<destination>",
  "channel": "email",
  "kind": "magic-link"
}'
Signature
iam.api.auth.startPasswordless(
  input: {
    tenantId: string;
    destination: string;
    channel: 'email' | 'sms';
    kind: 'magic-link' | 'code';
  },
): Promise<{ success: true }>

startPhoneVerification

Texts a six-digit code to a phone number the caller wants to verify.

POST/api/iam/auth/startPhoneVerification
client.auth.startPhoneVerification()Credential

Used inVerification and recovery

  • Permission: The caller's own session, authenticated recently.
  • Audited as: not audited; confirmPhoneVerification records auth:phone:verify.
  • Errors: INVALID_INPUT when phone is not in E.164 format (such as +14155550100); FEATURE_DISABLED when SMS delivery is not configured; RECENT_AUTH_REQUIRED; IMPERSONATION_RESTRICTED; RATE_LIMITED.

The code, sent with the phone-verify template, is valid for five minutes and only from the session that requested it.

Input

Prop

Type

Returns

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/startPhoneVerification" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "phone": "<phone>"
}'
Signature
iam.api.auth.startPhoneVerification(
  credentials: CredentialInput,
  input: { phone: string },
): Promise<{ success: true }>

verifyEmail

Marks an email address verified using the token from a verification email.

POST/api/iam/auth/verifyEmail
client.auth.verifyEmail()Public

Used inVerification and recovery

  • Permission: None: public (the token is the proof).
  • Audited as: auth:email:verify.
  • Errors: INVALID_CHALLENGE when the token is invalid, already used, or older than 24 hours; UNAUTHENTICATED when the account is no longer active; RATE_LIMITED.

It does not sign the person in: send them to your sign-in page afterwards.

Input

Prop

Type

Returns

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/verifyEmail" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "tenantId": "<tenantId>",
  "token": "<token>"
}'
Signature
iam.api.auth.verifyEmail(
  input: { tenantId: string; token: string },
): Promise<{ success: true }>

verifyMfa

Answers an MFA challenge with a code from the person's authenticator, or an emailed code, and issues the session.

POST/api/iam/auth/verifyMfa
client.auth.verifyMfa()Public

Used inTyped client,Nuxt,Multi-factor authentication,Sign-in and devices

  • Permission: None: public, with the challenge of an mfaRequired result.
  • Audited as: auth:session:create (and auth:device:trust with rememberDevice); a wrong code is recorded as auth:signin:fail.
  • Errors: INVALID_MFA for a wrong, expired, or already-used code; MFA_NOT_ENROLLED (403) when the person has no authenticator and no emailed code was requested; INVALID_CHALLENGE when the challenge is invalid or expired; RATE_LIMITED (counted per challenge and per person).

Authenticator codes are six digits. The current 30-second step and one on either side are accepted, and each step only once, so a captured code cannot be replayed. With rememberDevice: true the result also carries a deviceToken for later sign-ins (see the sign-in flow). Over HTTP it sets the session cookie and, when a device was remembered, the device cookie.

const session = await iam.api.auth.verifyMfa({
  tenantId,
  challenge: result.challenge,
  code: '492039',
  rememberDevice: true,
});
Input

Prop

Type

Returns

A MfaSessionResult object:

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/auth/verifyMfa" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "tenantId": "<tenantId>",
  "challenge": "<challenge>",
  "code": "<code>"
}'
Signature
iam.api.auth.verifyMfa(
  input: {
    tenantId: string;
    challenge: string;
    code: string;
    rememberDevice?: boolean;
  },
): Promise<MfaSessionResult>

Was this page helpful?

Better IAM is created by Sean Filimon

Last updated

On this page