Users
Create, query, update and manage users.
Users are the people who sign in to your applications. Each user belongs to one membership, has a role and a user type, and may have permissions of their own (see Authorization).
The user object#
{
"_id": "66f1c0d2a4b5c6d7e8f90127",
"username": "ada",
"email_address": "ada@example.com",
"firstname": "Ada",
"lastname": "Lovelace",
"role": "support",
"user_type": "employee",
"permissions": [ "orders.approve" ],
"forbidden": [],
"is_active": true,
"source_provider": "ErtisAuth",
"connected_accounts": [],
"membership_id": "66f1c0d2a4b5c6d7e8f90123",
"sys": {
"created_at": "2026-01-01T12:00:00Z",
"created_by": "admin",
"modified_at": "2026-01-05T09:12:44Z",
"modified_by": "ada"
},
"department": "Engineering",
"phone": "+44 20 7946 0000"
}The standard fields come from the built-in base-user type; every other field (department, phone above) is declared by the user's user type.
| Field | Description |
|---|---|
username | Required, unique in the membership. Can be used to sign in. |
email_address | Required, a valid email address, unique in the membership. Can be used to sign in. |
firstname | Required. |
lastname | Optional. |
role | Required. The slug of an existing role. |
user_type | Required. The slug (or name) of a non-abstract user type; always stored as the slug. |
permissions, forbidden | Optional UBAC entries (resource.action.object). See Authorization. |
is_active | Whether the user can sign in. Set by the server on creation (see Activation). |
source_provider | Where the user came from: ErtisAuth, or the type of the provider they signed up with. Read-only. |
connected_accounts | The external provider accounts linked to the user. Read-only. |
membership_id | Read-only. |
The password hash is stored in a hidden field and is never returned, filtered or sorted on.
Endpoints#
All routes are under /memberships/{membershipId}.
| Method | Route | Description | Permission |
|---|---|---|---|
GET | /users/{id} | Get a user | users.read.{id} |
GET | /users | List users | users.read |
POST | /users/_query | Query users | users.read |
GET | /users/search?keyword= | Search users | users.read |
POST | /users | Create a user | users.create |
PUT | /users/{id} | Update a user | users.update.{id} |
DELETE | /users/{id} | Delete a user | users.delete.{id} |
DELETE | /users | Delete several users | users.delete |
PUT | /users/{id}/change-password | Change a password | users.update.{id} |
GET | /users/check-password?password= | Check the caller's own password | users.read |
GET | /users/{id}/activate | Activate a user | users.update.{id} |
GET | /users/{id}/freeze | Freeze a user | users.update.{id} |
GET | /users/activation?uat= | Activate with an activation token | users.update |
POST | /users/resend-activation-mail | Send the activation mail again | users.create |
POST | /users/reset-password | Start a password reset | users.update |
GET | /users/verify-reset-token?token= | Check a reset token | users.read |
POST | /users/set-password | Set a new password with a reset token | users.update |
GET | /users/{id}/generate-otp | Generate a one-time password | otp.create.{id} |
The activation, reset and OTP endpoints are described in Account Recovery and Activation.
A user can always update their own record (the own-record rule), except for the privileged fields.
Get a user#
GET /memberships/{membershipId}/users/{id}
Authorization: Bearer <access_token>Response 200 OK: the user, with the custom fields of their user type. 404 UserNotFound when it does not exist.
List, query and search users#
GET /memberships/{membershipId}/users?skip=0&limit=20&with_count=true&sort=lastname
POST /memberships/{membershipId}/users/_query
GET /memberships/{membershipId}/users/search?keyword=adaSee API Conventions. The query endpoint also accepts locale (e.g. locale=tr) to sort names by the rules of a language. The search looks for the keyword in username, firstname, lastname and email_address, ignoring case and diacritics.
Find a user by email address:
curl -X POST 'https://auth.example.com/memberships/<membership_id>/users/_query' \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{ "where": { "email_address": "ada@example.com" } }'Create a user#
curl -X POST https://auth.example.com/memberships/<membership_id>/users \
-H 'Authorization: Bearer <access_token>' \
-H 'X-Host: https://app.example.com/activate' \
-H 'Content-Type: application/json' \
-d '{
"username": "ada",
"email_address": "ada@example.com",
"firstname": "Ada",
"lastname": "Lovelace",
"password": "<at least 6 characters>",
"role": "support",
"user_type": "employee",
"department": "Engineering"
}'- The body is validated against the schema of
user_type, including its inherited fields. Unknown fields are rejected unless the user type allows additional properties. passwordis required and must be at least 6 characters long. It is hashed with the membership's algorithm and never stored in plain text.is_active,source_providerandconnected_accountsin the body are ignored: when the membership requires activation, the user is created inactive and the activation mail is sent to the link host inX-Host; otherwise the user is active immediately.user_typeis required in practice: without it the built-inbase-usertype would be used, which is abstract (400 InheritedTypeIsAbstract).email_addressis stored in lower case.
Response 201 Created: the user.
A duplicate username, email address or other unique field is reported as a field error:
{
"message": "…",
"errorCode": "ValidationException",
"statusCode": 400,
"errors": [
{
"message": "The 'email_address' field has unique constraint. The same value is already using in another user.",
"fieldName": "email_address",
"fieldPath": "email_address"
}
]
}| Error | When |
|---|---|
400 PasswordRequired, 400 PasswordMinLengthRuleError | Missing or too short password |
400 RoleRequired | role is missing |
400 FieldValidationException, 400 ValidationException | A field does not match the user type schema |
400 ValidationException | The username, the email address or another unique field is already used by another user (see above) |
404 RoleNotFound, 404 UserTypeNotFound | Unknown role or user type |
409 UbacsConflicted | The same entry is in permissions and forbidden |
501 NotDefinedAnyMailProvider, 501 ActivationMailHookWasNotDefined | Activation is required but no mail can be sent |
Update a user#
PUT /memberships/{membershipId}/users/{id}Updates are partial: the fields you send are merged into the current user, the others keep their values.
curl -X PUT https://auth.example.com/memberships/<membership_id>/users/<user_id> \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '{ "lastname": "King", "department": "Research" }'- The merged user is validated against the schema of its user type.
- The password can't be changed here; use change password.
- Changing
role,permissions,forbidden,is_activeoruser_typerequires a realusers.updatepermission on the user, also when users update themselves (see Authorization). user_typecan't be changed once the user is created: sending another type answers400 UserTypeImmutable. To move a user to another type, create a new user.
Response 200 OK: the updated user. An update without any change answers 409 IdenticalDocumentError.
Delete a user#
DELETE /memberships/{membershipId}/users/{id}Response 204 No Content, or 404 UserNotFound. Several users can be deleted at once with bulk delete.
Change a password#
PUT /memberships/{membershipId}/users/{id}/change-password
Content-Type: application/json
{ "password": "<new password>" }Response 200 OK.
After a password change the user is signed out on every device: all their tokens are revoked. When users change their own password, the session they used to change it stays signed in. This protects the account after a takeover: changing the password also kicks the attacker out.
A user can change their own password with the own-record rule; changing someone else's password requires users.update on them.
Check the caller's password#
Asks whether a password is the caller's own current password, for example before a sensitive operation:
GET /memberships/{membershipId}/users/check-password?password=<password>
Authorization: Bearer <access_token>Answers 200 OK when it matches and 401 when it does not.
Activate a user#
GET /memberships/{membershipId}/users/{id}/activateActivates the user without an activation mail, for example by an administrator. Response 200 OK with the user, 400 UserAlreadyActive if already active.
Freeze a user#
GET /memberships/{membershipId}/users/{id}/freezeDeactivates the user and revokes all their tokens: they are signed out everywhere and can't sign in again until they are activated. Pending activation links stop working too. Response 200 OK with the user, 400 UserAlreadyInactive if already inactive.
GET requests that change data. Don't expose them as plain links that browsers or crawlers might prefetch.Events#
| Event | When |
|---|---|
UserCreated | A user was created (also by a provider sign-up) |
UserUpdated | A user was updated, activated or frozen |
UserDeleted | A user was deleted |
UserPasswordChanged | A password was changed or set |
UserPasswordReset | A password reset was started |
See Events.
Found a mistake in the docs? Open an issue