Skip to content

Users API

/api/v1/users/* is the admin user surface. It is called by trusted backends — your management UIs, your batch jobs, your migration scripts. Scoped to users:read (reads) and users:write (mutations).

End-user self-service does NOT happen here; that's the Me API.

GET /api/v1/users
Authorization: Bearer <access-token>
Required scope: users:read
Query parameters:
cursor — opaque pagination cursor
limit — page size (1–250, default 50)
email — exact email filter
state — 'active' | 'disabled' | 'pending'
created_after — ISO timestamp
q — free-text search across email + name + attributes
{
"data": [
{
"id": "usr_01HZX...",
"email": "user@cymmetri.com",
"email_verified": true,
"name": "User Name",
"state": "active",
"created_at": "2026-01-15T10:00:00Z",
"last_signed_in_at": "2026-05-17T08:00:00Z"
}
],
"meta": { "next_cursor": "...", "limit": 50 }
}
POST /api/v1/users
Authorization: Bearer <access-token>
Content-Type: application/json
Required scope: users:write
Idempotency-Key: <uuid> (recommended)
{
"email": "finance.lead@cymmetri.com",
"name": "Finance Lead",
"password": "a temporary one",
"password_must_be_reset": true,
"email_verified": false,
"attributes": { "department": "finance" }
}

Returns the created user. If email_verified is omitted or false, a verification email is sent.

If the email already exists, returns 409 user_exists. If you want "create or get", call GET /api/v1/users?email=... first.

GET /api/v1/users/{user_id}
Authorization: Bearer <access-token>
Required scope: users:read

The detailed user record includes:

{
"data": {
"id": "usr_01HZX...",
"email": "user@cymmetri.com",
"email_verified": true,
"name": "User Name",
"state": "active",
"attributes": { ... },
"mfa_factors": [
{ "kind": "webauthn", "count": 2 },
{ "kind": "totp", "count": 1 }
],
"groups": [
{ "id": "grp_01HZX...", "name": "Finance" }
],
"applications_used": [ "app_01HZX...", "app_01HZY..." ],
"created_at": "...",
"updated_at": "...",
"last_signed_in_at": "..."
}
}

The DTO summarises sub-resources (factor counts, group memberships, recently-used applications) so a single fetch is enough for a typical admin "user detail" page. Fetch the full lists with the per-sub-resource endpoints when needed.

PATCH /api/v1/users/{user_id}
Authorization: Bearer <access-token>
Content-Type: application/json
Required scope: users:write
{
"name": "Anita Singh",
"attributes": { "department": "engineering" }
}

Fields not in the body are unchanged. Some fields are immutable (id, created_at); the API rejects an attempt to PATCH them.

Soft state changes that preserve the record:

POST /api/v1/users/{user_id}/disable
POST /api/v1/users/{user_id}/enable
Required scope: users:write

A disabled user cannot sign in. All their sessions are revoked. Their record stays. Re-enabling restores the ability to sign in but does not restore sessions.

DELETE /api/v1/users/{user_id}
Authorization: Bearer <access-token>
Required scope: users:write

Hard delete: the user record + all their factors + all their sessions are removed. The audit log entries about them are retained per the tenant's retention policy.

For GDPR-style erasure where you want absolute removal including audit, the data export + erasure flow is in the tenant admin console.

POST /api/v1/users/{user_id}/reset-password
Authorization: Bearer <access-token>
Required scope: users:write

Sends the user a password-reset email. Does not return a new password.

To set a temporary password directly (operator-set rather than user-set), use PATCH with { "password": "...", "password_must_be_reset": true }. The user is forced to change it on next sign-in.

POST /api/v1/users/{user_id}/force-mfa-reset
Authorization: Bearer <access-token>
Required scope: users:write

Removes all the user's MFA factors. They are forced to re-enrol on next sign-in. Recovery path for users who lost their second factor; pair with a separate user-identity-verification step.

POST /api/v1/users/bulk-import
Authorization: Bearer <access-token>
Content-Type: application/x-ndjson
Required scope: users:write
{"email":"a@cymmetri.com","name":"A","password":"..."}
{"email":"b@cymmetri.com","name":"B","password":"..."}

Streamed input. Returns a job id; poll the job for status:

GET /api/v1/users/bulk-import/{job_id}
{
"data": {
"id": "job_01HZX...",
"state": "running",
"imported": 1500,
"failed": 3,
"errors": [ ... ]
}
}

For tens-of-thousands-of-users migrations, the bulk-import endpoint is preferable to a serial loop over POST /api/v1/users — it batches transactionally on the platform side.

ErrorWhen
user_not_foundThe id doesn't exist or you can't see it (404)
user_existsA user with that email already exists on create
insufficient_scopeToken doesn't carry the capability this action requires
password_policy_violationThe password does not meet the tenant's policy
email_invalidThe email is malformed