API reference

Every endpoint, the role that may call it, and the use case it implements.

Schema-level documentation — every parameter, every request and response body — is published as an OpenAPI document and rendered by the API explorer , which is the same document a running instance serves through its own Swagger UI at /swagger. To call the endpoints, use that, or the .http files or Bruno collection in api-client/.

This page is the map — what exists, who may call it, and which use case it implements.

Reading the “Who” column

NotationMeaning
Anonymous[AllowAnonymous] — no bearer token required.
System Admin[RoleRequirement(SystemAdmin)].
+ Scope AdminThe attribute admits Scope Admins too; the handler then confirms they own the addressed scope.
Any authenticatedNo role attribute on purpose — the actor set the use case grants includes a User, so the attribute would have to admit them and the handler settles it.

Two rules apply everywhere and are not repeated per row:

  • The identifier in a path is always a PublicId (GUID), never an internal id.
  • A challenge token (issued by a 2FA-gated login) is rejected by every endpoint below except POST /api/auth/2fa/verify, with a 401.

Authentication — /api/auth

Method & pathWhoUse case
POST /loginAnonymousUC-11 — password login; answers a token, or a challenge token if 2FA is active
POST /password-recoveryAnonymousUC-12 — request a reset link
POST /password-resetAnonymousUC-13 — set a new password from the token
POST /verify-emailAnonymousUC-14 — confirm an address from the mailed token
POST /resend-verificationAuthenticated (self)UC-15 — retire outstanding links, mail a fresh one; 429 when the account-email budget refuses it (cooldown or daily limit); rate-limited
POST /googleAnonymousUC-25 — Google sign-up/sign-in against a scope
POST /google/sign-outAuthenticated (Google User)UC-26 — end a Google session
POST /2fa/enableAuthenticated (self)UC-36 — begin 2FA setup; needs currentPassword (401 otherwise); with Email, 429 when the account-email budget refuses it
POST /2fa/confirmAuthenticated (self)UC-37 — confirm setup, receive recovery codes
POST /2fa/verifyAnonymousUC-38 — redeem the challenge token with a second factor
POST /2fa/disableAuthenticated (self)UC-39 — turn 2FA off
POST /2fa/recovery-codes/regenerateAuthenticated (self)UC-40 — issue a fresh set of ten
GET /2faAny authenticatedFR-2F-15 — the caller’s own two-factor status; 403 for a Google User
POST /data-exportAny authenticated (self)UC-41 — a copy of everything held about you, with recipients and retention
POST /erasure-requestAny authenticated (self)UC-42 — ask for your own erasure; requires a password or a fresh Google ID token, and lifts your own restriction if you have one (restrictionLifted)
POST /processing-restrictionAny authenticated (self)UC-44 — suspend processing without deleting, on an Art. 18(1) ground
POST /processing-restriction/liftSelf, or System Admin for anotherUC-45 — lift it; an admin must inform the subject first
GET /erasure-requestsSystem AdminUC-43 — the outstanding erasure requests, soonest deadline first

Every anonymous endpoint here is rate-limited to 10 requests per minute per IP. Each of them checks a credential, and none is protected by a token — see Operations for why that matters and what the limit does not replace. The authenticated endpoints that check a password or a second factor — 2fa/enable, 2fa/confirm, 2fa/disable, 2fa/recovery-codes/regenerate, erasure-request and PUT /persons/{id} — share the same limit.

The 2FA endpoints never name a person: the subject is always the caller, read from the bearer token, so a caller can only ever act on their own configuration.

A token naming a restricted identity (UC-44), or one logically deleted and not yet anonymised, is refused everywhere with a 401 — except data-export, erasure-request, processing-restriction and processing-restriction/lift, through which the subject exercises their own rights (NFR-24). Those answer with the use case’s own flows instead: a restricted subject can export, request erasure — which lifts the restriction, so the erasure is not held behind it — lift their own restriction, and is told a second restriction is a conflict (AF-44a).

A restricted identity can still sign in — login, 2fa/verify and google answer 200 with processingRestricted: true and a subject-rights-only token (UC-11 AF-11i, UC-25 AF-25e). That token claims no scope and only those four endpoints accept it; every other endpoint answers 403, even after the restriction is lifted, when the subject signs in again for a full token.

Scopes — /api/scopes

Method & pathWhoUse case
POST /System AdminUC-01 — create a scope with one or more initial owners
GET /System Admin; Scope Admin (the scopes they own)UC-02 — list scopes (paginated, filterable); a Scope Admin’s page and total count their own scopes only
GET /{id}Any authenticatedUC-02 — read one scope
PUT /{id}System AdminUC-03 — update name and description
DELETE /{id}System AdminUC-04 — logical delete (cascades to Users, Google Users, applications)
DELETE /{id}/hardSystem AdminUC-05 — hard delete (also purges permissions and join rows)
PUT /{id}/google-signinSystem Admin + Scope AdminUC-24 — turn Google Sign-In on or off
GET /{id}/pending-registrationsSystem Admin + Scope AdminUC-49 — the self-registrations awaiting confirmation, newest first: name, address, expiry, guesses left; never the hash, salt or token
DELETE /{id}/pending-registrations/{registrationId}System Admin + Scope AdminUC-50 — discard one; its link then answers “Invalid token” and the registrant may sign up again

