identities
Identities are the people and service accounts that sign in to a tenant, and this group manages them from invitation to offboarding.
Identities are the people and service accounts that sign in to a tenant, and this group manages them from invitation to offboarding. Every identity belongs to exactly one tenant, and its email is unique only within that tenant: another tenant may hold a separate identity with the same address. The group creates people directly or through email invitations, reads their sessions, groups, and effective roles, changes their profile, status, and ownership, answers data-subject requests, and removes access cleanly when someone leaves.
Service accounts (kind: 'service') live in the same directory. You create them with
serviceAccounts.create, but reading, listing, disabling,
offboarding, and deleting them go through these methods and the same iam:identities:* actions. See
tenants and identities for the model.
Invitations
An invitation lets an administrator decide what a new member receives, while the member proves they control the
address and chooses their own password. invite stores the email, optional roles and groups, and only a
hash of a single-use token, then queues a member-invitation email through the
delivery outbox. The token is sealed inside that message, so neither the inviter nor the
API response ever sees it. Your invitation page reads the token from the link and calls the public
acceptInvitation.
- Lifetime. An invitation lasts
onboarding.invitationLifetimeMs(24 hours by default).resendInvitationissues a new token and lifetime and the earlier link stops working;revokeInvitationcancels it. Accepted, revoked, and expired invitations fail withINVITATION_INVALID. - Authority. Roles and groups are checked when you invite and applied when the person accepts, as bindings under the you held at invite time. That authority is re-validated at acceptance: if it was revoked in the meantime, the invitation can no longer be accepted.
- Delivery. Invitations need an email delivery callback (
authentication.sendEmail); without one,inviteandresendInvitationfail withDELIVERY_REQUIRED. - Limits. The plan's member limit is checked at acceptance, not when you invite, so an invitation can fail with
LIMIT_EXCEEDEDif the tenant filled up in the meantime.
Organization owners are invited differently: tenants.create sends an owner
invitation for a new tenant.
Disabling, offboarding, and deleting
Four calls take access away, from lightest to heaviest:
| Call | Takes away | Keeps | Typical use |
|---|---|---|---|
revokeSessions | Sessions, API keys, remembered devices | The account and all its access | A lost device or a suspected stolen token |
setStatus disabled | The ability to sign in or authenticate; sessions and keys end | Roles, groups, and attributes, which apply again when re-enabled | Leave of absence, an investigation |
offboard | Every grant: bindings, memberships, packages, activations, relationships, authorities, sessions, keys | A disabled record for retention | Someone leaves |
delete | The record itself, with credentials and factors | A tombstone, so audit records still resolve | After your retention period |
An identity can also carry an expiresAt for contractors and temporary accounts. From that moment its credentials
are refused, and the retention worker (purgeDeleted) disables it, ends its sessions and activations, and records
identity:expire. An expired identity cannot be re-enabled until you extend or clear the date with
update, so nobody quietly turns a contractor back on. See
time-bound identities.
Two kinds of account are protected throughout. The last active owner of a tenant cannot be disabled, offboarded,
deleted, or demoted (LAST_OWNER), and the last active root administrator cannot be disabled, offboarded, or
deleted (LAST_ROOT_ADMIN). A root administrator's status, sessions, expiry, sign-in address, and password can be
changed only by a root administrator.
| Method | What it does | Access |
|---|---|---|
acceptInvitation | Redeems a member invitation: creates the person's account with the invited email, applies the invitation's roles and groups, and signs them in. | Public |
create | Creates a person in the tenant, optionally with a password, a manager, and a scheduled deactivation date. | Credential |
createMany | Creates up to 100 people in one transaction, each with optional attributes, roles, groups, password, and expiry. | Credential |
delete | Removes a person or service account for good, leaving a tombstone so audit records still name who acted. | Credential |
export | Returns everything the tenant stores about one identity as JSON, to answer a data-subject access request. | Credential |
get | Returns one identity by ID, including the tombstone of a deleted identity. | Credential |
impersonate | Opens a short-lived "view as" session as a member, for support and troubleshooting, and returns its token. | Credential |
invite | Invites a person to the tenant by email, with roles and groups they receive when they accept. | Credential |
list | Lists the tenant's people and service accounts, with filters and paging. | Credential |
listBindings | Lists an identity's role bindings, direct and through its groups, with their activation and window state. | Credential |
listGroups | Lists the groups an identity currently belongs to, with membershipExpiresAt on temporary memberships. | Credential |
listInvitations | Lists every member invitation of the tenant, pending or not, without tokens. | Credential |
listReports | Lists the active people whose manager is this identity, by name. | Credential |
listSessions | Lists an identity's unexpired sessions and API keys, most recently used first, without token hashes. | Credential |
offboard | Disables an identity and removes everything that gave it access in one transaction, handing what it owned to a successor. | Credential |
requestPasswordReset | Emails a member a password-reset link on an administrator's behalf. | Credential |
resendInvitation | Sends a member invitation again with a new token and a fresh lifetime; the earlier link stops working. | Credential |
revokeInvitation | Cancels a member invitation so its link can no longer be used. | Credential |
revokeSessions | Ends every session and API key of an identity without disabling it. | Credential |
setBoundary | Sets a root-controlled permissions boundary on one identity, capping what it may do in the tenant whatever its roles grant. | Credential |
setOwner | Makes a member an owner of the tenant, or removes their ownership. | Credential |
setStatus | Disables an identity or re-enables it. | Credential |
unlock | Clears the rate-limit counters that lock a person out of sign-in, recovery, and MFA after too many attempts. | Credential |
update | Changes an identity's name, declared attributes, email, manager, or scheduled deactivation. | Credential |
acceptInvitation
Redeems a member invitation: creates the person's account with the invited email, applies the invitation's roles and groups, and signs them in.
Used inTyped client,Tenants and identities
- Permission: None: public. The token from the invitation email is the proof.
- Audited as:
identity:invitation:accept, with the new member as the actor and the inviter, roles, and groups in the metadata. - Errors:
INVITATION_INVALIDwhen the token is unknown, already used, revoked, or expired, or the inviter's grant authority was revoked;TENANT_UNAVAILABLEwhen the tenant or one of its ancestors is not active;INVALID_INPUTwhen neither the call nor the invitation gives a name;IDENTITY_EXISTSwhen an account with that email was created in the meantime;LIMIT_EXCEEDEDat the tenant's member limit;WEAK_PASSWORDorBREACHED_PASSWORDwhen the password fails the password rules;NOT_FOUNDwhen one of the invitation's roles or groups was deleted since;SOD_CONFLICTwhen the invitation's roles together break a separation-of-duties rule.
The email counts as verified, because following the link proved control of the address. name overrides the name
the inviter suggested. The result is the new public identity plus either { token, session } or an MFA challenge
(mfaRequired: true) when the tenant requires MFA; continue with the
MFA flow. Over HTTP, a response that issues a session
also sets the session cookie. Everything happens in one transaction, so a failure leaves the invitation usable.
const result = await client.identities.acceptInvitation({
tenantId: params.tenant,
token: params.token,
password: form.password,
});
if ('mfaRequired' in result) {
// enroll or verify the second factor, then continue
}Prop
Type
One of object & SessionResult | object & 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/identities/acceptInvitation" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"token": "<token>",
"password": "<password>"
}'iam.api.identities.acceptInvitation(
input: { tenantId: string; token: string; name?: string; password: string },
): Promise<EnrollmentResult>create
Creates a person in the tenant, optionally with a password, a manager, and a scheduled deactivation date.
Used inAccess lifecycle,Access lifecycle
- Permission:
iam:identities:createon the tenant. - Audited as:
iam:identities:create. - Errors:
IDENTITY_EXISTS(409) when the email is already used in this tenant;LIMIT_EXCEEDEDat the tenant's member limit;WEAK_PASSWORDorBREACHED_PASSWORDfor a password the rules refuse;INVALID_INPUTfor anexpiresAtthat is not in the future or is more than ten years ahead, or a manager who is not active;NOT_FOUNDwhen the manager is not in this tenant;INVARIANT_VIOLATIONwhen the new person would break an enforced access invariant.
The account starts active, with an unverified email and no roles. Without a password the person cannot sign in
with one: send them a reset link with requestPasswordReset, or use invite
instead, which lets them choose it. expiresAt (epoch milliseconds) schedules deactivation. Declared attributes
are set with update or createMany. After the call commits,
automatic access-package rules are
reconciled for the new person, so a matching package applies right away.
const contractor = await iam.api.identities.create(credential, {
tenantId,
email: 'sam@contractor.example',
name: 'Sam Rivera',
managerId: teamLeadId,
expiresAt: Date.parse('2026-12-31T23:59:59Z'),
});Prop
Type
A PublicIdentity 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/identities/create" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"email": "<email>",
"name": "<name>"
}'iam.api.identities.create(
credential: CredentialInput,
input: {
tenantId: string;
email: string;
name: string;
password?: string;
expiresAt?: number;
managerId?: string;
},
): Promise<PublicIdentity>createMany
Creates up to 100 people in one transaction, each with optional attributes, roles, groups, password, and expiry.
Used inFrequently asked questions,Tenancy and limits
- Permission:
iam:identities:createon the tenant. With roles, alsoiam:bindings:createon each role and an active grant authority; with groups,iam:groups:updateon each group and authority over each of its role bindings. - Audited as:
iam:identities:create. - Errors:
INVALID_INPUTfor an empty list, more than 100 entries, or an undeclared or mistyped attribute;ACCESS_DENIEDwithout the right to grant one of the roles or fill one of the groups;PROTECTED_RESOURCEfor the Owner role;GRANT_AUTHORITY_REQUIREDwhen roles are given and you hold no active grant authority;IDENTITY_EXISTS,LIMIT_EXCEEDED,SOD_CONFLICT, andINVARIANT_VIOLATIONas for single creation. Any failure rejects the whole batch.
Use it for migrations and cohort onboarding. Roles are bound directly to each person under your grant authority and
group memberships are permanent. An entry's expiresAt is that identity's deactivation date, not an expiry for its
grants. Attributes are checked against permissions.identityAttributes. The rights to grant each role and fill
each group are checked once for the whole batch, so an import can never grant more than you could bind by hand.
const { identities } = await iam.api.identities.createMany(credential, {
tenantId,
identities: newHires.map((hire) => ({
email: hire.email,
name: hire.name,
attributes: { department: hire.department }, // declared in permissions.identityAttributes
groupIds: [everyoneGroupId],
})),
});Prop
Type
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/identities/createMany" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"identities": [
{
"email": "<email>",
"name": "<name>"
}
]
}'iam.api.identities.createMany(
credential: CredentialInput,
input: {
tenantId: string;
identities: Array<{
email: string;
name: string;
password?: string;
attributes?: Record<string, Json>;
roleIds?: string[];
groupIds?: string[];
expiresAt?: number;
}>;
},
): Promise<{ identities: PublicIdentity[] }>delete
Removes a person or service account for good, leaving a tombstone so audit records still name who acted.
Used inAccess lifecycle,Access lifecycle,Support and privacy
- Permission:
iam:identities:deleteon the identity, with recent authentication. - Audited as:
iam:identities:deleteandidentity:delete(with the kind and former email). - Errors:
CONFLICTwhen the identity is already deleted;INVALID_INPUTwhen you try to delete yourself;ACCESS_DENIEDfor a root administrator unless you are root;LAST_OWNERorLAST_ROOT_ADMINfor the last active owner or root administrator;RECENT_AUTH_REQUIREDwhen your sign-in is not recent.
In one transaction it ends every session, API key, remembered device, and pending challenge; deletes role bindings, group memberships, activations, package assignments and requests, relationships, boundaries, passkeys, MFA enrollment, password history, and external-provider mappings; revokes the grant authorities the identity held and its account links; cancels its pending access requests; and clears it as the manager of anyone who reported to it. Revoking its grant authorities means grants it issued as a delegated administrator stop applying (see grant authorities).
The tombstone keeps the ID, name, and kind with status: 'deleted'. It has no sign-in email, phone, or password;
the former address is kept as deletedEmail. list leaves tombstones out unless asked, and get still returns
them. Unlike offboard, deletion hands nothing to a successor, so for leavers call offboard first
and delete after your retention period.
Prop
Type
A PublicIdentity 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/identities/delete" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"identityId": "<identityId>"
}'iam.api.identities.delete(
credential: CredentialInput,
input: { tenantId: string; identityId: string },
): Promise<PublicIdentity>export
Returns everything the tenant stores about one identity as JSON, to answer a data-subject access request.
Used inSupport and privacy
- Permission:
iam:identities:readon the identity, with recent authentication. Audit events are included only when you also holdiam:audit:readon the tenant. - Audited as:
iam:identities:readandidentity:export(with the kind and whether audit events were included). - Errors:
RECENT_AUTH_REQUIREDwhen your sign-in is not recent;NOT_FOUNDwhen the identity is not in this tenant.
The export contains the public identity; its stored sessions and API keys without token hashes; whether MFA is
enabled; passkey identifiers and transports; external-provider subjects; effective role bindings; groups;
relationships; access requests; boundaries; grant authorities; account links; and SCIM links. With
iam:audit:read it adds the audit events the identity performed, newest first and at most 5,000, and
auditIncluded tells you which you got. Secrets, password hashes, and tokens are never included. Deleted
identities can be exported too, which helps when a request arrives after the account was removed.
Prop
Type
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/identities/export" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"identityId": "<identityId>"
}'iam.api.identities.export(
credential: CredentialInput,
input: { tenantId: string; identityId: string },
): Promise<{
exportedAt: number;
tenantId: string;
identity: PublicIdentity;
sessions: {
[key: string]: unknown;
identityId: string;
createdAt: number;
expiresAt: number;
lastSeenAt: number;
authenticatedAt: number;
mfa: boolean;
kind: 'user' | 'role' | 'api-key' | 'session-token' | 'delegated';
agentId?: string;
delegationId?: string;
mfaAuthenticatedAt?: number;
credentialAuthorityId?: string;
sessionName?: string;
sourceIdentity?: string;
sessionTags?: Record<string, string>;
sourcePolicy?: PolicyDocument;
format?: 'jwt';
audience?: string[];
webIdentity?: { providerId: string; issuer: string; subject: string };
method?: AuthMethod;
client?: SessionClientInfo;
name?: string;
description?: string;
impersonatorId?: string;
impersonatorSessionId?: string;
trustedDeviceId?: string;
originalIdentityId?: string;
sourceTenantId?: string;
roleId?: string;
trustId?: string;
sourceSessionId?: string;
sourceAuthorityIds?: string[];
policy?: PolicyDocument;
previousSignIn?: SignInRecord;
id: string;
tenantId: string;
}[];
mfa: { enabled: boolean };
passkeys: { id: string; credentialId: unknown; transports: unknown }[];
externalIdentities: { providerId: unknown; issuer: unknown; subject: unknown }[];
bindings: EffectiveBinding[];
groups: Group[];
relationships: Relationship[];
accessRequests: AccessRequest[];
boundaries: PolicyDocument[];
grantAuthorities: {
id: string;
revoked: boolean;
parentAuthorityId: string | undefined;
}[];
links: { id: string; linkedIdentityId: string; revoked: boolean }[];
scim: {
connectionId: unknown;
externalId: unknown;
userName: unknown;
active: unknown;
}[];
auditIncluded: boolean;
audit: AuditEvent[] | undefined;
}>get
Returns one identity by ID, including the tombstone of a deleted identity.
- Permission:
iam:identities:readon the identity. - Audited as:
iam:identities:read. - Errors:
NOT_FOUNDwhen the identity is not in this tenant.
Password hashes are never returned.
Prop
Type
A PublicIdentity 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/identities/get" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"identityId": "<identityId>"
}'iam.api.identities.get(
credential: CredentialInput,
input: { tenantId: string; identityId: string },
): Promise<PublicIdentity>impersonate
Opens a short-lived "view as" session as a member, for support and troubleshooting, and returns its token.
Used inImpersonation,Support and privacy
- Permission:
iam:identities:impersonateon the member, with recent authentication, and the tenant's authentication policy must setallowImpersonation. - Audited as:
iam:identities:impersonateandidentity:impersonate(with the reason, the new session ID, and its expiry). - Errors:
FEATURE_DISABLEDwhen the tenant does not allow impersonation;ACCESS_DENIEDfor an owner or root administrator;INVALID_INPUTfor yourself, a service account, a missing reason, or adurationMsoutside one minute to eight hours;IMPERSONATION_RESTRICTEDunless you act through an ordinary session of your own;MFA_REQUIREDwhen the member requires MFA and your session did not complete it;IP_NOT_ALLOWEDorIP_BLOCKEDwhen the tenant's network rules refuse your address;RECENT_AUTH_REQUIRED.
The session lasts durationMs (one hour by default) and never outlives your own session. Each operation it
attempts is allowed only when both the member and you may perform it, and it cannot do anything that needs recent
authentication, assume roles, or grant OAuth consent. Every audit record it produces carries impersonatorId,
policies see principal.impersonated, and the member sees the session in their own session list. Over HTTP the
token is returned in the body only, never as a cookie, so keep it in a separate context such as a dedicated tab.
See impersonation.
const { token, session } = await iam.api.identities.impersonate(credential, {
tenantId,
identityId: memberId,
reason: 'Ticket 4821: export button missing',
durationMs: 30 * 60_000,
});Prop
Type
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/identities/impersonate" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"identityId": "<identityId>",
"reason": "<reason>"
}'iam.api.identities.impersonate(
credential: CredentialInput,
input: { tenantId: string; identityId: string; reason: string; durationMs?: number },
): Promise<{
token: string;
session: {
[x: string]: unknown;
identityId: string;
createdAt: number;
expiresAt: number;
lastSeenAt: number;
authenticatedAt: number;
mfa: boolean;
kind: 'user' | 'role' | 'api-key' | 'session-token' | 'delegated';
agentId?: string | undefined;
delegationId?: string | undefined;
mfaAuthenticatedAt?: number | undefined;
credentialAuthorityId?: string | undefined;
sessionName?: string | undefined;
sourceIdentity?: string | undefined;
sessionTags?: Record<string, string> | undefined;
sourcePolicy?: PolicyDocument | undefined;
format?: 'jwt' | undefined;
audience?: string[] | undefined;
webIdentity?: { providerId: string; issuer: string; subject: string } | undefined;
method?: AuthMethod | undefined;
client?: SessionClientInfo | undefined;
name?: string | undefined;
description?: string | undefined;
impersonatorId?: string | undefined;
impersonatorSessionId?: string | undefined;
trustedDeviceId?: string | undefined;
originalIdentityId?: string | undefined;
sourceTenantId?: string | undefined;
roleId?: string | undefined;
trustId?: string | undefined;
sourceSessionId?: string | undefined;
sourceAuthorityIds?: string[] | undefined;
policy?: PolicyDocument | undefined;
previousSignIn?: SignInRecord | undefined;
id: string;
tenantId: string;
};
identity: PublicIdentity;
}>invite
Invites a person to the tenant by email, with roles and groups they receive when they accept.
Used inQuickstart,HTTP and configuration,Architecture overview,Tenants and identities
- Permission:
iam:identities:createon the tenant. With roles, alsoiam:bindings:createon each role and an active grant authority; with groups,iam:groups:updateon each group and authority over each of its role bindings. - Audited as:
iam:identities:create. - Errors:
DELIVERY_REQUIREDwithout an email delivery callback;IDENTITY_EXISTSwhen the email already belongs to an identity in this tenant;PROTECTED_RESOURCEfor the Owner role;ACCESS_DENIEDwithout the right to grant one of the roles or fill one of the groups;GRANT_AUTHORITY_REQUIREDwhen roles or groups are given and you hold no active grant authority;NOT_FOUNDfor an unknown role or group.
The result has the invitation ID and expiry but never the token, which travels only in the email. name is a
suggestion the person can change when accepting. Nothing is granted until acceptance; see
Invitations. Inviting the same address again does not cancel an earlier invitation, so use
resendInvitation for a lost email. If the person already has a disabled account, re-enable
it with setStatus instead.
const invitation = await iam.api.identities.invite(credential, {
tenantId,
email: 'alice@example.com',
name: 'Alice Chen',
roleIds: [editorRoleId],
groupIds: [designGroupId],
});
// invitation.expiresAt: when the link stops workingProp
Type
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/identities/invite" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"email": "<email>"
}'iam.api.identities.invite(
credential: CredentialInput,
input: {
tenantId: string;
email: string;
name?: string;
roleIds?: string[];
groupIds?: string[];
},
): Promise<{
invitationId: string;
email: string;
expiresAt: number;
roleIds: string[];
groupIds: string[];
}>list
Lists the tenant's people and service accounts, with filters and paging.
Used inReact Router,SvelteKit,Next.js,Access lifecycle,Access lifecycle
- Permission:
iam:identities:readon the tenant. - Audited as:
iam:identities:read. - Errors:
INVALID_INPUTfor an unknownkindorstatus, or alimitoutside 1 to 1,000.
Results are ordered by name, then ID. kind (user or service) and status (active, disabled, deleted)
narrow the list; tombstones are left out unless you pass includeDeleted or ask for status: 'deleted'. query
matches the name or email case-insensitively. expiresBefore keeps identities whose scheduled deactivation is at
or before that time, including ones already past it. limit and offset page through the result.
// Active accounts that end within the next 14 days.
const ending = await iam.api.identities.list(credential, {
tenantId,
status: 'active',
expiresBefore: Date.now() + 14 * 86_400_000,
});Prop
Type
An array of PublicIdentity.
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/identities/list" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>"
}'iam.api.identities.list(
credential: CredentialInput,
input: {
tenantId: string;
kind?: Identity['kind'];
status?: Identity['status'];
includeDeleted?: boolean;
query?: string;
expiresBefore?: number;
limit?: number;
offset?: number;
},
): Promise<PublicIdentity[]>listBindings
Lists an identity's role bindings, direct and through its groups, with their activation and window state.
Used inJust-in-time elevation
- Permission:
iam:bindings:readon the identity. - Audited as:
iam:bindings:read. - Errors:
NOT_FOUNDwhen the identity is not in this tenant.
Each entry is the binding with its role and via ('identity', or { groupId } for a group binding).
Eligible bindings carry activation while activated and
pendingActivation while a request awaits approval, and bindings with an access window carry inWindow.
Future-dated bindings are listed with their start; expired bindings and lapsed memberships are left out. The list
shows what is bound, not a decision: to see why a specific action is allowed, use
access paths.
Prop
Type
An array of EffectiveBinding.
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/identities/listBindings" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"identityId": "<identityId>"
}'iam.api.identities.listBindings(
credential: CredentialInput,
input: { tenantId: string; identityId: string },
): Promise<EffectiveBinding[]>listGroups
Lists the groups an identity currently belongs to, with membershipExpiresAt on temporary memberships.
- Permission:
iam:groups:readon the identity. - Audited as:
iam:groups:read. - Errors:
NOT_FOUNDwhen the identity is not in this tenant.
Lapsed memberships are left out even before the purge job removes them, so the list matches the groups that currently give the identity roles.
Prop
Type
An array of Group & 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/identities/listGroups" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"identityId": "<identityId>"
}'iam.api.identities.listGroups(
credential: CredentialInput,
input: { tenantId: string; identityId: string },
): Promise<(Group & { membershipExpiresAt?: number })[]>listInvitations
Lists every member invitation of the tenant, pending or not, without tokens.
- Permission:
iam:identities:readon the tenant. - Audited as:
iam:identities:read.
Each invitation shows the email, suggested name, roles, groups, the inviter, the grant authority its roles will be
issued under (present only when it carries roles or groups), when it was created and expires, and whether it was
consumed or revoked. An invitation past expiresAt that is neither has simply expired;
resendInvitation renews it.
Prop
Type
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/identities/listInvitations" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>"
}'iam.api.identities.listInvitations(
credential: CredentialInput,
input: { tenantId: string },
): Promise<{
[key: string]: unknown;
email: string;
name?: string;
roleIds: string[];
groupIds: string[];
authorityId?: string;
inviterId: string;
createdAt: number;
expiresAt: number;
consumed: boolean;
revoked?: boolean;
id: string;
tenantId: string;
}[]>listReports
Lists the active people whose manager is this identity, by name.
- Permission:
iam:identities:readon the identity. - Audited as:
iam:identities:read. - Errors:
NOT_FOUNDwhen the identity is not in this tenant.
Disabled and deleted reports are left out. Managers are set with managerId on create or
update, and they can approve requests for eligible bindings and access packages that ask for manager
approval (see approver groups and managers).
Prop
Type
An array of PublicIdentity.
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/identities/listReports" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"identityId": "<identityId>"
}'iam.api.identities.listReports(
credential: CredentialInput,
input: { tenantId: string; identityId: string },
): Promise<PublicIdentity[]>listSessions
Lists an identity's unexpired sessions and API keys, most recently used first, without token hashes.
- Permission:
iam:identities:readon the identity. - Audited as:
iam:identities:read. - Errors:
NOT_FOUNDwhen the identity is not in this tenant.
Use it for device lists and support. Each session shows when it was created, last used, and expires, how it was
established (method), whether it completed MFA, the client details recorded at sign-in, and impersonatorId for
"view as" sessions. End them with revokeSessions.
Prop
Type
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/identities/listSessions" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"identityId": "<identityId>"
}'iam.api.identities.listSessions(
credential: CredentialInput,
input: { tenantId: string; identityId: string },
): Promise<{
[key: string]: unknown;
identityId: string;
createdAt: number;
expiresAt: number;
lastSeenAt: number;
authenticatedAt: number;
mfa: boolean;
kind: 'user' | 'role' | 'api-key' | 'session-token' | 'delegated';
agentId?: string;
delegationId?: string;
mfaAuthenticatedAt?: number;
credentialAuthorityId?: string;
sessionName?: string;
sourceIdentity?: string;
sessionTags?: Record<string, string>;
sourcePolicy?: PolicyDocument;
format?: 'jwt';
audience?: string[];
webIdentity?: { providerId: string; issuer: string; subject: string };
method?: AuthMethod;
client?: SessionClientInfo;
name?: string;
description?: string;
impersonatorId?: string;
impersonatorSessionId?: string;
trustedDeviceId?: string;
originalIdentityId?: string;
sourceTenantId?: string;
roleId?: string;
trustId?: string;
sourceSessionId?: string;
sourceAuthorityIds?: string[];
policy?: PolicyDocument;
previousSignIn?: SignInRecord;
id: string;
tenantId: string;
}[]>offboard
Disables an identity and removes everything that gave it access in one transaction, handing what it owned to a successor.
Used inEnterprise onboarding,Access lifecycle,Access lifecycle
- Permission:
iam:identities:updateon the identity, with recent authentication. Offboarding an owner also requires you to be an owner of this tenant, signed in to it with your own account, or root. Direct bindings and group memberships are removed asbindings.deleteandgroups.removeMemberwould, under your grant authority. - Audited as:
iam:identities:updateandidentity:offboard(with the reason, kind, successor, and every count). - Errors:
INVALID_INPUTfor yourself, a missing reason, or a successor who is the same identity or not active;ACCESS_DENIEDfor a root administrator unless you are root, for an owner unless you are an owner or root, or for a binding or group membership issued under another administrator's grant authority;LAST_OWNERorLAST_ROOT_ADMIN;NOT_FOUNDfor an unknown or deleted identity or successor;RECENT_AUTH_REQUIRED.
In order, it removes ownership (the protected Owner binding); ends role activations; revokes
access-package assignments with the bindings and memberships
they created; deletes the remaining direct bindings and group memberships; deletes relationships; cancels pending
access and package requests; revokes the grant authorities the identity holds, so grants it issued as a delegated
administrator stop applying; moves its reports to the successor; transfers the managed resources it owns to the
successor; ends every session and API key; and disables it. Without a successor, reports are left without a
manager and owned resources are only counted (resourcesOwned). The result counts each step, which makes a good
record for auditors.
The identity stays as a disabled record so the audit trail still names who they were; remove it later with
delete. Package rules owned by the leaver are suspended when their authority is revoked, so hand them
over first. See offboarding.
const summary = await iam.api.identities.offboard(credential, {
tenantId,
identityId: leaverId,
reason: 'Left the company (HR-1234)',
successorId: managerId,
});
// summary.bindings, summary.memberships, summary.resourcesReassigned, ...Prop
Type
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/identities/offboard" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"identityId": "<identityId>",
"reason": "<reason>"
}'iam.api.identities.offboard(
credential: CredentialInput,
input: { tenantId: string; identityId: string; reason: string; successorId?: string },
): Promise<{
departmentsReassigned?: number | undefined;
teamsLeft?: number | undefined;
delegationsRevoked?: number | undefined;
agentsUnsponsored?: number | undefined;
agentsReassigned?: number | undefined;
sessions: number;
bindings: number;
memberships: number;
activations: number;
packages: number;
relationships: number;
accessRequests: number;
authorities: number;
resourcesReassigned: number;
resourcesOwned: number;
reportsReassigned: number;
identity: PublicIdentity;
}>requestPasswordReset
Emails a member a password-reset link on an administrator's behalf.
Used inVerification and recovery,Tenancy and limits
- Permission:
iam:identities:updateon the member, with recent authentication. For an owner you must be another owner of the same tenant or root; for a root administrator, root. - Audited as:
iam:identities:updateandidentity:password-reset. - Errors:
FEATURE_DISABLEDwhen email delivery or password sign-in is not configured;INVALID_INPUTfor a service account, a disabled identity, or one without an email;ACCESS_DENIEDfor an owner or root administrator you may not control;RECENT_AUTH_REQUIRED.
Unlike the public auth.requestPasswordReset, it works whether or not the address is verified, which makes it the
way to onboard someone created without a password. It returns { queued: true, email }; the reset token goes only
to the member's inbox. Whoever controls a password reset controls the account, so the owner and root rules keep
iam:identities:update alone from taking over a more powerful account. See
recovery.
Prop
Type
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/identities/requestPasswordReset" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"identityId": "<identityId>"
}'iam.api.identities.requestPasswordReset(
credential: CredentialInput,
input: { tenantId: string; identityId: string },
): Promise<{ queued: boolean; email: string | undefined }>resendInvitation
Sends a member invitation again with a new token and a fresh lifetime; the earlier link stops working.
- Permission:
iam:identities:updateon the invitation. - Audited as:
iam:identities:update. - Errors:
CONFLICTwhen the invitation was already accepted or revoked;DELIVERY_REQUIREDwithout an email delivery callback;NOT_FOUNDwhen the invitation is not in this tenant.
Use it when the first email expired, was lost, or went to spam: expired invitations can be resent. The new email names you as the inviter, while the invitation keeps its original inviter, roles, groups, and grant authority.
Prop
Type
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/identities/resendInvitation" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"invitationId": "<invitationId>"
}'iam.api.identities.resendInvitation(
credential: CredentialInput,
input: { tenantId: string; invitationId: string },
): Promise<{ invitationId: string; email: string; expiresAt: number }>revokeInvitation
Cancels a member invitation so its link can no longer be used.
- Permission:
iam:identities:updateon the invitation. - Audited as:
iam:identities:update. - Errors:
CONFLICTwhen the invitation was already accepted or revoked;NOT_FOUNDwhen it is not in this tenant.
The invitation stays in listInvitations with revoked: true, and a revoked invitation cannot
be re-sent.
Prop
Type
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/identities/revokeInvitation" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"invitationId": "<invitationId>"
}'iam.api.identities.revokeInvitation(
credential: CredentialInput,
input: { tenantId: string; invitationId: string },
): Promise<{
revoked: boolean;
email: string;
name?: string;
roleIds: string[];
groupIds: string[];
authorityId?: string;
inviterId: string;
createdAt: number;
expiresAt: number;
consumed: boolean;
id: string;
tenantId: string;
}>revokeSessions
Ends every session and API key of an identity without disabling it.
Used inSign-in and devices
- Permission:
iam:identities:updateon the identity, with recent authentication. - Audited as:
iam:identities:updateandidentity:revoke-sessions(with the number revoked). - Errors:
ACCESS_DENIEDfor a root administrator unless you are root;NOT_FOUNDwhen the identity is not in this tenant;RECENT_AUTH_REQUIRED.
Use it for incident response or a lost device. Remembered devices and pending sign-in challenges are cleared too,
so the next sign-in needs the second factor again, and role sessions assumed from the identity and "view as"
sessions opened through its sessions end as well. The account and its access stay, so a person can sign in again
at once. For a service account this deletes its API keys; issue new ones with
credentials.create. The result's revoked is the number of session
records the identity held.
Pass keepApiKeys: true to end everything except the API keys: user sessions, role sessions the identity assumed in
other tenants, session tokens (including those minted from its keys), remembered devices, and pending challenges end,
while the keys keep working. Use it when a service account's session tokens may have leaked but its keys have not.
The audit event records keptApiKeys. A value other than true or false is INVALID_INPUT.
Prop
Type
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/identities/revokeSessions" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"identityId": "<identityId>"
}'iam.api.identities.revokeSessions(
credential: CredentialInput,
input: { tenantId: string; identityId: string; keepApiKeys?: boolean },
): Promise<{ revoked: number }>setBoundary
Sets a root-controlled permissions boundary on one identity, capping what it may do in the tenant whatever its roles grant.
- Permission:
iam:boundaries:updateon the identity, and you must be a root administrator. - Audited as:
iam:boundaries:update. - Errors:
ACCESS_DENIEDfor anyone but root;INVALID_POLICY,INVALID_ACTION, orINVALID_RESOURCE_TYPEfor a document the catalog rejects;NOT_FOUNDwhen the identity is not in this tenant;INVARIANT_VIOLATIONwhen the change would break an enforced access invariant.
A never grants: an action is allowed only when a role grants it and the boundary allows it too, and boundaries set on the tenant and its ancestors apply on top. Each identity has at most one boundary per tenant, and calling again replaces it. Boundaries are platform controls, which is why tenant administrators cannot set them. See boundaries.
// A vendor account may never reach beyond support tickets, whatever roles it is given.
await iam.api.identities.setBoundary(rootCredential, {
tenantId,
identityId: vendorId,
document: {
version: 1,
statements: [{ effect: 'allow', actions: ['tickets:*'], resources: ['*'] }],
},
});Prop
Type
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/identities/setBoundary" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"identityId": "<identityId>",
"document": {
"version": 1,
"statements": [
{
"effect": "allow",
"actions": [
{}
],
"resources": [
{}
]
}
]
}
}'iam.api.identities.setBoundary(
credential: CredentialInput,
input: { tenantId: string; identityId: string; document: PolicyDocument },
): Promise<{
id: string;
tenantId: string;
uniqueKey: string;
identityId: string;
document: PolicyDocument;
}>setOwner
Makes a member an owner of the tenant, or removes their ownership.
- Permission:
iam:identities:updateon the member, with recent authentication, and you must be an owner of this tenant yourself (signed in to it with your own account, not through an assumed role) or root. - Audited as:
iam:identities:update. - Errors:
ACCESS_DENIEDwhen you are not an owner or root;INVALID_INPUTfor a service account, a disabled member, or a non-booleanowner;LAST_OWNERwhen removing the last active owner;RECENT_AUTH_REQUIRED.
Ownership is the protected Owner role, which allows every action
in the tenant and cannot be bound, edited, or requested any other way. Granting it binds the Owner role under a new,
unrestricted grant authority delegated from the one you grant under, so the new owner can administer and delegate
like you, within your authority chain; if an authority above theirs is revoked (offboarding or deleting you
revokes yours), their grants stop applying. Removing ownership deletes the Owner binding but leaves the person's
grant authorities, so grants they issued keep applying; revoke those with
authorities.revoke if they should not.
Prop
Type
A PublicIdentity 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/identities/setOwner" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"identityId": "<identityId>",
"owner": true
}'iam.api.identities.setOwner(
credential: CredentialInput,
input: { tenantId: string; identityId: string; owner: boolean },
): Promise<PublicIdentity>setStatus
Disables an identity or re-enables it.
- Permission:
iam:identities:updateon the identity, with recent authentication. - Audited as:
iam:identities:update. - Errors:
INVALID_INPUTfor a status other thanactiveordisabled;ACCESS_DENIEDfor a root administrator unless you are root;LAST_OWNERorLAST_ROOT_ADMINwhen disabling the last active owner or root administrator;INVALID_TRANSITION(409) when re-enabling an identity whoseexpiresAthas passed;NOT_FOUNDfor a deleted identity;INVARIANT_VIOLATIONwhen the change would break an enforced access invariant.
Disabling ends every session and API key at once, and the identity can no longer sign in or authenticate. Its
roles, groups, and attributes are kept and apply again when you re-enable it, but ended sessions and keys do not
come back. It works for people and service accounts alike
(serviceAccounts.setStatus is the service-account
equivalent). Access-package rules are reconciled after the change and never assign anything to a disabled
identity. To remove access for good, use offboard.
Prop
Type
A PublicIdentity 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/identities/setStatus" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"identityId": "<identityId>",
"status": "active"
}'iam.api.identities.setStatus(
credential: CredentialInput,
input: { tenantId: string; identityId: string; status: 'active' | 'disabled' },
): Promise<PublicIdentity>unlock
Clears the rate-limit counters that lock a person out of sign-in, recovery, and MFA after too many attempts.
Used inVerification and recovery,Sign-in and devices
- Permission:
iam:identities:updateon the identity, with recent authentication. - Audited as:
iam:identities:updateandidentity:unlock(withsupportedandcleared). - Errors:
RECENT_AUTH_REQUIRED;NOT_FOUNDfor an unknown or deleted identity.
It resets the counters kept for the identity's email, phone, and ID across sign-in, sign-up, re-authentication,
email verification and change, phone verification, password reset and change, passwordless, passkey, and
second-factor flows. Counters kept per client address and
network blocks are not affected. A custom limiter without a reset
method returns { supported: false, cleared: 0 }; otherwise cleared is the number of counters reset. See
lockouts.
Prop
Type
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/identities/unlock" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"identityId": "<identityId>"
}'iam.api.identities.unlock(
credential: CredentialInput,
input: { tenantId: string; identityId: string },
): Promise<{ supported: boolean; cleared: number }>update
Changes an identity's name, declared attributes, email, manager, or scheduled deactivation.
Used inPermission catalog,Access lifecycle,Access lifecycle
- Permission:
iam:identities:updateon the identity. An email change also needs recent authentication, and for an owner or root administrator the same control asrequestPasswordReset; changing a root administrator's expiry needs root. - Audited as:
iam:identities:update, plusidentity:email-change(with the old and new address) when the email changes. - Errors:
INVALID_INPUTwhen no field is given, for an undeclared or mistyped attribute, anexpiresAtthat is not in the future or is more than ten years ahead, an email on a service account, or a manager who is the identity itself, is not active, or reports to the identity (directly or further down);IDENTITY_EXISTSwhen the new email is taken in this tenant;LAST_OWNERwhen setting an expiry on the last active owner;ACCESS_DENIEDfor a protected account you may not change;NOT_FOUNDfor a deleted identity;INVARIANT_VIOLATIONwhen the change would break an enforced access invariant.
Only the fields you pass change. attributes replaces the whole set of declared attributes, validated against
permissions.identityAttributes. A new email is marked unverified and every session ends, because whoever
controls the sign-in address controls the account. expiresAt: null clears a scheduled deactivation and
managerId: null removes the manager; to end access immediately, disable the identity instead of setting an
expiry. After the change, automatic access-package rules are reconciled for the identity, so new attribute values
can change which packages it receives.
await iam.api.identities.update(credential, {
tenantId,
identityId,
attributes: { department: 'finance', level: 3 },
managerId: newManagerId,
expiresAt: null, // no longer a temporary account
});Prop
Type
A PublicIdentity 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/identities/update" \
-H "Authorization: Bearer $BETTER_IAM_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Better-IAM: 1" \
-d '{
"tenantId": "<tenantId>",
"identityId": "<identityId>"
}'iam.api.identities.update(
credential: CredentialInput,
input: {
tenantId: string;
identityId: string;
name?: string;
attributes?: Record<string, Json>;
email?: string;
expiresAt?: number | null;
managerId?: string | null;
},
): Promise<PublicIdentity>Better IAM is created by Sean Filimon
Last updated