ErtisAuth

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#

json
{
	"_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.

FieldDescription
usernameRequired, unique in the membership. Can be used to sign in.
email_addressRequired, a valid email address, unique in the membership. Can be used to sign in.
firstnameRequired.
lastnameOptional.
roleRequired. The slug of an existing role.
user_typeRequired. The slug (or name) of a non-abstract user type; always stored as the slug.
permissions, forbiddenOptional UBAC entries (resource.action.object). See Authorization.
is_activeWhether the user can sign in. Set by the server on creation (see Activation).
source_providerWhere the user came from: ErtisAuth, or the type of the provider they signed up with. Read-only.
connected_accountsThe external provider accounts linked to the user. Read-only.
membership_idRead-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}.

MethodRouteDescriptionPermission
GET/users/{id}Get a userusers.read.{id}
GET/usersList usersusers.read
POST/users/_queryQuery usersusers.read
GET/users/search?keyword=Search usersusers.read
POST/usersCreate a userusers.create
PUT/users/{id}Update a userusers.update.{id}
DELETE/users/{id}Delete a userusers.delete.{id}
DELETE/usersDelete several usersusers.delete
PUT/users/{id}/change-passwordChange a passwordusers.update.{id}
GET/users/check-password?password=Check the caller's own passwordusers.read
GET/users/{id}/activateActivate a userusers.update.{id}
GET/users/{id}/freezeFreeze a userusers.update.{id}
GET/users/activation?uat=Activate with an activation tokenusers.update
POST/users/resend-activation-mailSend the activation mail againusers.create
POST/users/reset-passwordStart a password resetusers.update
GET/users/verify-reset-token?token=Check a reset tokenusers.read
POST/users/set-passwordSet a new password with a reset tokenusers.update
GET/users/{id}/generate-otpGenerate a one-time passwordotp.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#

http
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#

http
GET /memberships/{membershipId}/users?skip=0&limit=20&with_count=true&sort=lastname
POST /memberships/{membershipId}/users/_query
GET /memberships/{membershipId}/users/search?keyword=ada

See 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:

shell
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#

shell
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.
  • password is 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_provider and connected_accounts in the body are ignored: when the membership requires activation, the user is created inactive and the activation mail is sent to the link host in X-Host; otherwise the user is active immediately.
  • user_type is required in practice: without it the built-in base-user type would be used, which is abstract (400 InheritedTypeIsAbstract).
  • email_address is stored in lower case.

Response 201 Created: the user.

A duplicate username, email address or other unique field is reported as a field error:

json
{
	"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"
		}
	]
}
ErrorWhen
400 PasswordRequired, 400 PasswordMinLengthRuleErrorMissing or too short password
400 RoleRequiredrole is missing
400 FieldValidationException, 400 ValidationExceptionA field does not match the user type schema
400 ValidationExceptionThe username, the email address or another unique field is already used by another user (see above)
404 RoleNotFound, 404 UserTypeNotFoundUnknown role or user type
409 UbacsConflictedThe same entry is in permissions and forbidden
501 NotDefinedAnyMailProvider, 501 ActivationMailHookWasNotDefinedActivation is required but no mail can be sent

Update a user#

http
PUT /memberships/{membershipId}/users/{id}

Updates are partial: the fields you send are merged into the current user, the others keep their values.

shell
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_active or user_type requires a real users.update permission on the user, also when users update themselves (see Authorization).
  • user_type can't be changed once the user is created: sending another type answers 400 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#

http
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#

http
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:

http
GET /memberships/{membershipId}/users/check-password?password=<password>
Authorization: Bearer <access_token>

Answers 200 OK when it matches and 401 when it does not.

Note: the password is sent in the query string, which proxies and servers may write to their access logs. Make sure your infrastructure doesn't log query strings for this route.

Activate a user#

http
GET /memberships/{membershipId}/users/{id}/activate

Activates 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#

http
GET /memberships/{membershipId}/users/{id}/freeze

Deactivates 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.

Note: activate and freeze are GET requests that change data. Don't expose them as plain links that browsers or crawlers might prefetch.

Events#

EventWhen
UserCreatedA user was created (also by a provider sign-up)
UserUpdatedA user was updated, activated or frozen
UserDeletedA user was deleted
UserPasswordChangedA password was changed or set
UserPasswordResetA password reset was started

See Events.

Found a mistake in the docs? Open an issue