REST API
The Users area publishes 42 REST endpoints. Paths are relative to https://api.lessly.com; see Authentication for the required header.
GET/users/api-keys
List this product's API keys, newest first — id, label, type, visible prefix and revocation state. A server key's secret is never here; a publishable key's full value is, because it is not a secret.
MCP tool: users_apikeys_list
Parameters
No parameters.
Response
| Field | Type | Always | Description |
|---|
| keys | array | • | — |
POST/users/api-keys
Issue an API key for this product. type='server' mints a secret backend key (usk_…) returned EXACTLY ONCE — it authenticates the code-exchange leg and is stored only as a hash, so a lost key must be replaced, never recovered. type='publishable' mints a non-secret browser key (upk_…) that only names this product and stays readable afterwards.
MCP tool: users_apikeys_create
Parameters
| Name | In | Type | Required | Description |
|---|
| name | body | string | • | A label only humans read — which backend or environment holds this key. |
| type | body | string | • | 'server' mints a SECRET key (usk_…) for the Product's backend — returned exactly once and stored hashed. 'publishable' mints a non-secret key (upk_…) that identifies this product in browser flows and stays readable. |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | — |
| key | string | • | — |
| name | string | • | — |
| type | string | • | — |
| value | string | • | — |
| prefix | string | • | — |
| createdAt | string | • | — |
| revokedAt | string | • | — |
DELETE/users/api-keys/:id
Revoke an API key immediately. A revoked server key stops authenticating code exchanges on the next request; there is no grace period and no un-revoke.
MCP tool: users_apikeys_revoke
Parameters
| Name | In | Type | Required | Description |
|---|
| id | path | string | • | Id of the key to revoke. Revocation is immediate and final. |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | — |
| revoked | boolean | • | — |
GET/users/audit
Read this product's audit log, newest first. Every management-plane mutation writes one entry (user.created, user.updated, user.deleted, user.banned, user.unbanned) with the actor, target and timestamp. Filter by action, target and time range; paginate with the opaque `nextCursor`.
MCP tool: users_audit_list
Parameters
| Name | In | Type | Required | Description |
|---|
| action | query | string | — | Exact action to filter by, e.g. 'user.banned'. Omit for all actions. |
| targetType | query | string | — | Target kind to filter by, e.g. 'user'. |
| targetId | query | string | — | Exact id of the audited target, e.g. a user id. |
| since | query | string | — | ISO-8601 lower bound (inclusive) on the event timestamp. |
| until | query | string | — | ISO-8601 upper bound (exclusive) on the event timestamp. |
| limit | query | string | — | Page size, 1-100. Default 25. |
| cursor | query | string | — | Opaque cursor from a previous response's `nextCursor`. Omit for the first page. |
Response
| Field | Type | Always | Description |
|---|
| events | array | • | — |
| nextCursor | string | • | — |
GET/users/config
Read this product's end-user auth configuration (signup mode, password policy, session lifetime). Returns the defaults when nothing has been configured yet.
MCP tool: users_config_get
Parameters
No parameters.
Response
| Field | Type | Always | Description |
|---|
| mfa | object | • | — |
| email | object | • | — |
| oauth | object | • | — |
| invite | object | • | — |
| status | string | • | — |
| captcha | object | • | — |
| recovery | object | • | — |
| testMode | object | • | — |
| waitlist | object | • | — |
| productId | string | • | — |
| retention | object | • | — |
| protection | object | • | — |
| signupMode | string | • | — |
| emailChange | object | • | — |
| notifications | object | • | — |
| allowedOrigins | array | • | — |
| passwordPolicy | object | • | — |
| sessionTtlHours | integer | • | — |
| lifecycleBlocked | boolean | • | — |
| lifecycleArchived | boolean | • | — |
| refreshSlidingDays | integer | • | — |
| refreshAbsoluteDays | integer | • | — |
| redirectUriAllowlist | array | • | — |
| accessTokenTtlMinutes | integer | • | — |
| captchaSecretConfigured | boolean | • | — |
PUT/users/config
Create or update this product's end-user auth configuration. Only the fields you pass change; the rest keep their current values. Rejected while the product is paused (archived or blocked by the platform).
MCP tool: users_config_upsert
Parameters
| Name | In | Type | Required | Description |
|---|
| signupMode | body | string | — | How end users may join this product: 'public' (anyone), 'invite-only', or 'waitlist'. Default 'public'. |
| passwordPolicy | body | object | — | Password rules for this product. Merged field-by-field into the current policy. |
| sessionTtlHours | body | integer | — | End-user session lifetime in hours. Default 720 (30 days). |
| accessTokenTtlMinutes | body | integer | — | Access JWT lifetime in minutes, 5-60. Default 10. Short on purpose: a revoked session dies within one TTL. |
| refreshSlidingDays | body | integer | — | Inactivity window of a session in days — every refresh slides it forward. Default 30. |
| refreshAbsoluteDays | body | integer | — | Hard time-box of a session in days, counted from sign-in and never extended. Default 365. |
| redirectUriAllowlist | body | array | — | Callback URIs the browser code handoff may deliver a one-time authorization code to, up to 20. Matched EXACTLY — no wildcards, no prefixes, no fragments — so list every callback in full. Replaces the whole list. |
| allowedOrigins | body | array | — | Browser origins allowed to drive this product's end-user flows, up to 20. Scheme + host + port only, matched exactly. Replaces the whole list. Empty means "no browser origin" once this product has a publishable key. |
| protection | body | object | — | Rate limits, lockout and breached-password settings. Merged field-by-field. |
| captcha | body | object | — | Captcha (Cloudflare Turnstile) settings for flow creation. Merged field-by-field. |
| email | body | object | — | Email OTP / magic-link settings. Merged field-by-field. Set `email.from` to a sender on a domain verified in this product's mail toolkit — until then the email strategies are not offered. |
| recovery | body | object | — | Password-recovery settings (whether it is offered, how long a reset link lives). Merged field-by-field. |
| emailChange | body | object | — | Email-change settings: confirmation lifetime, the old address's revert window, the post-revert cooldown and the operator-override delay. Merged field-by-field. |
| invite | body | object | — | Invite lifetime and re-invite cooldown. Merged field-by-field. |
| waitlist | body | object | — | Waitlist settings: the double-opt-in toggle, how long a pending entry is retained before the sweep erases it, the confirmation link lifetime, and the embeddable widget’s copy and colours. Merged field-by-field, including inside `widget`. |
| testMode | body | object | — | Test mode: whether this product may have TEST USERS — accounts that are excluded from metering, kept out of the default directory listing, never really emailed, and (when the deployment sets one) able to sign in with the fixed magic OTP. REFUSED on a production deployment. Merged field-by-field. |
| retention | body | object | — | How long DEAD rows are kept before the retention sweep removes them: spent one-time tokens, expired flow attempts, expired sessions and their refresh tokens. These are grace periods on top of expiry, never lifetimes — an unswept expired row is inert. Merged field-by-field. |
| mfa | body | object | — | MFA settings: backup-code batch size, the operator-reset delay, how fresh authentication must be to enroll, and the second-factor lockout ceilings. Merged field-by-field. |
| notifications | body | object | — | Which security notification emails this product sends. All on by default. Merged field-by-field. |
| oauth | body | object | — | OAuth provider credentials and switches, per provider. Merged field-by-field. A provider is offered as a sign-in strategy only when it is enabled AND a full client id + secret pair resolves — from here, or from the deployment's shared development credentials. |
Response
| Field | Type | Always | Description |
|---|
| mfa | object | • | — |
| email | object | • | — |
| oauth | object | • | — |
| invite | object | • | — |
| status | string | • | — |
| captcha | object | • | — |
| recovery | object | • | — |
| testMode | object | • | — |
| waitlist | object | • | — |
| productId | string | • | — |
| retention | object | • | — |
| protection | object | • | — |
| signupMode | string | • | — |
| emailChange | object | • | — |
| notifications | object | • | — |
| allowedOrigins | array | • | — |
| passwordPolicy | object | • | — |
| sessionTtlHours | integer | • | — |
| lifecycleBlocked | boolean | • | — |
| lifecycleArchived | boolean | • | — |
| refreshSlidingDays | integer | • | — |
| refreshAbsoluteDays | integer | • | — |
| redirectUriAllowlist | array | • | — |
| accessTokenTtlMinutes | integer | • | — |
| captchaSecretConfigured | boolean | • | — |
GET/users/keys
List this product's JWT signing keys, newest first: the key id (the `kid` a token header carries), its lifecycle state, and when it was created, retired and revoked. `current` signs; `retired` no longer signs but is STILL published in JWKS until `overlapExpiresAt`, so tokens it signed keep verifying; `standby` is the pre-generated successor and is not published; `revoked` was evicted from JWKS immediately. Key material — public or private — is never returned here.
MCP tool: users_keys_list
Parameters
No parameters.
Response
| Field | Type | Always | Description |
|---|
| keys | array | • | — |
POST/users/keys/revoke
THE KEY-COMPROMISE DRILL. Evict a signing key from the published JWKS IMMEDIATELY — no overlap, no grace — so every token it ever signed stops verifying at once, mint or promote a fresh signing key in its place, and (by default) sign every end user of this product out, because a token the attacker forged with the stolen key is indistinguishable from a real one. Pass `revokeSessions: false` only when you are certain no forged token was issued. Give a reason: it is the only account of the incident the audit log will keep. This is disruptive and irreversible — for routine rotation use users_keys_rotate.
MCP tool: users_keys_revoke
Parameters
| Name | In | Type | Required | Description |
|---|
| kid | body | string | • | The key to evict, as listed by users_keys_list. |
| reason | body | string | — | Why. Goes into the audit row and is the only account of the incident it keeps. |
| revokeSessions | body | boolean | — | Sign every end user of this product out as well. Defaults to TRUE: a compromised signing key could have minted any of those sessions. Pass false ONLY when you know no forged token was ever issued. |
Response
| Field | Type | Always | Description |
|---|
| currentKid | string | • | The fresh key minted or promoted in its place. |
| revokedKid | string | • | — |
| sessionsRevoked | integer | • | — |
POST/users/keys/rotate
Rotate this product's JWT signing key with ZERO verifier downtime. The standby key (generated now if there is none) becomes the signing key, and the old one is retired into an overlap window — BOTH key ids stay in the published JWKS until `overlapExpiresAt`, so every token signed before the rotation keeps verifying for the rest of its life and no verifier has to re-fetch anything to keep working. The overlap is the product's access-token TTL plus five minutes. After it elapses the old key is revoked automatically and stops verifying. Use users_keys_revoke instead if the key is COMPROMISED and must die now.
MCP tool: users_keys_rotate
Parameters
No parameters.
Response
| Field | Type | Always | Description |
|---|
| currentKid | string | • | The key signing from now on. |
| previousKid | string | • | The key just retired. Null if there was none. |
| overlapExpiresAt | string | • | When the retired key stops verifying. Both kids are in JWKS until then. |
POST/users/ops/retention
What this product keeps and for how long. Returns the EFFECTIVE retention windows (spent one-time tokens, expired flow attempts, expired sessions and their refresh tokens) next to the documented defaults, the separate waitlist-entry window, the single cron that performs every sweep, and — deliberately — the list of what is NEVER swept and why. For the waitlist it also answers the two questions a published retention promise raises: whether this product's window is WIDER than the 180 days Lessly published (`waitlistWiderThanPromised`, a flag rather than a refusal), and whether the job that honours it is actually running — `lastSweep` carries the last pass this replica performed, counting THIS product's erasures only, and is NULL when it has observed none — which is not the same as a pass that erased zero. The windows are GRACE PERIODS ON TOP OF EXPIRY, never lifetimes: every live read carries its own expiry predicate, so an unswept expired row is inert and a window of 0 shortens nothing. Change them with users_config_upsert `retention`.
MCP tool: users_ops_retention_get
Parameters
No parameters.
Response
| Field | Type | Always | Description |
|---|
| sweep | object | • | The single retention job. There is exactly one — it replaced the sessions reaper and the waitlist sweep. |
| windows | object | • | This product’s EFFECTIVE windows — what it configured, with the documented default filling anything it did not. |
| defaults | object | • | The documented defaults, so a reader can tell a deliberate window from an inherited one without a second call. |
| notSwept | array | • | What this service deliberately keeps forever, and why. Reading a retention surface that lists only what IS deleted is how an operator concludes the audit log ages out — it does not. |
| lastSweep | object | • | The last sweep this replica performed, or NULL when it has observed none. Null and a run with `erased: 0` are DIFFERENT STATES and must never be collapsed: one means the job is working and there was nothing due, the other means this replica has no evidence the job ran at all. The durable per-entry trail is elsewhere — the append-only `waitlist.erased` audit rows. |
| waitlistRetentionDays | integer | • | How long an UNCONVERTED waitlist entry — still pending, or invited and never registered — is kept, counted from SIGN-UP. It lives in the `waitlist` config block rather than in `retention`, because it is not a grace period on a dead row: such an entry never expires on its own, so this window is the whole of what makes it go away — and the sweep ERASES it (audit row, `users/waitlist-entry.erased`) rather than deleting it. |
| waitlistPromisedMaxDays | integer | • | What Lessly PUBLISHED, in its Privacy Policy §6: a waitlist address that never converted is deleted 6 months from sign-up. It rides along so the window above can be read against the promise it came from, rather than as a number with no provenance. |
| waitlistWiderThanPromised | boolean | • | True when this product configured a window WIDER than the published 180 days. Nothing refuses such a value and nothing clamps it — §6 is Lessly’s own policy and a client Product answers to its own, under which a longer window may be perfectly lawful. This flag exists INSTEAD of that ceiling: a deliberate divergence from what Lessly published must be diagnosable in one call rather than found by comparing two numbers nobody thought to compare. |
POST/users/ops/slo
This product's service-level objectives and how it is doing against them RIGHT NOW: sign-in success rate, refresh error rate and mail-send failure rate, each with the objective, the evaluation window, the sample floor below which it is not judged, and the `users/alert.raised` kind a breach fires. Also returns the raw current window (sign-ins, refreshes, detected token reuse, mail sends) and the product's own trailing sign-in failure baseline — the number the per-product anomaly signal compares a spike against. A `status` of `insufficient_data` is NOT health: it means the window is too quiet to judge. The numbers come from ONE replica's in-process window, so treat them as a spot check rather than a fleet-wide error budget.
MCP tool: users_ops_slo_get
Parameters
No parameters.
Response
| Field | Type | Always | Description |
|---|
| slos | array | • | Every published objective and its verdict for this window. |
| scope | string | • | These numbers come from ONE replica’s in-process window, not from the whole deployment. Treat them as a spot check, not as the fleet’s error budget. |
| window | object | • | The current window, exactly as the alert hook sees it. |
GET/users/sessions
List the end-user sessions of one user of this product, newest first — one row per device, with its status, device metadata and both expiries (the sliding inactivity window and the hard time-box). Revoked and expired sessions are included so an operator can see what happened, not only what is live.
MCP tool: users_sessions_list
Parameters
| Name | In | Type | Required | Description |
|---|
| userId | query | string | • | The end user whose sessions to list. |
Response
| Field | Type | Always | Description |
|---|
| sessions | array | • | — |
GET/users/sessions/:sessionId
Read one end-user session in full: its status, device metadata, both expiries, the assurance level and factors it was established with, why it was revoked if it was, and the operator behind it if it is an impersonated session.
MCP tool: users_sessions_get
Parameters
| Name | In | Type | Required | Description |
|---|
| sessionId | path | string | • | The session to read. |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | — |
| ip | string | • | — |
| aal | string | • | `aal2` only if a verified second factor completed. |
| amr | array | • | The factors this session was established with. |
| status | string | • | — |
| userId | string | • | — |
| createdAt | string | • | — |
| expiresAt | string | • | — |
| revokedAt | string | • | — |
| userAgent | string | • | — |
| revokedReason | string | • | — |
| impersonatedBy | string | • | The operator behind an impersonated session, or null for an ordinary one. |
| lastRefreshedAt | string | • | — |
| absoluteExpiresAt | string | • | — |
POST/users/sessions/impersonate
Start an impersonated session as one user of this product — support access, hard-capped. Returns an access token that expires within 30 minutes and CANNOT be refreshed; there is no refresh token. While it is in use, every credential, factor, identifier and session mutation is refused, the user is emailed (unless the product turned that notice off), and the whole thing is recorded in the audit trail against you. Give a real reason: it goes into the audit row and into that email.
MCP tool: users_sessions_impersonate
Parameters
| Name | In | Type | Required | Description |
|---|
| userId | body | string | • | The end user to sign in as. |
| reason | body | string | • | Why. Recorded in the audit trail and shown to the user in the notification email — write what you would want to read in an incident review (a ticket number and one sentence). |
Response
| Field | Type | Always | Description |
|---|
| userId | string | • | — |
| expiresAt | string | • | When the session dies. It cannot be extended or refreshed. |
| expiresIn | integer | • | Seconds until the access token expires. |
| sessionId | string | • | — |
| accessToken | string | • | The access JWT of the impersonated session. There is NO refresh token. |
POST/users/sessions/revoke
Revoke end-user sessions: pass `sessionId` to sign out one device, or `userId` to sign out every device of that user. Revocation is immediate in this toolkit's own checked verification; an access token already issued keeps verifying locally until it expires (≤10 minutes by default). Returns how many sessions this call actually revoked — a second call on the same target returns 0.
MCP tool: users_sessions_revoke
Parameters
| Name | In | Type | Required | Description |
|---|
| sessionId | body | string | — | Revoke exactly this session. |
| userId | body | string | — | Revoke every session of this user. |
Response
| Field | Type | Always | Description |
|---|
| revoked | integer | • | — |
POST/users/stats/get
One snapshot of this product's auth deployment: directory users split by DERIVED status (active, waitlisted, banned, soft-deleted, GDPR-erased), sessions that are live right now, verified second factors, the sign-in success rate over `window`, and an abuse block — lockouts (with the MFA subset), failed sign-ins, detected refresh-token reuse and breached-password sightings. The populations are point-in-time; only the sign-in and abuse numbers honour `window`. A rising abuse block against a falling success rate is what credential stuffing looks like from here. `successRate` is null, not 0, when nothing was attempted.
MCP tool: users_stats_get
Parameters
| Name | In | Type | Required | Description |
|---|
| window | body | string | — | Look-back window for the TIME-BOUNDED numbers — the sign-in counters, the abuse block and `sessions.createdInWindow`: '24h', '7d', '30d' or '90d'. Default '7d'. The user, live-session and factor populations are point-in-time and ignore it. |
Response
| Field | Type | Always | Description |
|---|
| abuse | object | • | Abuse signals over the window. Denied origins are deliberately absent: that check fires on unauthenticated traffic and is served by logs and metrics, not by per-product audit rows. |
| since | string | • | ISO start of that window — the `created_at` floor every counter used. |
| users | object | • | Point-in-time directory populations by DERIVED status — `user` has no status column, these are computed from `waitlisted` and the ban/delete/erase timestamps. |
| window | string | • | The window the time-bounded numbers were computed over. |
| factors | object | • | Enrolled second factors (TOTP and backup-code batches). |
| signIns | object | • | Sign-in outcomes over the window, from the audit log. |
| sessions | object | • | Device sessions. |
GET/users/users
List the end users of this product, newest first. Filter by an identifier/name substring and by status. Soft-deleted users are hidden unless `includeDeleted` is true. Paginate with the opaque `nextCursor`.
MCP tool: users_users_list
Parameters
| Name | In | Type | Required | Description |
|---|
| query | query | string | — | Case-insensitive substring match against the user's identifiers (email) and name. |
| status | query | string | — | Filter by status: 'active' (not banned, not waitlisted), 'banned', or 'waitlisted'. Omit for all. |
| includeDeleted | query | boolean | — | Include soft-deleted users. Default false — deleted users are hidden. |
| includeWaitlisted | query | boolean | — | Include waitlist signups. Default false: a waitlisted row is somebody who ASKED to join and cannot sign in, so it is not part of the directory an operator browses. Use `status: "waitlisted"` to see only them, or `users_waitlist_list` for the funnel. |
| includeTestUsers | query | boolean | — | Include TEST USERS. Default false: a test user is a fixture, not a customer — it is excluded from metering and from this listing for the same reason. Ask for it explicitly when you are debugging the fixtures themselves. |
| limit | query | string | — | Page size, 1-100. Default 25. |
| cursor | query | string | — | Opaque cursor from a previous response's `nextCursor`. Omit for the first page. |
Response
| Field | Type | Always | Description |
|---|
| users | array | • | — |
| nextCursor | string | • | — |
POST/users/users
Create an end user of this product with at least one identifier (email). Optionally store a password HASH — this toolkit never accepts a plaintext password, and nothing verifies the credential until auth flows land in Phase 2. Only one VERIFIED copy of an email may exist per product; unverified duplicates are allowed.
MCP tool: users_users_create
Parameters
| Name | In | Type | Required | Description |
|---|
| name | body | string | — | The user's display name. |
| imageUrl | body | string | — | URL of the user's avatar image. |
| identifiers | body | array | • | The email addresses that reach this user. At least one is required. |
| password | body | object | — | Optional credential to store for this user. Hashes only — this toolkit never accepts a plaintext password. Nothing verifies it until Phase 2. |
| waitlisted | body | boolean | — | Create the user as a waitlist signup: no way in until promoted. Default false. |
| testUser | body | boolean | — | Create this user as a TEST FIXTURE: excluded from metering (it never counts toward retained-users), hidden from the default users_users_list view, never really emailed, and — when the deployment configures one — able to sign in with the fixed magic OTP. REFUSED unless this product has test mode enabled, which a production deployment cannot do. There is no way to set or clear this later. Default false. |
| requiresReset | body | boolean | — | Force a password reset before the user can sign in (use for unverifiable imported hashes). Default false. |
| publicMetadata | body | object | — | Metadata the end user may READ and that may flow into the JWT. Management-plane write only. |
| privateMetadata | body | object | — | Metadata visible to the management plane only. Never exposed to the end user. |
| unsafeMetadata | body | object | — | Metadata the end user may WRITE in a later phase. Never trust it for authorization. |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | — |
| name | string | • | — |
| bannedAt | string | • | — |
| erasedAt | string | • | — |
| imageUrl | string | • | — |
| testUser | boolean | • | — |
| createdAt | string | • | — |
| deletedAt | string | • | — |
| productId | string | • | — |
| updatedAt | string | • | — |
| waitlisted | boolean | • | — |
| identifiers | array | • | — |
| lockedUntil | string | • | — |
| lastSignInAt | string | • | — |
| requiresReset | boolean | • | — |
| publicMetadata | object | • | — |
| unsafeMetadata | object | • | — |
| privateMetadata | object | • | — |
| emailChangeLockedUntil | string | • | — |
DELETE/users/users/:userId
Soft-delete an end user: the record is retained with `deletedAt` set and disappears from the default list. This is NOT GDPR erasure — no data is purged, and the user can still be fetched by id.
MCP tool: users_users_delete
Parameters
| Name | In | Type | Required | Description |
|---|
| userId | path | string | • | The opaque user id returned by users_users_create or _list. |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | — |
| name | string | • | — |
| bannedAt | string | • | — |
| erasedAt | string | • | — |
| imageUrl | string | • | — |
| testUser | boolean | • | — |
| createdAt | string | • | — |
| deletedAt | string | • | — |
| productId | string | • | — |
| updatedAt | string | • | — |
| waitlisted | boolean | • | — |
| identifiers | array | • | — |
| lockedUntil | string | • | — |
| lastSignInAt | string | • | — |
| requiresReset | boolean | • | — |
| publicMetadata | object | • | — |
| unsafeMetadata | object | • | — |
| privateMetadata | object | • | — |
| emailChangeLockedUntil | string | • | — |
GET/users/users/:userId
Fetch one end user of this product by id, including their identifiers and metadata bags. Soft-deleted users are still returned (with `deletedAt` set).
MCP tool: users_users_get
Parameters
| Name | In | Type | Required | Description |
|---|
| userId | path | string | • | The opaque user id returned by users_users_create or _list. |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | — |
| name | string | • | — |
| bannedAt | string | • | — |
| erasedAt | string | • | — |
| imageUrl | string | • | — |
| testUser | boolean | • | — |
| createdAt | string | • | — |
| deletedAt | string | • | — |
| productId | string | • | — |
| updatedAt | string | • | — |
| waitlisted | boolean | • | — |
| identifiers | array | • | — |
| lockedUntil | string | • | — |
| lastSignInAt | string | • | — |
| requiresReset | boolean | • | — |
| publicMetadata | object | • | — |
| unsafeMetadata | object | • | — |
| privateMetadata | object | • | — |
| emailChangeLockedUntil | string | • | — |
PATCH/users/users/:userId
Update an end user's profile, status flags and metadata bags. Only the fields you pass change. A metadata field REPLACES that whole bag — send the full object you want stored. Write matrix in this phase: the management plane writes all three bags (public: end-user readable and may flow into the JWT; private: management plane only; unsafe: end-user writable later and never authoritative for authorization).
MCP tool: users_users_update
Parameters
| Name | In | Type | Required | Description |
|---|
| userId | path | string | • | The user to update. |
| name | body | string | — | The user's display name. Pass null to clear. |
| imageUrl | body | string | — | URL of the user's avatar image. Pass null to clear. |
| waitlisted | body | boolean | — | Whether the user is still a waitlist signup. |
| requiresReset | body | boolean | — | Whether the user must reset their password before signing in. |
| lockedUntil | body | string | — | ISO-8601 timestamp of a temporary, self-healing lockout — the failed-attempt ceiling sets it and it expires on its own. Pass null to unlock the account immediately (audited as `user.unlocked`). |
| publicMetadata | body | object | — | REPLACES the whole public metadata bag. Management-plane write, end-user read. |
| privateMetadata | body | object | — | REPLACES the whole private metadata bag. Management plane only. |
| unsafeMetadata | body | object | — | REPLACES the whole unsafe metadata bag. End-user writable in a later phase; never authoritative. |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | — |
| name | string | • | — |
| bannedAt | string | • | — |
| erasedAt | string | • | — |
| imageUrl | string | • | — |
| testUser | boolean | • | — |
| createdAt | string | • | — |
| deletedAt | string | • | — |
| productId | string | • | — |
| updatedAt | string | • | — |
| waitlisted | boolean | • | — |
| identifiers | array | • | — |
| lockedUntil | string | • | — |
| lastSignInAt | string | • | — |
| requiresReset | boolean | • | — |
| publicMetadata | object | • | — |
| unsafeMetadata | object | • | — |
| privateMetadata | object | • | — |
| emailChangeLockedUntil | string | • | — |
POST/users/users/:userId/ban
Ban an end user by stamping `bannedAt`. NOTE: sessions do not exist yet — until Phase 2 lands auth flows, this is a flag on the record and nothing is revoked or refused by it. Idempotent: banning an already-banned user refreshes the stamp.
MCP tool: users_users_ban
Parameters
| Name | In | Type | Required | Description |
|---|
| userId | path | string | • | The opaque user id returned by users_users_create or _list. |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | — |
| name | string | • | — |
| bannedAt | string | • | — |
| erasedAt | string | • | — |
| imageUrl | string | • | — |
| testUser | boolean | • | — |
| createdAt | string | • | — |
| deletedAt | string | • | — |
| productId | string | • | — |
| updatedAt | string | • | — |
| waitlisted | boolean | • | — |
| identifiers | array | • | — |
| lockedUntil | string | • | — |
| lastSignInAt | string | • | — |
| requiresReset | boolean | • | — |
| publicMetadata | object | • | — |
| unsafeMetadata | object | • | — |
| privateMetadata | object | • | — |
| emailChangeLockedUntil | string | • | — |
POST/users/users/:userId/erase
GDPR hard erasure (Art. 17) of one end user. IRREVERSIBLE and quite different from users_users_delete: the identifier and identity rows are DELETED, the profile fields nulled and all three metadata bags emptied. What survives is a tombstone — the user row with its id and `erasedAt` — so that every foreign reference and webhook consumer holding that id still resolves. The audit trail is KEPT but pseudonymized (the metadata of rows targeting this user is cleared), because a security log that erasure can rewrite is not a security log. The email is freed: registering it again creates a NEW user id. Emits `users/user.erased` so consumers purge their own copies. Idempotent — erasing an already-erased user returns the same tombstone and changes nothing.
MCP tool: users_users_erase
Parameters
| Name | In | Type | Required | Description |
|---|
| userId | path | string | • | The opaque user id returned by users_users_create or _list. |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | — |
| name | string | • | — |
| bannedAt | string | • | — |
| erasedAt | string | • | — |
| imageUrl | string | • | — |
| testUser | boolean | • | — |
| createdAt | string | • | — |
| deletedAt | string | • | — |
| productId | string | • | — |
| updatedAt | string | • | — |
| waitlisted | boolean | • | — |
| identifiers | array | • | — |
| lockedUntil | string | • | — |
| lastSignInAt | string | • | — |
| requiresReset | boolean | • | — |
| publicMetadata | object | • | — |
| unsafeMetadata | object | • | — |
| privateMetadata | object | • | — |
| emailChangeLockedUntil | string | • | — |
GET/users/users/:userId/factors
List the MFA factors an end user has enrolled: `totp` (an authenticator app) and `backup_codes` (single-use recovery codes), each with its status (`unverified` until enrollment was confirmed — an unverified factor gates NOTHING), when it was created and confirmed, and when it was last used. Backup codes also report `remaining`, the number still unspent; a user at 0 can no longer use that factor and should regenerate. NO SECRET IS EVER RETURNED — not the TOTP secret, not a backup code, not a digest of one. A user with no factors returns an empty list, which also means their sign-in completes at `aal1`.
MCP tool: users_factors_list
Parameters
| Name | In | Type | Required | Description |
|---|
| userId | path | string | • | The end user whose enrolled factors to list. |
Response
| Field | Type | Always | Description |
|---|
| factors | array | • | — |
POST/users/users/:userId/factors/reset
OPERATOR RESET of an end user's MFA, for the case the normal recovery cannot serve: the user lost their authenticator AND their backup codes. This does NOT remove anything now. It SCHEDULES the removal behind this product's mandatory delay (config `mfa.resetDelayHours`, default 72) and immediately emails the user's primary address an abort link; if that mailbox is alive and its owner clicks, nothing happens. When the delay elapses, EVERY factor is deleted, every backup code with it, and EVERY SESSION of that user is revoked (a session that reached `aal2` must not outlive the factor that earned it) — after which the user signs in with their first factor alone and enrolls again. Every step is audited (`factor.reset_scheduled` / `_cancelled` / `_applied`) with the `reason` you pass, so do the identity proofing FIRST and write down what it was: this tool is a supervised account takeover, and the trail is what makes it reviewable.
MCP tool: users_factors_reset
Parameters
| Name | In | Type | Required | Description |
|---|
| userId | path | string | • | The end user whose second factor is being reset. |
| reason | body | string | • | What identity proofing was done and under which ticket. Recorded verbatim in the audit row — this is the whole point of the tool being audited. |
Response
| Field | Type | Always | Description |
|---|
| requestId | string | • | — |
| scheduledFor | string | • | — |
POST/users/users/:userId/primary-identifier
OPERATOR OVERRIDE of a user's primary email address, for the case the normal flow cannot serve: the old address is unreachable (dead mailbox, imported user), so the user cannot approve the change themselves. This does NOT swap the address now. It SCHEDULES the swap behind this product's mandatory delay (config `emailChange.overrideDelayHours`, default 72) and immediately emails the OLD address an abort link; if that mailbox is alive and its owner clicks, nothing happens. When the delay elapses the address is swapped, verified, and EVERY SESSION of that user is revoked. Every step is audited (`email_change.override_scheduled` / `_cancelled` / `_applied`) with the `reason` you pass, so do the identity proofing first and write down what it was. Use users_users_update for profile fields; this tool is only for the identity a user signs in with.
MCP tool: users_users_set_primary_identifier
Parameters
| Name | In | Type | Required | Description |
|---|
| userId | path | string | • | The user whose primary email address is being replaced. |
| email | body | string | • | The address to make primary. Must not already belong, verified, to another user of this product. |
| reason | body | string | • | What identity proofing was done and under which ticket. Recorded verbatim in the audit row — this is the whole point of the tool being audited. |
Response
| Field | Type | Always | Description |
|---|
| requestId | string | • | — |
| scheduledFor | string | • | — |
POST/users/users/:userId/unban
Lift a ban by clearing `bannedAt`. See users_users_ban: the flag has no enforcement until Phase 2. Idempotent.
MCP tool: users_users_unban
Parameters
| Name | In | Type | Required | Description |
|---|
| userId | path | string | • | The opaque user id returned by users_users_create or _list. |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | — |
| name | string | • | — |
| bannedAt | string | • | — |
| erasedAt | string | • | — |
| imageUrl | string | • | — |
| testUser | boolean | • | — |
| createdAt | string | • | — |
| deletedAt | string | • | — |
| productId | string | • | — |
| updatedAt | string | • | — |
| waitlisted | boolean | • | — |
| identifiers | array | • | — |
| lockedUntil | string | • | — |
| lastSignInAt | string | • | — |
| requiresReset | boolean | • | — |
| publicMetadata | object | • | — |
| unsafeMetadata | object | • | — |
| privateMetadata | object | • | — |
| emailChangeLockedUntil | string | • | — |
GET/users/users/export
Export the end users of this product as pages of full records (profile, identifiers, metadata bags), using the same filters as users_users_list. Password hashes are NEVER included — neither a PHC hash nor an imported foreign one. Exporting credentials is deferred to a later phase, behind explicit elevated approval; until that gating exists, a hash cannot leave this toolkit. Every call is AUDITED (action `users.export`, with the filter and the row count), so this is not a silent read: use it for migrations, backups and subject-access requests, not as a listing shortcut. Page through with `nextCursor`.
MCP tool: users_users_export
Parameters
| Name | In | Type | Required | Description |
|---|
| query | query | string | — | Case-insensitive substring match against the user's identifiers (email) and name. |
| status | query | string | — | Filter by status: 'active', 'banned' or 'waitlisted'. Omit for all. |
| includeDeleted | query | boolean | — | Include soft-deleted users. Default false. |
| limit | query | string | — | Rows per page, 1-500. Default 100. |
| cursor | query | string | — | Opaque cursor from the previous page's `nextCursor`. Omit for the first page. |
Response
| Field | Type | Always | Description |
|---|
| count | integer | • | — |
| users | array | • | — |
| exportedAt | string | • | — |
| nextCursor | string | • | — |
POST/users/users/import
Bulk-import end users into this product, up to 500 rows per call — batch client-side beyond that. Hashes only: pass `password.phcHash` for an argon2id PHC string, or `password.foreignHash` + `foreignHashAlgo` (bcrypt/argon2/pbkdf2/scrypt) for a hash from the system you are migrating off. A foreign hash is stored INERT — nothing verifies it until sign-in lands in Phase 2, where it is checked once and rehashed — so set `requiresReset: true` on rows whose source hash you do not trust. IDEMPOTENT by canonical identifier: a row whose email already exists VERIFIED in this product is skipped, not duplicated, so re-running the same file is safe. Imported identifiers default to verified (unlike users_users_create) — pass `verified: false` per identifier for addresses the source system never confirmed. Returns a PER-ROW report ({row, status, userId, reason}) plus counts: one malformed row never fails the batch. Emits ONE batch event (`users/user.imported`, with counts) rather than a per-user created event, and writes ONE audit row.
MCP tool: users_users_import
Parameters
| Name | In | Type | Required | Description |
|---|
| users | body | array | • | The users to import, at most 500 per call — batch client-side beyond that. Each row: identifiers (email), optional profile, optional password HASH, optional flags. |
Response
| Field | Type | Always | Description |
|---|
| failed | integer | • | — |
| created | integer | • | — |
| results | array | • | — |
| skipped | integer | • | — |
| requested | integer | • | — |
POST/users/users/invite
Invite somebody to this product by email. Creates the user immediately with an UNVERIFIED identifier and NO credential — they appear in users_users_list straight away but cannot sign in until they accept — then mails a one-time invite link. Accepting the link sets their first password and verifies the address; no session comes from the link itself. IDEMPOTENT by address: re-inviting returns the SAME user (`created: false`), and whether it re-mails is a cooldown decision, so pressing the button twice cannot flood a mailbox (`mailed: false` means the invite is live but no email went out this time). Refuses an address that already belongs to a signed-up user. Requires a verified sender in this product's mail toolkit (config `email.from`); without one nothing is sent and `mailed` is false.
MCP tool: users_users_invite
Parameters
| Name | In | Type | Required | Description |
|---|
| email | body | string | • | The address to invite. Stored canonicalised (lowercase, NFC) plus raw for display. |
| name | body | string | — | The invitee's display name. |
| publicMetadata | body | object | — | Metadata the end user may READ and that may flow into the JWT. |
| privateMetadata | body | object | — | Metadata visible to the management plane only. Never exposed to the end user. |
Response
| Field | Type | Always | Description |
|---|
| mailed | boolean | • | — |
| userId | string | • | — |
| created | boolean | • | — |
| expiresAt | string | • | — |
POST/users/waitlist/erase
GDPR-erase one waitlist signup: the entry with its attribution and consent proof is deleted and the directory row is hard-erased (the id survives as a tombstone, the PII does not). IRREVERSIBLE. Use it for a right-to-erasure request; routine cleanup of stale pending entries happens on its own through the retention sweep.
MCP tool: users_waitlist_erase
Parameters
| Name | In | Type | Required | Description |
|---|
| userId | body | string | • | The waitlisted user whose entry and directory row to erase. |
Response
| Field | Type | Always | Description |
|---|
| erased | boolean | • | — |
POST/users/waitlist/funnel
Count this product's waitlist by funnel stage. `pending` is people waiting, `unconfirmed` is double-opt-in signups whose address never confirmed (they are NEVER invited), `invited` have a live invite, `registered` accepted it and are now full users, `withdrawn` asked to be removed. One consistent snapshot, not five separate reads.
MCP tool: users_waitlist_funnel
Parameters
No parameters.
Response
| Field | Type | Always | Description |
|---|
| total | integer | • | — |
| invited | integer | • | — |
| pending | integer | • | — |
| withdrawn | integer | • | — |
| registered | integer | • | — |
| unconfirmed | integer | • | — |
POST/users/waitlist/import
Import a LEGACY waitlist into this product. Each row becomes a directory user in state `waitlisted` (no credential — a waitlist signup has nothing to sign in with) plus a WaitlistEntry whose consent proof is stored VERBATIM: the policy version, the timestamp and the source the OLD system recorded, never the import time. Rides the Phase 1 bulk import, so it is IDEMPOTENT by canonical identifier — re-running the same file skips instead of duplicating — and reports PER ROW ({row, status, userId, reason}) so one bad row never fails the batch. Addresses that already have an entry are skipped `already_on_list`; addresses on the suppression list (they withdrew) are skipped `suppressed` and are NOT resurrected. SENDS NO EMAIL: an import is a migration, not a signup gesture. Imported addresses land UNVERIFIED, and by default confirmed — pass `confirmed: false` for rows the source system never reached, which then count as `unconfirmed` and are never invited. Up to 500 rows per call.
MCP tool: users_waitlist_import
Parameters
| Name | In | Type | Required | Description |
|---|
| entries | body | array | • | The legacy signups to import, at most 500 per call — batch client-side beyond that. |
Response
| Field | Type | Always | Description |
|---|
| failed | integer | • | — |
| results | array | • | — |
| skipped | integer | • | — |
| imported | integer | • | — |
| requested | integer | • | — |
POST/users/waitlist/invite
Promote waitlisted users by sending them the STANDARD invite email — the same one-time invite link `users_users_invite` sends, so accepting it sets their first password, verifies their address and turns the entry into `registered`. Batched: pass up to 100 user ids. Entries that already registered, that belong to another product, or whose address is on the suppression list (they withdrew) are SKIPPED with a reason rather than mailed. Requires a verified sender in this product's mail toolkit (config `email.from`).
MCP tool: users_waitlist_invite
Parameters
| Name | In | Type | Required | Description |
|---|
| userIds | body | array | • | The waitlisted users to promote, up to 100 per call. Each gets the STANDARD invite email; accepting it is what creates their credential. |
Response
| Field | Type | Always | Description |
|---|
| invited | integer | • | — |
| results | array | • | — |
| skipped | integer | • | — |
POST/users/waitlist/list
List this product's waitlist entries, newest first. Each row is a directory user in state `waitlisted` PLUS the feature-local funnel state: where the signup came from (referrer/UTM), the consent proof recorded for it, and its status. Filter by `status` to work one stage of the funnel, or by `confirmed: false` to find double-opt-in signups whose address never confirmed. Paginate with `cursor`.
MCP tool: users_waitlist_list
Parameters
| Name | In | Type | Required | Description |
|---|
| status | body | string | — | Filter by funnel status: 'pending', 'invited', 'registered' or 'withdrawn'. Omit for all. |
| confirmed | body | boolean | — | Double opt-in only: false returns entries whose address never confirmed. Omit for both. |
| limit | body | integer | — | Entries per page, 1-100. Default 25. |
| cursor | body | string | — | Opaque cursor from the previous page's `nextCursor`. Omit for the first page. |
Response
| Field | Type | Always | Description |
|---|
| entries | array | • | — |
| nextCursor | string | • | — |
GET/users/webhooks
List this product's webhook endpoints, newest first — url, subscribed events, paused state and the visible prefix of the current signing secret. The secret itself is never here.
MCP tool: users_webhooks_list
Parameters
No parameters.
Response
| Field | Type | Always | Description |
|---|
| webhooks | array | • | — |
POST/users/webhooks
Register an outbound webhook endpoint for this product. Deliveries are signed with the Standard Webhooks wire format (webhook-id / webhook-timestamp / webhook-signature). The signing secret is returned EXACTLY ONCE — it is derived, never stored, so a lost secret is rotated with users_webhooks_rotate_secret, never recovered.
MCP tool: users_webhooks_create
Parameters
| Name | In | Type | Required | Description |
|---|
| url | body | string | • | Where deliveries are POSTed. HTTPS only, no embedded credentials. |
| events | body | array | • | Which events to deliver. `user.created`, `user.updated` and `user.deleted` mirror the directory; `session.revoked` fires on sign-out, ban, erase and refresh-token reuse. |
| description | body | string | — | A label only humans read — which system this endpoint feeds. |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | — |
| url | string | • | — |
| active | boolean | • | — |
| events | array | • | — |
| secret | string | • | — |
| createdAt | string | • | — |
| updatedAt | string | • | — |
| description | string | • | — |
| secretPrefix | string | • | — |
DELETE/users/webhooks/:id
Delete a webhook endpoint, its signing secrets and its delivery log. Immediate and final; deliveries already in flight stop.
MCP tool: users_webhooks_delete
Parameters
| Name | In | Type | Required | Description |
|---|
| id | path | string | • | Id of the webhook endpoint. |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | — |
| deleted | boolean | • | — |
GET/users/webhooks/:id
One webhook endpoint. Another product’s endpoint and one that never existed answer identically.
MCP tool: users_webhooks_get
Parameters
| Name | In | Type | Required | Description |
|---|
| id | path | string | • | Id of the webhook endpoint. |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | — |
| url | string | • | — |
| active | boolean | • | — |
| events | array | • | — |
| createdAt | string | • | — |
| updatedAt | string | • | — |
| description | string | • | — |
| secretPrefix | string | • | — |
PATCH/users/webhooks/:id
Change a webhook endpoint: its url, its subscribed events, its description, or `active` to pause and resume delivery without losing the endpoint or its secret.
MCP tool: users_webhooks_update
Parameters
| Name | In | Type | Required | Description |
|---|
| id | path | string | • | Id of the webhook endpoint. |
| url | body | string | — | Where deliveries are POSTed. HTTPS only, no embedded credentials. |
| events | body | array | — | Which events to deliver. `user.created`, `user.updated` and `user.deleted` mirror the directory; `session.revoked` fires on sign-out, ban, erase and refresh-token reuse. |
| description | body | string | — | — |
| active | body | boolean | — | false pauses delivery without losing the endpoint or its secret. |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | — |
| url | string | • | — |
| active | boolean | • | — |
| events | array | • | — |
| createdAt | string | • | — |
| updatedAt | string | • | — |
| description | string | • | — |
| secretPrefix | string | • | — |
GET/users/webhooks/:id/deliveries
This endpoint's recent deliveries, newest first: event type and id, status (pending / succeeded / failed), attempt count, the last response status and the last error. This is where 'my webhook never arrived' is answered.
MCP tool: users_webhooks_deliveries_list
Parameters
| Name | In | Type | Required | Description |
|---|
| id | path | string | • | Id of the webhook endpoint whose deliveries to list. |
| limit | query | integer | — | — |
Response
| Field | Type | Always | Description |
|---|
| deliveries | array | • | — |
POST/users/webhooks/:id/rotate-secret
Mint a NEW signing secret for this endpoint and return it exactly once. The previous secret keeps verifying for 24 hours — both signatures ride the webhook-signature header — so a receiver can swap its copy without dropping a delivery.
MCP tool: users_webhooks_rotate_secret
Parameters
| Name | In | Type | Required | Description |
|---|
| id | path | string | • | Id of the webhook endpoint. |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | — |
| url | string | • | — |
| active | boolean | • | — |
| events | array | • | — |
| secret | string | • | — |
| createdAt | string | • | — |
| updatedAt | string | • | — |
| description | string | • | — |
| secretPrefix | string | • | — |
Was this page helpful?Thanks for your feedback!