Skip to main content

Users

GET /orgs/{orgId}/api/v1/admin/users
POST /orgs/{orgId}/api/v1/admin/users
GET /orgs/{orgId}/api/v1/admin/users/{userId}
PUT /orgs/{orgId}/api/v1/admin/users/{userId}
PATCH /orgs/{orgId}/api/v1/admin/users/{userId}
DELETE /orgs/{orgId}/api/v1/admin/users/{userId}
POST /orgs/{orgId}/api/v1/admin/users/{userId}/block
POST /orgs/{orgId}/api/v1/admin/users/{userId}/unblock
GET /orgs/{orgId}/api/v1/admin/users/{userId}/roles
POST /orgs/{orgId}/api/v1/admin/users/{userId}/roles
DELETE /orgs/{orgId}/api/v1/admin/users/{userId}/roles/{roleId}
GET /orgs/{orgId}/api/v1/admin/users/{userId}/groups
POST /orgs/{orgId}/api/v1/admin/users/{userId}/groups
DELETE /orgs/{orgId}/api/v1/admin/users/{userId}/groups/{groupId}
GET /orgs/{orgId}/api/v1/admin/users/{userId}/permissions
POST /orgs/{orgId}/api/v1/admin/users/{userId}/permissions
DELETE /orgs/{orgId}/api/v1/admin/users/{userId}/permissions/{permissionId}
PUT /orgs/{orgId}/api/v1/admin/users/{userId}/password
POST /orgs/{orgId}/api/v1/admin/users/{userId}/password-reset
POST /orgs/{orgId}/api/v1/admin/users/{userId}/verify-email
POST /orgs/{orgId}/api/v1/admin/users/{userId}/mark-verified
POST /orgs/{orgId}/api/v1/admin/users/{userId}/mfa/reset (removed — 410 Gone)

MFA for a user (authenticators, temporary access codes) is managed with the MFA API.

Authentication​

All user management endpoints require an admin API key or a Bearer token with admin:users:read / admin:users:write.

Each endpoint needs the credential's scope and a permission. A session or Bearer caller must hold the permission; for an API key its creator must still hold it. A missing permission returns 403 forbidden with details.required_permissions. See Required permissions.

MethodEndpointPermissionAPI-key / token scope
GET/admin/usersusers.viewadmin:users:read
GET/admin/users/{userId}users.viewadmin:users:read
POST/admin/usersusers.createadmin:users:write
PUT PATCH/admin/users/{userId}users.editadmin:users:write
DELETE/admin/users/{userId}users.deleteadmin:users:write
POST/admin/users/{userId}/blockusers.editadmin:users:write
POST/admin/users/{userId}/unblockusers.editadmin:users:write
POST/admin/users/{userId}/mfa/resetusers.editadmin:users:write
GET/admin/users/{userId}/rolesusers.viewadmin:users:read
PUT/admin/users/{userId}/rolesusers.editadmin:users:write
POST/admin/users/{userId}/rolesusers.editadmin:users:write
DELETE/admin/users/{userId}/roles/{roleId}users.editadmin:users:write
GET/admin/users/{userId}/groupsusers.viewadmin:users:read
PUT/admin/users/{userId}/groupsusers.editadmin:users:write
POST/admin/users/{userId}/groupsusers.editadmin:users:write
DELETE/admin/users/{userId}/groups/{groupId}users.editadmin:users:write
GET/admin/users/{userId}/permissionsusers.viewadmin:users:read
POST/admin/users/{userId}/permissionsusers.editadmin:users:write
DELETE/admin/users/{userId}/permissions/{permissionId}users.editadmin:users:write
POST/admin/users/{userId}/verify-emailusers.editadmin:users:write
POST/admin/users/{userId}/mark-verifiedusers.editadmin:users:write
POST/admin/users/{userId}/password-resetusers.editadmin:users:write
PUT POST/admin/users/{userId}/passwordusers.editadmin:users:write

Setting a password, changing the e-mail, activating or deactivating, blocking and deleting a user also require that you hold every permission the target user holds. Otherwise the call returns 403 forbidden. A password you set must meet the organization's password policy (400). See Account-takeover containment.

MethodHeader
API KeyAuthorization: ApiKey lmk_... or X-API-Key: lmk_...
Bearer TokenAuthorization: Bearer eyJ...

The User Object​

Single-user responses are wrapped in a data envelope. The blocked flag is blocked (not isBlocked), and roles / groups are arrays of objects, not slug strings.

