ErtisAuth

API Conventions

Routes, headers, pagination, queries and errors.

The conventions on this page apply to every endpoint of the ErtisAuth API.

Base URL and routes#

All examples in this documentation use https://auth.example.com as the base URL. ErtisAuth serves its routes from the root of the host; if you publish it under a path prefix (for example /api/v1 behind a gateway), add that prefix to every route.

Routes fall into three groups:

GroupRoutesMembership given by
Membership-bounded resources/memberships/{membershipId}/users, /roles, /applications, /user-types, /providers, /webhooks, /mailhooks, /events, /code-policies, /codes, /active-tokens, /revoked-tokensthe route
Token endpoints/generate-token, /refresh-token, /verify-token, /revoke-token, /me, /whoami, /verify-otp, /oauth/{slug}/loginthe X-Ertis-Alias header (only where a membership is needed)
Installation-wide/memberships, /setup, /healthcheck, /pingnot bound to a membership

Membership isolation#

A request to a membership-bounded route must be made with a token of that same membership. A valid token of another membership is rejected with 403 AccessDenied, whatever its permissions are.

Headers#

HeaderUsed byDescription
Authorizationalmost all endpointsBearer <access_token> for users, Basic <application_id>:<secret> for applications. The scheme is case-insensitive.
X-Ertis-Aliassign-in endpointsThe membership id. Membership and MembershipId are accepted as alternative header names.
X-IpAddresssign-in endpointsOptional. The end user's IP address, stored with the session (useful when the request comes from your backend).
X-UserAgentsign-in endpointsOptional. The end user's user agent, stored with the session.
X-Hostactivation, password reset, OTPThe base URL of the page that the links in the mails point to (see Account Recovery).
X-Setup-Token/setupThe setup token (see Getting Started).
Content-Typerequests with a bodyapplication/json

Request and response bodies#

  • Bodies are JSON. Field names are snake_case for most resources (email_address, expires_in); providers, mail hooks and the flags of user types use camelCase (defaultRole, mailSubject, isAbstract). Each reference page shows the exact names.
  • Dates are ISO 8601 strings in UTC.
  • Responses are compressed with Brotli or Gzip when the client accepts it.

Status codes#

CodeMeaning in ErtisAuth
200 OKSuccess with a body. Also returned by a partially successful bulk delete (see below).
201 CreatedA resource or a token was created. Tokens are always returned with 201.
204 No ContentSuccess without a body (deletes, sign-out).
400 Bad RequestThe request is malformed or fails validation.
401 UnauthorizedThe token is missing, invalid, expired or revoked, or the credentials are wrong. Comes with a WWW-Authenticate header.
403 ForbiddenThe caller is authenticated but not allowed: missing permission, other membership, or a disabled provider.
404 Not FoundThe resource does not exist in this membership.
409 ConflictA duplicate (same slug, same username…), an update without changes, a resource still in use, or a setup already done.
500 Internal Server ErrorAn unexpected error. The details are only written to the server log.
501 Not ImplementedA feature the request needs is not configured, e.g. no mail provider or no activation mail hook.
503 Service UnavailableAn external provider (Google, Apple…) could not be reached.

Errors#

Errors have a common shape:

json
{
	"message": "User not found in db by given _id: <66f1c0d2a4b5c6d7e8f90124>",
	"errorCode": "UserNotFound",
	"statusCode": 404
}

Use errorCode in your code; message is meant for people and may change. All codes are listed in Error Codes.

Validation errors carry the list of problems in data:

json
{
	"message": "Some fields are not validated, invalid or missing. Check response detail.",
	"errorCode": "ModelValidationError",
	"statusCode": 400,
	"data": [ "Expires-in is required", "Secret key is required" ]
}

Errors of a user's custom fields, validated by the user type schema, name the field:

json
{
	"message": "String length can not be greater than 20",
	"fieldName": "phone",
	"fieldPath": "phone",
	"errorCode": "FieldValidationException",
	"statusCode": 400
}

When several fields are invalid at once, all of them are reported:

json
{
	"message": "…",
	"errorCode": "ValidationException",
	"statusCode": 400,
	"errors": [
		{ "message": "phone is required", "fieldName": "phone", "fieldPath": "phone" },
		{ "message": "String length can not be less than 2", "fieldName": "firstname", "fieldPath": "firstname" }
	]
}

An id that is not a valid ObjectId answers 400 ParameterFormatError.

Listing resources#