Persons — /api

Method & pathWhoUse case
POST /personsSystem AdminUC-06 path b — create a ScopeAdmin or SystemAdmin with no scope
POST /scopes/{scopeId}/personsSystem Admin + Scope AdminUC-06 path a — create a User in a scope
POST /scopes/{scopeId}/ownersSystem Admin + Scope AdminUC-06 path c — create a new ScopeAdmin directly as a co-owner
POST /scopes/{scopeId}/owners/{personId}System Admin + Scope AdminUC-21 — add an existing ScopeAdmin or SystemAdmin as an owner
POST /scopes/{scopeId}/users/{personId}/promoteSystem Admin + Scope AdminUC-23 — promote a scope’s User to owner
GET /persons/{id}Any authenticatedUC-07 — read one person (a person may read themselves)
GET /scopes/{scopeId}/personsSystem Admin + Scope AdminUC-07 — list a scope’s Users
GET /scopes/{scopeId}/ownersSystem Admin + Scope AdminUC-07 — list a scope’s owners
GET /persons/scope-adminsSystem Admin + Scope AdminUC-07 read d — list who may own a scope (Scope Admins, plus System Admins for a System Admin caller) for an owner picker; optionally excludes a scope’s current owners
GET /persons/system-adminsSystem AdminUC-07 read e — list the System Admins: who else holds full access
PUT /persons/{id}Any authenticatedUC-08 — update name and email; only a System Admin may change the role. Changing your own email needs currentPassword, and twoFactorCode when your 2FA is active (401 otherwise); any email change revokes the person’s tokens and notifies the previous address
DELETE /persons/{id}System Admin + Scope AdminUC-09 — logical delete
DELETE /persons/{id}/hardSystem AdminUC-10 — hard delete, cascading to owned applications, tokens, 2FA and join rows
POST /persons/{id}/verification-emailSystem Admin + Scope AdminUC-51 — mail a fresh verification link to the person’s own address; a Scope Admin only for Users of scopes they own; 429 within the cooldown
POST /persons/{id}/password-resetSystem Admin + Scope AdminUC-52 — mail a password-reset link to the person’s own address, with UC-12’s eligibility; 429 within the cooldown
DELETE /scopes/{scopeId}/owners/{personId}System Admin + Scope AdminUC-22 — remove an owner (rejected if it would be the last one)

Applications — /api/scopes/{scopeId}/applications

Method & pathWhoUse case
POST /System Admin + Scope AdminUC-16 — register an application in the scope
GET /{id}System Admin + Scope AdminUC-17 — read one
GET /System Admin + Scope AdminUC-17 — list the scope’s applications
PUT /{id}System Admin + Scope AdminUC-18 — update name and owner
DELETE /{id}System Admin + Scope AdminUC-19 — logical delete
DELETE /{id}/hardSystem AdminUC-20 — hard delete

An application’s owner must be a ScopeAdmin or SystemAdmin who owns that scope — a User may never own one (FR-AP-03).

Application credentials — /api/scopes/{scopeId}/applications/{id}/credentials

Method & pathWhoUse case
POST /System Admin + Scope AdminUC-53 — issue a credential; the secret is in this response only; 409 with two live credentials already
GET /System Admin + Scope AdminUC-54 — list the application’s credentials, newest first; never a secret or hash
DELETE /{credentialId}System Admin + Scope AdminUC-55 — revoke a credential; tokens issued from it fail the next live check

A Scope Admin may manage the credentials only of an application they own, in a scope they still own, the rule GET/PUT /{id} apply (FR-AP-12).

Application tokens — /api/applications

Method & pathWhoUse case
POST /tokenAnonymous (the secret authenticates)UC-56 — exchange a credential’s secret for an application token; every refusal is one 401
GET /selfAnonymous (validates its own bearer token)UC-57 — check an application token live; one 401 for every refusal

The Application authentication guide is the page for the service consuming these tokens.

Scope permissions — /api/scopes/{scopeId}/permissions

Method & pathWhoUse case
POST /System Admin + Scope AdminUC-31 — create a permission
GET /{id}System Admin + Scope AdminUC-32 — read one
GET /System Admin + Scope AdminUC-32 — list the scope’s permissions
PUT /{id}System Admin + Scope AdminUC-33 — update name, description, and the JWT-claim flag
DELETE /{id}System Admin + Scope AdminUC-34 — logical delete
DELETE /{id}/hardSystem AdminUC-35 — hard delete