User Object
{
"data": {
"id": "019d6190-9bba-7b5f-a0a5-07a6d10184d2",
"email": "john@acme.com",
"username": "john.doe",
"name": "John Doe",
"givenName": "John",
"familyName": "Doe",
"picture": null,
"emailVerified": true,
"phoneNumber": null,
"phoneNumberVerified": false,
"isActive": true,
"blocked": false,
"mfaEnabled": false,
"isAdmin": false,
"createdAt": "2026-01-15T10:30:00Z",
"lastLoginAt": "2026-04-10T09:15:00Z",
"loginCount": 12,
"roles": [{ "id": 17, "name": "Tenant Admin", "slug": "tenant_admin" }],
"groups": [{ "id": 5, "name": "Engineering", "slug": "engineering" }],
"locale": "en",
"zoneinfo": "America/New_York",
"updatedAt": "2026-03-01T08:00:00Z",
"blockedAt": null,
"isJitProvisioned": false,
"jitProvisionedBy": null,
"socialProvider": null
}
}
note

roles, groups, locale, zoneinfo, updatedAt, blockedAt, isJitProvisioned, jitProvisionedBy, and socialProvider are returned only on the single-user GET /users/{userId} (detailed) response. List rows omit them.

List Users​

curl "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users?page=1&limit=50" \
-H "Authorization: ApiKey lmk_abc123"

Query Parameters​

ParameterDescription
pagePage number (default: 1)
limitResults per page (default: 25, max: 100)
searchFilter by email, name, or username
activeFilter by active status (true/false)
blockedFilter by blocked status (true/false)
emailVerifiedFilter by email-verified status (true/false)
mfaEnabledFilter by MFA-enabled status (true/false)
roleFilter by role slug
groupFilter by group slug
sortByField to sort by
sortDirSort direction (ASC/DESC, default DESC)

List rows are a trimmed projection of the User Object (no roles/groups/locale/zoneinfo/updatedAt).

List Response
{
"data": [
{
"id": "019d6190-9bba-7b5f-a0a5-07a6d10184d2",
"email": "john@acme.com",
"username": "john.doe",
"name": "John Doe",
"givenName": "John",
"familyName": "Doe",
"picture": null,
"emailVerified": true,
"phoneNumber": null,
"phoneNumberVerified": false,
"isActive": true,
"blocked": false,
"mfaEnabled": false,
"isAdmin": false,
"createdAt": "2026-01-15T10:30:00Z",
"lastLoginAt": "2026-04-10T09:15:00Z",
"loginCount": 12
}
],
"meta": {
"total": 142,
"page": 1,
"limit": 25,
"totalPages": 6,
"hasNextPage": true,
"hasPreviousPage": false
}
}

Create User​

Create a User
curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users \
-H "Authorization: ApiKey lmk_abc123" \
-H "Content-Type: application/json" \
-d '{
"email": "jane@acme.com",
"name": "Jane Smith",
"givenName": "Jane",
"familyName": "Smith",
"password": "Str0ng!Pass",
"emailVerified": true,
"roles": ["viewer"]
}'

Returns 201 Created with the new user object.

note

A password must meet the organization's password policy (length, character classes, and the breached-password check when enabled); otherwise the call returns 400 with the policy errors in message. Omit password to create a user who signs in passwordlessly or sets a password through a reset or invitation link.

Update User​

Update a User (PUT)
curl -X PUT https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE... \
-H "Authorization: ApiKey lmk_abc123" \
-H "Content-Type: application/json" \
-d '{
"name": "Jane Doe",
"givenName": "Jane",
"familyName": "Doe"
}'

Use PATCH for partial updates (only the supplied fields are changed).

Block / Unblock a User​

Block a User
curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../block \
-H "Authorization: ApiKey lmk_abc123"
Unblock a User
curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../unblock \
-H "Authorization: ApiKey lmk_abc123"

Manage User Roles​

Get User Roles​

Get User Roles
curl https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../roles \
-H "Authorization: ApiKey lmk_abc123"

Assign a Role​

Assign a Role to a User
curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../roles \
-H "Authorization: ApiKey lmk_abc123" \
-H "Content-Type: application/json" \
-d '{"roleId": "01JF3KROLE..."}'

Remove a Role​

Remove a Role from a User
curl -X DELETE https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../roles/01JF3KROLE... \
-H "Authorization: ApiKey lmk_abc123"

Manage User Groups​

