Users API
These endpoints create and manage users. They are how a deployment is administered: by automation such as CI, a marketplace image, or an infrastructure-as-code run, or by a separate user interface, such as Philter UI (in development).
Requests authenticate with API keys. Create and manage a user's keys with the API Keys API. A user can also have a password, for a person who signs in through a user interface. A user without a password can only use API keys. No endpoint returns a password or its hash. See Passwords.
Every endpoint requires an administrator in addition to its scope, except GET /api/users/me, which any key holding users:read can call, and PUT /api/users/me/password and the /api/users/me/mfa endpoints, which any key holding users:write can call. A request that lacks the scope and a request from a non-administrator are both refused with 403 Forbidden; the message says which.
There is no setting that turns these endpoints off. A headless deployment is administered through them, so an administrator's API key, or an administrator's session key from sign-in, is the credential that manages it, and they are guarded like every other administrator endpoint: by the administrator role and a scope. Limit which keys hold users:write and api-keys:write.
The first administrator key comes from PHILTER_BOOTSTRAP_API_KEY. These endpoints are how that key provisions the rest.
The user object
{
"id": "6a0f1c2e9b1d4e3f2a1b0c9d",
"username": "ci",
"email": "ci@example.com",
"role": "user",
"active": true,
"created": "2026-10-05T14:03:11.000Z",
"deactivatedAt": null,
"passwordSet": false,
"passwordChangeRequired": false,
"mfaEnabled": false,
"mfaLocked": false
}
id- The user's id. Audit events name their actor by this id, asapiKeyId, alongside itsusername.role-useroradmin.active-falseonce the user is deactivated. A deactivated user's API keys are rejected.deactivatedAt- When the user was deactivated, ornull.passwordSet- Whether the user has a password.passwordChangeRequired- Whether the user must change the password at next sign-in, because an administrator set it.mfaEnabled- Whether the user is enrolled in MFA. The secret is never returned.mfaLocked- Whether the user's MFA is locked after repeated bad codes, until an administrator unlocks it.
List users
GET /api/users?offset=0&limit=25
Returns users sorted by username, including deactivated users, and the total number that match. Requires users:read and an administrator. limit is capped at 100.
q(optional) - Only users whose username containsq, ignoring case.role(optional) - Only users with this role,useroradmin.active(optional) -truelists only active users,falseonly deactivated ones. Left out, both.sort(optional, defaultusername) -usernameorcreated. Withorder,ascordesc; see Listings.
{
"users": [ { "id": "6a0f1c2e9b1d4e3f2a1b0c9d", "username": "ci", "email": "ci@example.com", "role": "user", "active": true, "created": "2026-10-05T14:03:11.000Z", "deactivatedAt": null, "passwordSet": false, "passwordChangeRequired": false, "mfaEnabled": false, "mfaLocked": false } ],
"total": 1
}
Get the calling key's user
GET /api/users/me
Returns the user that owns the API key making the request. Requires users:read; does not require an administrator. me is reserved and cannot be used as a username.
The user object has two more fields here, about the deployment, which a user who is not an administrator cannot read from the Settings API:
mfaAvailable- Whether users may enroll in MFA.mfaRequired- Whether every user who signs in must enroll. NevertruewhilemfaAvailableisfalse.
Get a user
GET /api/users/{username}
Returns one user, active or deactivated. Requires users:read and an administrator. Returns 404 Not Found if there is no such user.
Create a user
POST /api/users
Creates a user with a default policy and a default context. Requires users:write and an administrator.
{
"username": "ci",
"email": "ci@example.com",
"role": "user"
}
username(required) - Must not already belong to a user, including a deactivated one holding the name in reserve, and must not beme. Because the username is how the user is addressed in a request path, it cannot contain/,\,;,%, or control characters, and cannot be.or... Other text, including an email address, is allowed.email(optional) - The user's email address, under the email rules. Change it later withPUT /api/users/{username}/email.role(optional) -user(the default) oradmin.password(optional) - See password rules. The user must change it at next sign-in, because an administrator chose it. Without one, the user can only use API keys.
201 Created:
{
"id": "6a0f1c2e9b1d4e3f2a1b0c9d",
"username": "ci",
"role": "user"
}
| Status | Meaning |
|---|---|
| 400 | The username is missing, is me, or cannot be used in a request path, the role is not user or admin, the password breaks the password rules, or the email address is not valid. |
| 409 | A user with that username already exists, active or deactivated. |
curl -k "https://localhost:8080/api/users" \
-H "Authorization: Bearer sk_abcdefghijklmnopqrstuvwxyz012345" \
-H "Content-Type: application/json" \
--data '{"username":"ci","role":"user"}'
Set a user's role
PUT /api/users/{username}/role
{
"role": "admin"
}
Sets the role to user or admin and returns the user. Requires users:write and an administrator.
| Status | Meaning |
|---|---|
| 400 | The role is missing or is not user or admin. |
| 404 | There is no user with that username. |
| 409 | The user is the last active administrator. Make another user an administrator first. This holds when several administrators are demoted or deactivated at once: at least one always remains. |
| 409 | Another change to the administrators is in progress; nothing was changed, and reason is admin_change_in_progress. Retry-After gives the seconds to wait. MongoDB storage only. |
Set a user's email address
PUT /api/users/{username}/email
{
"email": "jordan@example.com"
}
Sets the user's email address and returns the user. Send null or an empty email to remove it. Requires users:write and an administrator.
An address must have one @, something on each side of it, a dot in the domain, and no whitespace, and be at most 254 characters. It is trimmed before it is stored. The check catches typos; it does not confirm the address receives mail. The same rule applies when creating a user.
The email address is not how a user is addressed or signs in, so changing it changes nothing else about the user.
curl -k -X PUT "https://localhost:8080/api/users/ci/email" \
-H "Authorization: Bearer <admin key>" \
-H "Content-Type: application/json" \
--data '{"email":"jordan@example.com"}'
| Status | Meaning |
|---|---|
| 400 | The email address is not valid or is too long. |
| 404 | There is no user with that username. |
Deactivate a user
POST /api/users/{username}/deactivate
Deactivates the user and returns it. The user's API keys stop working at once. The user and all of its data (API keys, policies, contexts, lists, and redaction ledger) are retained, so the user can be reactivated. While cross-user access is enabled, an administrator can still read and manage that data by naming the user as owner, for example to release a legal hold, without reactivating the user. Requires users:write and an administrator.
| Status | Meaning |
|---|---|
| 404 | There is no user with that username. |
| 409 | The user is already deactivated, is the calling key's own user, or is the last active administrator. |
| 409 | Another change to the administrators is in progress; nothing was changed, and reason is admin_change_in_progress. Retry-After gives the seconds to wait. MongoDB storage only. |
Reactivate a user
POST /api/users/{username}/reactivate
Reactivates a deactivated user, restoring its API keys, and returns the user. Requires users:write and an administrator.
| Status | Meaning |
|---|---|
| 404 | There is no user with that username. |
| 409 | The user is already active. |
Passwords
Password rules
A password must be at least 16 characters and at most 72 bytes in UTF-8, the most bcrypt reads. A password outside those limits is refused with 400 Bad Request. Passwords are stored as bcrypt hashes.
Setting, changing, or resetting a password revokes the user's session keys, the keys issued when a person signs in. Long-lived API keys are not affected.
Change your own password
PUT /api/users/me/password
{
"currentPassword": "the-current-password",
"newPassword": "a-new-password-of-16-or-more"
}
Changes the password of the calling key's own user. The current password is required, and the new one must differ from it. Clears a required change. Requires users:write; does not require an administrator. The calling key is revoked too if it is a session key.
| Status | Meaning |
|---|---|
| 204 | The password was changed. |
| 400 | A field is missing, the new password breaks the password rules, or it is the same as the current one. |
| 403 | The current password is not correct, or the key does not hold users:write. |
| 409 | The user has no password, so an administrator sets the first one, or another request changed the password at the same time. |
| 429 | The user is locked after repeated wrong passwords. Retry-After gives the seconds to wait, and reason is locked. |
A wrong current password counts toward the user's sign-in lockout, together with failed sign-ins, so a key, such as a stolen session key, cannot be used to guess the password. When the count reaches SIGN_IN_MAX_FAILURES, both sign-in and this request are refused for SIGN_IN_LOCKOUT_MINUTES, even with the right password.
Set or reset a user's password
PUT /api/users/{username}/password
{
"password": "a-password-of-16-or-more"
}
Sets another user's password without the current one, and marks it as one the user must change at next sign-in. Requires users:write and an administrator.
On the calling administrator's own user, it sets only the first password, with no change required. This is how the admin user gets a password: call it with the bootstrap API key. Once the user has a password, change it with PUT /api/users/me/password, which requires the current one, so a stolen key cannot replace its own user's password unnoticed.
curl -k -X PUT "https://localhost:8080/api/users/admin/password" \
-H "Authorization: Bearer $PHILTER_BOOTSTRAP_API_KEY" \
-H "Content-Type: application/json" \
--data '{"password":"a-password-of-16-or-more"}'
| Status | Meaning |
|---|---|
| 204 | The password was set. |
| 400 | The password is missing or breaks the password rules. |
| 404 | There is no user with that username. |
| 409 | The user is the caller and already has a password, or another request changed the password at the same time. |
Clear a user's sign-in lock
POST /api/users/{username}/sign-in/unlock
Clears a user's sign-in lock before SIGN_IN_LOCKOUT_MINUTES ends, resets the count of failed sign-ins, and returns 204 No Content. The lock returns if sign-in keeps failing, so if someone is locking a user out deliberately, restrict who can reach sign-in; see Network placement. Requires users:write and an administrator.
| Status | Meaning |
|---|---|
| 404 | There is no user with that username. |
| 409 | The user's sign-in is not locked. |
Multi-factor authentication
Users who sign in with a password can add TOTP multi-factor authentication, with an authenticator app such as Google Authenticator, Authy, or 1Password. Once enrolled, sign-in asks for a code from the app as well as the password. MFA is available only when an administrator turns on mfaAvailable in the settings; mfaRequired makes every user who signs in enroll. A user who is already enrolled is always asked for a code, even if mfaAvailable is turned off later.
The secret is encrypted at rest with PHILTER_ENCRYPTION_KEY and returned only when enrollment starts. A code is six digits and changes every 30 seconds; the codes for the 30 seconds either side of now are also accepted, to allow for clock differences. Each code is accepted once. After five consecutive bad codes the user is locked: no code is accepted until an administrator unlocks them or removes the enrollment. A good code resets the count.
Start enrollment
POST /api/users/me/mfa
Generates a secret for the calling key's own user. Takes the user's current password, so a stolen key cannot enroll an authenticator of its own. A wrong password counts toward the sign-in lockout. Requires users:write; does not require an administrator.
{
"password": "the user's current password"
}
200 OK:
{
"secret": "JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP",
"otpauthUri": "otpauth://totp/Philter:jordan?secret=JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP&issuer=Philter&algorithm=SHA1&digits=6&period=30"
}
Show otpauthUri as a QR code for the app to scan, or have the person type in secret. Neither is returned again. Enrollment does not apply until it is confirmed; starting again replaces an unconfirmed secret.
| Status | Meaning |
|---|---|
| 400 | The body or password is missing. |
| 403 | The password is not correct; reason is wrong_password. |
| 409 | MFA is not available on this deployment, the user is already enrolled, or the user has no password. |
| 429 | The user's sign-in is locked after repeated failures; reason is locked. |
Confirm enrollment
POST /api/users/me/mfa/confirm
{
"code": "123456"
}
Completes enrollment with a code from the app, and returns 204 No Content. Revokes the user's session keys, so the person signs in again, this time with a code. Requires users:write.
| Status | Meaning |
|---|---|
| 400 | The code is missing or not valid. |
| 409 | MFA is not available, the user is already enrolled, or no enrollment was started. |
Remove your own enrollment
POST /api/users/me/mfa/remove
{
"code": "123456"
}
Removes the calling key's own user's MFA and returns 204 No Content. It takes a valid code, so a stolen key cannot turn MFA off, and a bad code counts toward the lock. A person who has lost their device asks an administrator. Requires users:write.
| Status | Meaning |
|---|---|
| 400 | The code is missing. |
| 403 | The code is not valid, or the user's MFA is locked. |
| 409 | The user is not enrolled. |
Remove a user's enrollment
DELETE /api/users/{username}/mfa
Removes another user's MFA, for a person who has lost their device, clears any lock, and returns 204 No Content. An administrator removes their own with POST /api/users/me/mfa/remove. Requires users:write and an administrator.
| Status | Meaning |
|---|---|
| 404 | There is no user with that username. |
| 409 | The user is the caller, or is not enrolled. |
Unlock a user
POST /api/users/{username}/mfa/unlock
Unlocks a user locked after five bad codes, resets the count, and returns 204 No Content. Requires users:write and an administrator.
| Status | Meaning |
|---|---|
| 404 | There is no user with that username. |
| 409 | The user is not locked. |
Errors common to every endpoint
| Status | Meaning |
|---|---|
| 401 | The Authorization header is absent or the API key is not recognized. |
| 403 | The key does not hold the endpoint's scope, or the caller is not an administrator. The message says which. |
Auditing
| Event | Recorded when |
|---|---|
user_created |
A user was created. The principal is the calling administrator, the associated object is the new user, and the details name the calling API key. |
user_role_changed |
A role was set. The principal is the calling administrator, the associated object is the user, and the details name the calling API key. |
user_email_changed |
An email address was set, changed, or removed. The principal is the calling administrator, the associated object is the user, and the details say whether the address was changed or removed and name the calling API key. The address itself is not recorded. |
user_deactivated |
A user was deactivated. The principal is the calling administrator, the associated object is the user, and the details name the calling API key. |
user_reactivated |
A user was reactivated. The principal is the calling administrator, the associated object is the user, and the details name the calling API key. |
user_password_set |
A user without a password was given one, at creation or with PUT /api/users/{username}/password. The principal is the calling administrator, the associated object is the user, and the details name the calling API key and whether a change is required. |
user_password_reset |
An administrator replaced a user's password. Recorded like user_password_set. |
user_password_changed |
A user changed their own password. The principal and associated object are the user, and the details name the calling API key. |
user_mfa_enrolled |
A user confirmed MFA enrollment. The principal and associated object are the user, and the details name the calling API key. |
user_mfa_removed |
A user's MFA was removed, by the user with a code or by an administrator. The principal is whoever removed it, and the details name the calling API key. |
user_mfa_locked |
A user's MFA was locked after five consecutive bad codes. Recorded once per lock. |
user_mfa_unlocked |
An administrator unlocked a user's MFA. The principal is the administrator, and the details name the calling API key. |
sign_in_unlocked |
An administrator cleared a user's sign-in lock. The principal is the administrator, the associated object is the user, and the details name the calling API key. |
api_key_deleted |
A session key was revoked because the password was set, changed, or reset, or MFA was enrolled. The details give the reason. |
No event records a password, its hash, or an MFA secret or code. These are security events, so they cannot be switched off. They are readable through GET /api/audit. See Auditing.