List endpoints (GET /memberships/{membershipId}/users and the like) are paginated and sortable with query parameters:

ParameterExampleDescription
skipskip=20Number of items to skip. Must not be negative.
limitlimit=10Maximum number of items to return. Must not be negative.
with_countwith_count=trueAlso return the total number of matching items in count.
sortsort=username or sort=sys.created_at descField to sort by, optionally followed by asc (default) or desc.
shell
curl 'https://auth.example.com/memberships/<membership_id>/users?skip=0&limit=2&with_count=true&sort=sys.created_at%20desc' \
	-H 'Authorization: Bearer <access_token>'
json
{
	"count": 1250,
	"items": [
		{ "_id": "…", "username": "jane", "…": "…" },
		{ "_id": "…", "username": "john", "…": "…" }
	]
}

Without with_count=true, count is not computed.

Querying resources#

Most resources have a POST …/_query endpoint that takes a MongoDB query in where, and an optional projection in select:

shell
curl -X POST 'https://auth.example.com/memberships/<membership_id>/users/_query?limit=50&sort=lastname' \
	-H 'Authorization: Bearer <access_token>' \
	-H 'Content-Type: application/json' \
	-d '{
		"where": {
			"user_type": "customer",
			"is_active": true,
			"sys.created_at": { "$gte": "2026-01-01T00:00:00Z" }
		},
		"select": {
			"username": 1,
			"email_address": 1,
			"firstname": 1
		}
	}'
  • The pagination and sorting parameters of the list endpoints apply.
  • select includes fields with 1 or true and excludes them with 0 or false.
  • The membership filter is always added by the server: a query can never read another membership's data.
  • JavaScript operators ($where, $function, $accumulator) are rejected with 400 InvalidQuery.
  • Hidden fields, such as password_hash, can be neither returned nor used in filters or sorting.
  • For users, the optional locale query parameter (e.g. locale=tr) sets the collation of the sorting, so that names sort correctly in that language.

Aggregation#

Active tokens also support POST …/active-tokens/_aggregate, which takes a JSON array of aggregation pipeline stages. The membership filter is added as the first stage by the server.

Only stages that transform the documents flowing through the pipeline are allowed: $match, $project, $addFields, $set, $unset, $group, $sort, $limit, $skip, $count, $unwind, $bucket, $bucketAuto, $sortByCount, $replaceRoot, $replaceWith, $sample, $setWindowFields and $facet. Any other stage, in particular those that read or write other collections ($lookup, $graphLookup, $unionWith, $out, $merge), is rejected with 400 UnsupportedAggregationStage.

shell
curl -X POST 'https://auth.example.com/memberships/<membership_id>/active-tokens/_aggregate' \
	-H 'Authorization: Bearer <access_token>' \
	-H 'Content-Type: application/json' \
	-d '[
		{ "$group": { "_id": "$user_id", "sessions": { "$sum": 1 } } },
		{ "$sort": { "sessions": -1 } },
		{ "$limit": 10 }
	]'

Searching resources#

Users, roles, applications and memberships have a full-text search:

shell
curl 'https://auth.example.com/memberships/<membership_id>/users/search?keyword=lovelace&limit=10' \
	-H 'Authorization: Bearer <access_token>'

keyword is required (400 SearchKeywordRequired otherwise). Pagination and sorting work as in the list endpoints.

Creating and updating#

  • POST creates a resource and answers 201 Created with the resource and a Location header.
  • PUT /{id} updates a resource. The id always comes from the route; an _id in the body is ignored.
  • An update that changes nothing answers 409 IdenticalDocumentError. Treat it as a success if your client may send unchanged data.
  • A sys object sent in a body is ignored.

Bulk delete#

Users, roles, applications, webhooks, mail hooks and code policies can be deleted in bulk with DELETE on the collection route and a JSON array of ids as the body:

shell
curl -X DELETE 'https://auth.example.com/memberships/<membership_id>/users' \
	-H 'Authorization: Bearer <access_token>' \
	-H 'Content-Type: application/json' \
	-d '[ "66f1c0d2a4b5c6d7e8f90124", "66f1c0d2a4b5c6d7e8f90127" ]'
ResultResponse
All deleted204 No Content
None deleted404 BulkDeleteFailed
Some deleted200 OK with the error body BulkDeletePartial
Note: a partial bulk delete answers 200 with an error body. Check errorCode before you treat a 200 as a full success.

Found a mistake in the docs? Open an issue