Get User Groups​

Get User Groups
curl https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../groups \
-H "Authorization: ApiKey lmk_abc123"

Add to a Group​

Add User to a Group
curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../groups \
-H "Authorization: ApiKey lmk_abc123" \
-H "Content-Type: application/json" \
-d '{"groupId": "01JF3KGRP..."}'

Remove from a Group​

Remove User from a Group
curl -X DELETE https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../groups/01JF3KGRP... \
-H "Authorization: ApiKey lmk_abc123"

Manage User Permissions​

Direct permissions on users are additive — the user retains all permissions inherited from their roles and groups.

Get User Permissions​

Get User Permissions
curl https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../permissions \
-H "Authorization: ApiKey lmk_abc123"

Assign a Permission​

Assign a Permission to a User
curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../permissions \
-H "Authorization: ApiKey lmk_abc123" \
-H "Content-Type: application/json" \
-d '{"permissionId": 42}'

Remove a Permission​

Remove a Permission from a User
curl -X DELETE https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../permissions/42 \
-H "Authorization: ApiKey lmk_abc123"

Password Management​

Set Password (admin override)
curl -X PUT https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../password \
-H "Authorization: ApiKey lmk_abc123" \
-H "Content-Type: application/json" \
-d '{"password": "NewStr0ng!Pass", "requireChange": true}'
Send Password Reset Email
curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../password-reset \
-H "Authorization: ApiKey lmk_abc123"

Email Verification​

POST /verify-email issues a fresh verification link (earlier links stop working) and e-mails it to the user, exactly like the self-service resend. For a user whose e-mail is already verified it sends nothing and still returns 200. To skip the e-mail and mark the address verified, use POST /mark-verified.

Send Verification Email
curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../verify-email \
-H "Authorization: ApiKey lmk_abc123"
Mark Email as Verified (without sending email)
curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../mark-verified \
-H "Authorization: ApiKey lmk_abc123"

Federated identities​

A user's federated identities are the external identities that sign in as the account: a SAML identity provider + NameID, an LDAP directory + DN, or a social provider. Linking, unlinking and relinking change who can sign in as the user, so they are treated like a credential change:

  • Reading needs users.view (API key scope admin:users:read); changes need users.edit (admin:users:write).
  • You cannot change the links of an account that holds permissions you do not have (403).
  • When calling with a user's login token (lumo login, OAuth), the admin must have passed MFA in the last 10 minutes: otherwise the call returns 403 step_up_required and you retry with an approved step-up challenge id in X-MFA-Challenge, as for temporary access codes. Organization API keys are not subject to step-up.
  • The user's sessions and tokens are revoked, the user is emailed, and the change is audited (identity.link.created, identity.link.removed, identity.link.bulk_relinked).

An account signs in through one federated source at a time: linking SAML is refused (409 identity_conflict) while the account is linked to LDAP or a social provider, and linking LDAP is refused while it is linked to SAML. Unlink the other source first.

List a user's identities​

List identities
curl https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../identities \
-H "Authorization: ApiKey lmk_abc123"
200 OK
{
"data": {
"auth_source": "saml",
"ldap_only": false,
"bindings": [
{
"type": "saml",
"idp_id": 7,
"idp_name": "Okta",
"name_id": "jane@acme.com",
"binding_key": "saml:7:jane@acme.com",
"hashed": false,
"legacy": false,
"orphaned": false
}
]
}
}

