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:
| Group | Routes | Membership given by |
|---|---|---|
| Membership-bounded resources | /memberships/{membershipId}/users, /roles, /applications, /user-types, /providers, /webhooks, /mailhooks, /events, /code-policies, /codes, /active-tokens, /revoked-tokens | the route |
| Token endpoints | /generate-token, /refresh-token, /verify-token, /revoke-token, /me, /whoami, /verify-otp, /oauth/{slug}/login | the X-Ertis-Alias header (only where a membership is needed) |
| Installation-wide | /memberships, /setup, /healthcheck, /ping | not 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#
| Header | Used by | Description |
|---|---|---|
Authorization | almost all endpoints | Bearer <access_token> for users, Basic <application_id>:<secret> for applications. The scheme is case-insensitive. |
X-Ertis-Alias | sign-in endpoints | The membership id. Membership and MembershipId are accepted as alternative header names. |
X-IpAddress | sign-in endpoints | Optional. The end user's IP address, stored with the session (useful when the request comes from your backend). |
X-UserAgent | sign-in endpoints | Optional. The end user's user agent, stored with the session. |
X-Host | activation, password reset, OTP | The base URL of the page that the links in the mails point to (see Account Recovery). |
X-Setup-Token | /setup | The setup token (see Getting Started). |
Content-Type | requests with a body | application/json |
Request and response bodies#
- Bodies are JSON. Field names are
snake_casefor most resources (email_address,expires_in); providers, mail hooks and the flags of user types usecamelCase(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#
| Code | Meaning in ErtisAuth |
|---|---|
200 OK | Success with a body. Also returned by a partially successful bulk delete (see below). |
201 Created | A resource or a token was created. Tokens are always returned with 201. |
204 No Content | Success without a body (deletes, sign-out). |
400 Bad Request | The request is malformed or fails validation. |
401 Unauthorized | The token is missing, invalid, expired or revoked, or the credentials are wrong. Comes with a WWW-Authenticate header. |
403 Forbidden | The caller is authenticated but not allowed: missing permission, other membership, or a disabled provider. |
404 Not Found | The resource does not exist in this membership. |
409 Conflict | A duplicate (same slug, same username…), an update without changes, a resource still in use, or a setup already done. |
500 Internal Server Error | An unexpected error. The details are only written to the server log. |
501 Not Implemented | A feature the request needs is not configured, e.g. no mail provider or no activation mail hook. |
503 Service Unavailable | An external provider (Google, Apple…) could not be reached. |
Errors#
Errors have a common shape:
{
"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:
{
"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:
{
"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:
{
"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:
| Parameter | Example | Description |
|---|---|---|
skip | skip=20 | Number of items to skip. Must not be negative. |
limit | limit=10 | Maximum number of items to return. Must not be negative. |
with_count | with_count=true | Also return the total number of matching items in count. |
sort | sort=username or sort=sys.created_at desc | Field to sort by, optionally followed by asc (default) or desc. |
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>'{
"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:
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.
selectincludes fields with1ortrueand excludes them with0orfalse.- 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 with400 InvalidQuery. - Hidden fields, such as
password_hash, can be neither returned nor used in filters or sorting. - For users, the optional
localequery 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.
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:
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#
POSTcreates a resource and answers201 Createdwith the resource and aLocationheader.PUT /{id}updates a resource. The id always comes from the route; an_idin 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
sysobject 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:
curl -X DELETE 'https://auth.example.com/memberships/<membership_id>/users' \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-d '[ "66f1c0d2a4b5c6d7e8f90124", "66f1c0d2a4b5c6d7e8f90127" ]'| Result | Response |
|---|---|
| All deleted | 204 No Content |
| None deleted | 404 BulkDeleteFailed |
| Some deleted | 200 OK with the error body BulkDeletePartial |
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