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.
| Method | Endpoint | Permission | API-key / token scope |
|---|---|---|---|
| GET | /admin/users | users.view | admin:users:read |
| GET | /admin/users/{userId} | users.view | admin:users:read |
| POST | /admin/users | users.create | admin:users:write |
| PUT PATCH | /admin/users/{userId} | users.edit | admin:users:write |
| DELETE | /admin/users/{userId} | users.delete | admin:users:write |
| POST | /admin/users/{userId}/block | users.edit | admin:users:write |
| POST | /admin/users/{userId}/unblock | users.edit | admin:users:write |
| POST | /admin/users/{userId}/mfa/reset | users.edit | admin:users:write |
| GET | /admin/users/{userId}/roles | users.view | admin:users:read |
| PUT | /admin/users/{userId}/roles | users.edit | admin:users:write |
| POST | /admin/users/{userId}/roles | users.edit | admin:users:write |
| DELETE | /admin/users/{userId}/roles/{roleId} | users.edit | admin:users:write |
| GET | /admin/users/{userId}/groups | users.view | admin:users:read |
| PUT | /admin/users/{userId}/groups | users.edit | admin:users:write |
| POST | /admin/users/{userId}/groups | users.edit | admin:users:write |
| DELETE | /admin/users/{userId}/groups/{groupId} | users.edit | admin:users:write |
| GET | /admin/users/{userId}/permissions | users.view | admin:users:read |
| POST | /admin/users/{userId}/permissions | users.edit | admin:users:write |
| DELETE | /admin/users/{userId}/permissions/{permissionId} | users.edit | admin:users:write |
| POST | /admin/users/{userId}/verify-email | users.edit | admin:users:write |
| POST | /admin/users/{userId}/mark-verified | users.edit | admin:users:write |
| POST | /admin/users/{userId}/password-reset | users.edit | admin:users:write |
| PUT POST | /admin/users/{userId}/password | users.edit | admin: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.
| Method | Header |
|---|---|
| API Key | Authorization: ApiKey lmk_... or X-API-Key: lmk_... |
| Bearer Token | Authorization: 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.
{
"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
}
}
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
- Node
- Python
- Go
curl "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users?page=1&limit=50" \
-H "Authorization: ApiKey lmk_abc123"
const resp = await fetch("https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users?page=1&limit=50", {
headers: { "Authorization": `ApiKey ${process.env.LUMO_ADMIN_KEY}` }
});
const users = await resp.json();
console.log(users.data, users.meta);
import os, requests
resp = requests.get(
"https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users",
params={"page": 1, "limit": 50},
headers={"Authorization": f"ApiKey {os.environ['LUMO_ADMIN_KEY']}"},
)
users = resp.json()
print(users["data"], users["meta"])
req, _ := http.NewRequest("GET", "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users?page=1&limit=50", nil)
req.Header.Set("Authorization", "ApiKey "+os.Getenv("LUMO_ADMIN_KEY"))
resp, err := http.DefaultClient.Do(req)
if err != nil { return err }
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))
Query Parameters
| Parameter | Description |
|---|---|
page | Page number (default: 1) |
limit | Results per page (default: 25, max: 100) |
search | Filter by email, name, or username |
active | Filter by active status (true/false) |
blocked | Filter by blocked status (true/false) |
emailVerified | Filter by email-verified status (true/false) |
mfaEnabled | Filter by MFA-enabled status (true/false) |
role | Filter by role slug |
group | Filter by group slug |
sortBy | Field to sort by |
sortDir | Sort direction (ASC/DESC, default DESC) |
List rows are a trimmed projection of the User Object (no roles/groups/locale/zoneinfo/updatedAt).
{
"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
- curl
- Node
- Python
- Go
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"]
}'
const resp = await fetch("https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users", {
method: "POST",
headers: { "Authorization": process.env.LUMO_AUTH, "Content-Type": "application/json" },
body: JSON.stringify({
"email": "jane@acme.com",
"name": "Jane Smith",
"givenName": "Jane",
"familyName": "Smith",
"password": "Str0ng!Pass",
"emailVerified": true,
"roles": ["viewer"]
})
});
console.log(await resp.json());
import os, requests
resp = requests.post(
"https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users",
headers={"Authorization": os.environ["LUMO_AUTH"], "Content-Type": "application/json"},
json={
"email": "jane@acme.com",
"name": "Jane Smith",
"givenName": "Jane",
"familyName": "Smith",
"password": "Str0ng!Pass",
"emailVerified": True,
"roles": ["viewer"]
}
)
print(resp.json())
import (
"io"
"net/http"
"os"
"strings"
)
body := strings.NewReader(`{
"email": "jane@acme.com",
"name": "Jane Smith",
"givenName": "Jane",
"familyName": "Smith",
"password": "Str0ng!Pass",
"emailVerified": true,
"roles": ["viewer"]
}`)
req, _ := http.NewRequest("POST", "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users", body)
req.Header.Set("Authorization", os.Getenv("LUMO_AUTH"))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))
Returns 201 Created with the new user object.
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
- curl
- Node
- Python
- Go
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"
}'
const resp = await fetch("https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE...", {
method: "PUT",
headers: { "Authorization": process.env.LUMO_AUTH, "Content-Type": "application/json" },
body: JSON.stringify({
"name": "Jane Doe",
"givenName": "Jane",
"familyName": "Doe"
})
});
console.log(await resp.json());
import os, requests
resp = requests.put(
"https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE...",
headers={"Authorization": os.environ["LUMO_AUTH"], "Content-Type": "application/json"},
json={
"name": "Jane Doe",
"givenName": "Jane",
"familyName": "Doe"
}
)
print(resp.json())
import (
"io"
"net/http"
"os"
"strings"
)
body := strings.NewReader(`{
"name": "Jane Doe",
"givenName": "Jane",
"familyName": "Doe"
}`)
req, _ := http.NewRequest("PUT", "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE...", body)
req.Header.Set("Authorization", os.Getenv("LUMO_AUTH"))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))
Use PATCH for partial updates (only the supplied fields are changed).
Block / Unblock a User
- curl
- Node
- Python
- Go
curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../block \
-H "Authorization: ApiKey lmk_abc123"
const resp = await fetch("https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../block", {
method: "POST",
headers: { "Authorization": process.env.LUMO_AUTH, "Content-Type": "application/json" }
});
console.log(await resp.json());
import os, requests
resp = requests.post(
"https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../block",
headers={"Authorization": os.environ["LUMO_AUTH"]}
)
print(resp.json())
import (
"io"
"net/http"
"os"
"strings"
)
req, _ := http.NewRequest("POST", "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../block", nil)
req.Header.Set("Authorization", os.Getenv("LUMO_AUTH"))
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))
- curl
- Node
- Python
- Go
curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../unblock \
-H "Authorization: ApiKey lmk_abc123"
const resp = await fetch("https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../unblock", {
method: "POST",
headers: { "Authorization": process.env.LUMO_AUTH, "Content-Type": "application/json" }
});
console.log(await resp.json());
import os, requests
resp = requests.post(
"https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../unblock",
headers={"Authorization": os.environ["LUMO_AUTH"]}
)
print(resp.json())
import (
"io"
"net/http"
"os"
"strings"
)
req, _ := http.NewRequest("POST", "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../unblock", nil)
req.Header.Set("Authorization", os.Getenv("LUMO_AUTH"))
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))
Manage User Roles
Get User Roles
- curl
- Node
- Python
- Go
curl https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../roles \
-H "Authorization: ApiKey lmk_abc123"
const resp = await fetch("https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../roles", {
method: "GET",
headers: { "Authorization": process.env.LUMO_AUTH, "Content-Type": "application/json" }
});
console.log(await resp.json());
import os, requests
resp = requests.get(
"https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../roles",
headers={"Authorization": os.environ["LUMO_AUTH"]}
)
print(resp.json())
import (
"io"
"net/http"
"os"
"strings"
)
req, _ := http.NewRequest("GET", "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../roles", nil)
req.Header.Set("Authorization", os.Getenv("LUMO_AUTH"))
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))
Assign a Role
- curl
- Node
- Python
- Go
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..."}'
const resp = await fetch("https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../roles", {
method: "POST",
headers: { "Authorization": process.env.LUMO_AUTH, "Content-Type": "application/json" },
body: JSON.stringify({"roleId": "01JF3KROLE..."})
});
console.log(await resp.json());
import os, requests
resp = requests.post(
"https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../roles",
headers={"Authorization": os.environ["LUMO_AUTH"], "Content-Type": "application/json"},
json={"roleId": "01JF3KROLE..."}
)
print(resp.json())
import (
"io"
"net/http"
"os"
"strings"
)
body := strings.NewReader(`{"roleId": "01JF3KROLE..."}`)
req, _ := http.NewRequest("POST", "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../roles", body)
req.Header.Set("Authorization", os.Getenv("LUMO_AUTH"))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))
Remove a Role
- curl
- Node
- Python
- Go
curl -X DELETE https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../roles/01JF3KROLE... \
-H "Authorization: ApiKey lmk_abc123"
const resp = await fetch("https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../roles/01JF3KROLE...", {
method: "DELETE",
headers: { "Authorization": process.env.LUMO_AUTH, "Content-Type": "application/json" }
});
console.log(await resp.json());
import os, requests
resp = requests.delete(
"https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../roles/01JF3KROLE...",
headers={"Authorization": os.environ["LUMO_AUTH"]}
)
print(resp.json())
import (
"io"
"net/http"
"os"
"strings"
)
req, _ := http.NewRequest("DELETE", "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../roles/01JF3KROLE...", nil)
req.Header.Set("Authorization", os.Getenv("LUMO_AUTH"))
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))
Manage User Groups
Get User Groups
- curl
- Node
- Python
- Go
curl https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../groups \
-H "Authorization: ApiKey lmk_abc123"
const resp = await fetch("https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../groups", {
method: "GET",
headers: { "Authorization": process.env.LUMO_AUTH, "Content-Type": "application/json" }
});
console.log(await resp.json());
import os, requests
resp = requests.get(
"https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../groups",
headers={"Authorization": os.environ["LUMO_AUTH"]}
)
print(resp.json())
import (
"io"
"net/http"
"os"
"strings"
)
req, _ := http.NewRequest("GET", "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../groups", nil)
req.Header.Set("Authorization", os.Getenv("LUMO_AUTH"))
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))
Add to a Group
- curl
- Node
- Python
- Go
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..."}'
const resp = await fetch("https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../groups", {
method: "POST",
headers: { "Authorization": process.env.LUMO_AUTH, "Content-Type": "application/json" },
body: JSON.stringify({"groupId": "01JF3KGRP..."})
});
console.log(await resp.json());
import os, requests
resp = requests.post(
"https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../groups",
headers={"Authorization": os.environ["LUMO_AUTH"], "Content-Type": "application/json"},
json={"groupId": "01JF3KGRP..."}
)
print(resp.json())
import (
"io"
"net/http"
"os"
"strings"
)
body := strings.NewReader(`{"groupId": "01JF3KGRP..."}`)
req, _ := http.NewRequest("POST", "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../groups", body)
req.Header.Set("Authorization", os.Getenv("LUMO_AUTH"))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))
Remove from a Group
- curl
- Node
- Python
- Go
curl -X DELETE https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../groups/01JF3KGRP... \
-H "Authorization: ApiKey lmk_abc123"
const resp = await fetch("https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../groups/01JF3KGRP...", {
method: "DELETE",
headers: { "Authorization": process.env.LUMO_AUTH, "Content-Type": "application/json" }
});
console.log(await resp.json());
import os, requests
resp = requests.delete(
"https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../groups/01JF3KGRP...",
headers={"Authorization": os.environ["LUMO_AUTH"]}
)
print(resp.json())
import (
"io"
"net/http"
"os"
"strings"
)
req, _ := http.NewRequest("DELETE", "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../groups/01JF3KGRP...", nil)
req.Header.Set("Authorization", os.Getenv("LUMO_AUTH"))
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))
Manage User Permissions
Direct permissions on users are additive — the user retains all permissions inherited from their roles and groups.
Get User Permissions
- curl
- Node
- Python
- Go
curl https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../permissions \
-H "Authorization: ApiKey lmk_abc123"
const resp = await fetch("https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../permissions", {
method: "GET",
headers: { "Authorization": process.env.LUMO_AUTH, "Content-Type": "application/json" }
});
console.log(await resp.json());
import os, requests
resp = requests.get(
"https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../permissions",
headers={"Authorization": os.environ["LUMO_AUTH"]}
)
print(resp.json())
import (
"io"
"net/http"
"os"
"strings"
)
req, _ := http.NewRequest("GET", "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../permissions", nil)
req.Header.Set("Authorization", os.Getenv("LUMO_AUTH"))
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))
Assign a Permission
- curl
- Node
- Python
- Go
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}'
const resp = await fetch("https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../permissions", {
method: "POST",
headers: { "Authorization": process.env.LUMO_AUTH, "Content-Type": "application/json" },
body: JSON.stringify({"permissionId": 42})
});
console.log(await resp.json());
import os, requests
resp = requests.post(
"https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../permissions",
headers={"Authorization": os.environ["LUMO_AUTH"], "Content-Type": "application/json"},
json={"permissionId": 42}
)
print(resp.json())
import (
"io"
"net/http"
"os"
"strings"
)
body := strings.NewReader(`{"permissionId": 42}`)
req, _ := http.NewRequest("POST", "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../permissions", body)
req.Header.Set("Authorization", os.Getenv("LUMO_AUTH"))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))
Remove a Permission
- curl
- Node
- Python
- Go
curl -X DELETE https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../permissions/42 \
-H "Authorization: ApiKey lmk_abc123"
const resp = await fetch("https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../permissions/42", {
method: "DELETE",
headers: { "Authorization": process.env.LUMO_AUTH, "Content-Type": "application/json" }
});
console.log(await resp.json());
import os, requests
resp = requests.delete(
"https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../permissions/42",
headers={"Authorization": os.environ["LUMO_AUTH"]}
)
print(resp.json())
import (
"io"
"net/http"
"os"
"strings"
)
req, _ := http.NewRequest("DELETE", "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../permissions/42", nil)
req.Header.Set("Authorization", os.Getenv("LUMO_AUTH"))
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))
Password Management
- curl
- Node
- Python
- Go
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}'
const resp = await fetch("https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../password", {
method: "PUT",
headers: { "Authorization": process.env.LUMO_AUTH, "Content-Type": "application/json" },
body: JSON.stringify({"password": "NewStr0ng!Pass", "requireChange": true})
});
console.log(await resp.json());
import os, requests
resp = requests.put(
"https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../password",
headers={"Authorization": os.environ["LUMO_AUTH"], "Content-Type": "application/json"},
json={"password": "NewStr0ng!Pass", "requireChange": True}
)
print(resp.json())
import (
"io"
"net/http"
"os"
"strings"
)
body := strings.NewReader(`{"password": "NewStr0ng!Pass", "requireChange": true}`)
req, _ := http.NewRequest("PUT", "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../password", body)
req.Header.Set("Authorization", os.Getenv("LUMO_AUTH"))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))
- curl
- Node
- Python
- Go
curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../password-reset \
-H "Authorization: ApiKey lmk_abc123"
const resp = await fetch("https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../password-reset", {
method: "POST",
headers: { "Authorization": process.env.LUMO_AUTH, "Content-Type": "application/json" }
});
console.log(await resp.json());
import os, requests
resp = requests.post(
"https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../password-reset",
headers={"Authorization": os.environ["LUMO_AUTH"]}
)
print(resp.json())
import (
"io"
"net/http"
"os"
"strings"
)
req, _ := http.NewRequest("POST", "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../password-reset", nil)
req.Header.Set("Authorization", os.Getenv("LUMO_AUTH"))
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))
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.
- curl
- Node
- Python
- Go
curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../verify-email \
-H "Authorization: ApiKey lmk_abc123"
const resp = await fetch("https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../verify-email", {
method: "POST",
headers: { "Authorization": process.env.LUMO_AUTH, "Content-Type": "application/json" }
});
console.log(await resp.json());
import os, requests
resp = requests.post(
"https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../verify-email",
headers={"Authorization": os.environ["LUMO_AUTH"]}
)
print(resp.json())
import (
"io"
"net/http"
"os"
"strings"
)
req, _ := http.NewRequest("POST", "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../verify-email", nil)
req.Header.Set("Authorization", os.Getenv("LUMO_AUTH"))
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))
- curl
- Node
- Python
- Go
curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../mark-verified \
-H "Authorization: ApiKey lmk_abc123"
const resp = await fetch("https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../mark-verified", {
method: "POST",
headers: { "Authorization": process.env.LUMO_AUTH, "Content-Type": "application/json" }
});
console.log(await resp.json());
import os, requests
resp = requests.post(
"https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../mark-verified",
headers={"Authorization": os.environ["LUMO_AUTH"]}
)
print(resp.json())
import (
"io"
"net/http"
"os"
"strings"
)
req, _ := http.NewRequest("POST", "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../mark-verified", nil)
req.Header.Set("Authorization", os.Getenv("LUMO_AUTH"))
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))
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 scopeadmin:users:read); changes needusers.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 returns403 step_up_requiredand you retry with an approved step-up challenge id inX-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
curl https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../identities \
-H "Authorization: ApiKey lmk_abc123"
{
"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 an 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"}'
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"}'
| Field | Type | Description |
|---|---|---|
type | string | Required. saml or ldap |
idp_id | integer | SAML: the SAML identity provider's id |
name_id | string | SAML: the NameID the IdP asserts for this user |
ldap_config_id | integer | LDAP: the directory connection's id |
dn | string | LDAP: the entry's distinguished name. Omit it to look the entry up in the directory by the user's email, then username |
ldap_only | boolean | LDAP: 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.
| Status | error | When |
|---|---|---|
403 | forbidden / step_up_required | The account holds permissions you lack, or step-up is needed |
404 | not_found | The user, IdP or directory is not in this organization |
409 | identity_conflict | Another user already holds that NameID at the IdP (or that DN in the directory), or the user is linked to a different federated source |
422 | invalid_identity | Missing or malformed type, name_id or dn |
422 | directory_lookup_failed | No dn given and the directory search found nothing or failed — pass the DN |
Unlink an identity
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.
Relink legacy SAML users
curl "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/identities/legacy-saml?idp_id=7" \
-H "Authorization: ApiKey lmk_abc123"
{
"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.
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}'
| Field | Type | Description |
|---|---|---|
idp_id | integer | Required. The IdP that issues these users' NameIDs |
user_ids | string[] | The legacy users to relink |
all_matching_domains | boolean | Instead of user_ids: every legacy user whose email domain the IdP's allowed email domains claim (the IdP must have some) |
dry_run | boolean | Default true: report what would change without changing anything |
{
"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)
curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE.../mfa/reset \
-H "Authorization: ApiKey lmk_abc123"
{
"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" }
}
Administrators can no longer switch off or clear a user's MFA. This endpoint now always returns 410 Gone (operation resetUserMfa, deprecated). Use instead:
POST /admin/users/{userId}/temporary-access-code— a one-time code that lets a locked-out user sign in and enroll a new factor. Requires a reason, is audited and notifies the user.DELETE /admin/users/{userId}/authenticators/{authenticatorId}— remove a single lost authenticator, such as a stolen phone.GET /admin/users/{userId}/authenticators— see what the user has enrolled.
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
- curl
- Node
- Python
- Go
curl -X DELETE https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE... \
-H "Authorization: ApiKey lmk_abc123"
const resp = await fetch("https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE...", {
method: "DELETE",
headers: { "Authorization": process.env.LUMO_AUTH, "Content-Type": "application/json" }
});
console.log(await resp.json());
import os, requests
resp = requests.delete(
"https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE...",
headers={"Authorization": os.environ["LUMO_AUTH"]}
)
print(resp.json())
import (
"io"
"net/http"
"os"
"strings"
)
req, _ := http.NewRequest("DELETE", "https://app.lumoauth.dev/orgs/acme-corp/api/v1/admin/users/01JF3KABCDE...", nil)
req.Header.Set("Authorization", os.Getenv("LUMO_AUTH"))
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(string(out))
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.