type is saml, ldap (ldap_config_id, ldap_config_name, dn, ldap_only, last_sync_at), social (provider, subject) or social_account (a provider linked from the user's account-security page; read-only here). A SAML binding with legacy: true stores a bare NameID from before per-IdP binding; while the organization has more than one SAML IdP the user cannot sign in with SAML until relinked (see Relink legacy SAML users). NameIDs too long for the column are stored hashed (hashed: true, name_id: null).

Link a SAML identity
curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../identities \
-H "Authorization: ApiKey lmk_abc123" \
-H "Content-Type: application/json" \
-d '{"type": "saml", "idp_id": 7, "name_id": "jane@acme.com"}'
Link an LDAP identity
curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../identities \
-H "Authorization: ApiKey lmk_abc123" \
-H "Content-Type: application/json" \
-d '{"type": "ldap", "ldap_config_id": 1, "dn": "uid=jane,ou=people,dc=acme,dc=com"}'
FieldTypeDescription
typestringRequired. saml or ldap
idp_idintegerSAML: the SAML identity provider's id
name_idstringSAML: the NameID the IdP asserts for this user
ldap_config_idintegerLDAP: the directory connection's id
dnstringLDAP: the entry's distinguished name. Omit it to look the entry up in the directory by the user's email, then username
ldap_onlybooleanLDAP: also disable the local password. Default false — the local password keeps working

Returns 201 Created with the new binding in data. A new link replaces the user's existing link of the same type.

StatuserrorWhen
403forbidden / step_up_requiredThe account holds permissions you lack, or step-up is needed
404not_foundThe user, IdP or directory is not in this organization
409identity_conflictAnother user already holds that NameID at the IdP (or that DN in the directory), or the user is linked to a different federated source
422invalid_identityMissing or malformed type, name_id or dn
422directory_lookup_failedNo dn given and the directory search found nothing or failed — pass the DN
Unlink LDAP
curl -X DELETE https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../identities/ldap \
-H "Authorization: ApiKey lmk_abc123"

{type} is saml, ldap or social. Returns 200 OK with the removed binding in data, or 404 when the user has no link of that type. Unlinking LDAP also clears ldap_only; the user then needs a password reset to sign in with a local password.

Report
curl "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/identities/legacy-saml?idp_id=7" \
-H "Authorization: ApiKey lmk_abc123"
200 OK
{
"data": {
"idp_count": 2,
"ambiguous": true,
"users": [
{
"user_id": "01JF3KABCDE...",
"email": "jane@acme.com",
"name": "Jane Doe",
"name_id": "jane@acme.com",
"candidate_idp_ids": [7],
"suggested_idp_id": 7
}
],
"idps": [
{ "id": 7, "display_name": "Okta", "entity_id": "http://www.okta.com/exk...", "allowed_email_domains": ["acme.com"] }
]
}
}

candidate_idp_ids are the IdPs whose Allowed email domains claim the user's address; suggested_idp_id is set when exactly one does. idp_id (optional) keeps only the users that IdP claims.

Preview, then relink
curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/identities/legacy-saml \
-H "Authorization: ApiKey lmk_abc123" \
-H "Content-Type: application/json" \
-d '{"idp_id": 7, "all_matching_domains": true, "dry_run": true}'
FieldTypeDescription
idp_idintegerRequired. The IdP that issues these users' NameIDs
user_idsstring[]The legacy users to relink
all_matching_domainsbooleanInstead of user_ids: every legacy user whose email domain the IdP's allowed email domains claim (the IdP must have some)
dry_runbooleanDefault true: report what would change without changing anything
200 OK
{
"data": {
"dry_run": true,
"idp_id": 7,
"relinked": [{ "user_id": "01JF3KABCDE...", "email": "jane@acme.com", "name_id": "jane@acme.com" }],
"skipped": [{ "user_id": "01JF3KXYZ...", "email": "cfo@acme.com", "name_id": "cfo@acme.com", "reason": "outranked" }]
}
}

Relinking keeps each user's NameID and binds it to idp_id. Skipped users carry a reason: outranked (the account holds permissions you lack), name_id_already_linked (another user holds that NameID at the IdP) or not_a_legacy_saml_user. A real run ("dry_run": false) signs each relinked user out, emails them, and is audited per user (identity.link.created) and once for the run (identity.link.bulk_relinked).

Reset MFA (removed)​

Reset MFA
curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../mfa/reset \
-H "Authorization: ApiKey lmk_abc123"
410 Gone
{
"error": "mfa_reset_removed",
"message": "Admins cannot disable MFA for a user. Issue a temporary access code instead.",
"status": 410,
"details": { "use": "/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../temporary-access-code" }
}
Removed in September 2026

Administrators can no longer switch off or clear a user's MFA. This endpoint now always returns 410 Gone (operation resetUserMfa, deprecated). Use instead:

The mfaEnabled field on the user object is read-only and reflects whether the user holds at least one active sign-in method (passkey, LumoAuth app, authenticator app, phone or email code).

Delete User​

Delete a User
curl -X DELETE https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE... \
-H "Authorization: ApiKey lmk_abc123"

Returns 200 OK with { "message": "User deleted successfully" } on success. This action is irreversible: deleting a user runs the full GDPR erasure, the same as the portal and a processed erasure request. The user's tokens, sessions, login attempts, consents, invitations and one-time links are removed, audit rows are pseudonymised (they keep ids only), and downstream recipients are notified under Article 19. See Right to erasure.