Owning the scope is the whole of the authorization to manage its permissions — a permission has no owner of its own. A permission with IncludeAsJwtClaim set has its name folded into the JWT of identities acting within that scope (FR-AU-08); see Scope permission claims .

Google Users — /api/scopes/{scopeId}/google-users

Method & pathWhoUse case
GET /System Admin + Scope AdminUC-27 — list the scope’s Google Users
GET /{id}Any authenticatedUC-27 — read one; admits the Google User themselves
DELETE /{id}System Admin + Scope AdminUC-28 — logical delete (idempotent: repeating answers 200 with alreadyDeleted: true)
DELETE /{id}/hardSystem AdminUC-29 — hard delete (a second call is a 404)

The by-id read carries no role attribute for a concrete reason: a Google User’s token is User-role, so any attribute strong enough to keep other Users out would lock out the actor UC-27 explicitly grants. The listing admits none of them.

Both deletions take the scope’s PublicId in the path and refuse a Google User reached through the wrong scope.

Audit trail — /api/audit-log

Method & pathWhoUse case
GET /api/audit-logSystem AdminUC-48 — the audit trail, newest first; filter by from/to (UTC, inclusive), actorId, action, targetId, succeeded

An entry is shown as stored: an actor the retention pass has cleared reads as null, so filtering by actorId cannot find it. action is the command’s type name, matched exactly (CreateScopeCommand). A Scope Admin is refused — the trail has no scope column to divide it by.

Health check — /api/healthcheck

Method & pathWhoUse case
GET /api/healthcheckAnonymousUC-30 — liveness; the process is up (no database read)
GET /api/healthcheck/detailedSystem AdminUC-30 — per-dependency status

The API also answers both at the root, /healthcheck and /healthcheck/detailed, for the container health check and yggdrasil’s status page, which call it on its own port. Behind Traefik, only the /api/ addresses reach it from the web UI’s host.

Response shape

Every endpoint answers one of two envelopes:

// DataOutput<T>
{
  "data": { /* the resource, or null */ },
  "messages": ["Scope created successfully"],
  "errors": [],
  "success": true
}
// PaginatedOutput<T> — every list endpoint
{
  "data": [ /* the page */ ],
  "pageNumber": 1,
  "pageSize": 20,
  "totalRecords": 137,
  "messages": [],
  "errors": [],
  "success": true
}

The HTTP status comes from the handler’s message, mapped through the area’s message map (a dictionary from message text to status code), rather than from the controller choosing a status per branch. Same message, same status, everywhere it is returned.

List endpoints accept pagination and filter parameters, all validated before the query runs (NFR-10), and most accept includeDeleted=true to see logically deleted rows.

Malformed requests

A request that cannot be bound — a body that is not JSON, or a value of the wrong type in the body or query string (?succeeded=maybe, {"name": 123}) — answers 400 with the error The request is malformed., on every endpoint (NFR-10). The answer is a DataOutput envelope even on list endpoints. Authentication and role checks come first, so a caller without a valid token still receives 401, and one of the wrong role 403; refusals that depend on data, made in a handler, come after the 400.

A query parameter that is not nullable no longer falls back to its default when it is empty or unparseable: ?pageNumber=, ?pageNumber=abc, ?pageSize=, ?includeDeleted= and ?includeDeleted=1 are all 400. A nullable one sent empty (?succeeded=) still binds to null and is ignored. A missing required field is not a binding failure: it keeps the field’s own message from the validator.

Of the endpoints that take a body, only POST …/credentials accepts an empty one, meaning “no expiry”.

Errors that are deliberately vague

Some responses tell the caller less than the API knows, on purpose:

  • POST /api/auth/login answers the same “invalid credentials” error whether the email is unknown, the password wrong, the person logically deleted, or their scope gone (AF-11a…AF-11e). The checks still run in the specification’s order so the code reads against UC-11 — only the answer is uniform.
  • POST /api/auth/password-recovery answers identically whether or not the address belongs to anyone — in the same time, too: it only queues the request, and the lookup, the token and the email happen after the response. A Mailgun failure is logged rather than surfaced — an email outage must not turn into a 500 that confirms an anonymous caller’s guess.
  • POST /api/auth/google answers alike for an unverifiable token, a token that does not carry the nonce the request sent, and a Google User whose account is gone (AF-25a, AF-25g, AF-25d).
  • POST /api/auth/password-reset and POST /api/auth/verify-email answer “Invalid token.” both for a token nobody issued and for one whose person has since been restricted, deleted or anonymised, or can no longer sign in (AF-13e, AF-14d).

The specification

§5 of the System Requirements Document lists the endpoints as specified, and §7 is the full authorization matrix — every action against every role, including Anonymous. The Use Case Specification Document gives each endpoint’s main flow and every numbered alternative flow.