Skip to content

Endpoints: Core & Sessions

Liveness and readiness probe.

Response 200

{"status": "healthy", "timestamp": "2024-01-15T10:30:00.000000"}

Create a new service account — a named, role-scoped API key, optionally restricted to a set of source IPs — scoped to the caller’s org. Requires admin role (or higher — owner).

Request body — application/json

{"role": "member", "name": "ci-pipeline", "ip_allowlist": ["203.0.113.0/24"]}
FieldTypeDefaultDescription
rolestringmemberviewer, member, admin, or owner
namestring | nullnullHuman-readable label
ip_allowliststring[] | nullnullSource IPs this key may be used from — see below. null/omitted = usable from anywhere

Each ip_allowlist entry is a bare IP ("203.0.113.5", treated as /32) or a CIDR block ("203.0.113.0/24") — a malformed entry rejects the whole request with 422.

Response 201 — key shown once, never retrievable again

{
"key_id": "3f4a5b6c-...",
"key": "xK9mZpQ...",
"role": "member",
"name": "ci-pipeline",
"ip_allowlist": ["203.0.113.0/24"],
"created_at": "2026-01-01T00:00:00Z"
}
StatusDescription
201Key created — save the key value now
401Missing or invalid API key
403Caller does not have admin (or owner) role
422ip_allowlist contains an entry that isn’t a valid IP or CIDR block

List all service accounts in the caller’s org. Raw key values and hashes are never included. Requires admin role.

Response 200

{
"keys": [
{
"key_id": "3f4a5b6c-...",
"role": "member",
"name": "ci-pipeline",
"ip_allowlist": ["203.0.113.0/24"],
"created_at": "2026-01-01T00:00:00Z",
"is_active": true
}
]
}

Revoke a service account. The key is deactivated immediately. Requires admin role.

ParameterDescription
key_idKey ID returned at creation
StatusDescription
200Key revoked
401Missing or invalid API key
403Caller does not have admin role
404Key ID not found

Human login via email/password, backed by a short-lived JWT (config.session.access_ttl_minutes, admin-configurable — see Configuration reference) in an httpOnly cookie — the auth path the dashboard uses. API keys remain the machine-to-machine path (CI, MCP clients); both are consumed identically by require_principal.

Public — no authentication. Creates a user account, then logs them in (sets the session cookie). Three mutually exclusive modes based on which optional field is set:

  • invite_token set → joins the org the invite grants access to, at the invite’s role. See GET /invite/{token} / POST /invite/{token}/accept.
  • org_name set (no invite_token) → creates a brand-new organization owned by the new user (role: owner) — the original, still-default behavior. 403s if a platform admin has turned off registration.allow_public_org_creation (see Configuration Reference — registration) — org-less registration and invite registration are unaffected either way.
  • Neither set → creates the account with no organization at all (org_id/role null in the response and every subsequent session). Every org-scoped endpoint 403s until the account creates an org (POST /orgs) or accepts an invite.

Request body

{"email": "[email protected]", "password": "hunter2", "name": "Ada Lovelace", "org_name": "Acme Corp"}
FieldTypeDescription
emailstring—
passwordstring—
namestring | nullDisplay name, optional at registration — can also be set/changed later via PATCH /auth/me
org_namestring | nullCreate a new org owned by this user
invite_tokenstring | nullJoin an existing org via invite instead

Response 201

{"user_id": "u1...", "org_id": "8f2a...", "role": "owner"}
StatusDescription
201Registered — session cookie set
403org_name given but public organization creation is disabled
404invite_token given but unknown, expired, or revoked
409Email already registered

Public — no authentication. What self-service registration currently allows, so a sign-up page can render the right form instead of offering a path the API would reject.

Response 200

{"allow_public_org_creation": true}
FieldTypeDescription
allow_public_org_creationbooleanMirrors registration.allow_public_org_creation (see Configuration Reference — registration). false → POST /auth/register with org_name, and POST /orgs for a non-superadmin, both 403

Reads the live value, so a PATCH /admin/settings flip shows up on the very next call — no restart.

Public. Verifies email/password and sets the session cookie. A user who belongs to multiple organizations lands in the one they were last scoped to — set by their last login, switch, invitation acceptance or departure, and re-checked against a live, active membership, falling back to their first active membership when it names an org they have since left or been deactivated in. Call POST /auth/session/switch to move somewhere else; that choice is what the next login honours. A user with no organization at all logs in successfully with org_id/role null, not an error.

If the account has two-factor authentication enabled, no session cookie is set here — the response instead carries mfa_token/methods (no user/org_id/role); complete login with POST /auth/session/2fa.

Request body

{"email": "[email protected]", "password": "hunter2"}

Response 200

{
"user": {"user_id": "u1...", "email": "[email protected]", "name": null, "created_at": "2026-01-01T00:00:00Z", "is_active": true, "two_factor_enabled": false},
"org_id": "8f2a...",
"role": "owner",
"is_superadmin": false
}

Response 200 — 2FA enabled, challenge issued instead

{"mfa_token": "eyJhbGciOi...", "methods": ["totp", "recovery_code"]}
StatusDescription
200Logged in (session cookie set), or a 2FA challenge issued (mfa_token, no cookie)
401Invalid email or password, or the account is inactive

Public. Redeems the mfa_token from a POST /auth/session response that required a second factor. code is checked as a current TOTP code first, then as an unused recovery code (tried in that order) — either completes login. A matched recovery code is consumed and can’t be reused. On success, same response shape and session cookie as POST /auth/session.

Request body

{"mfa_token": "eyJhbGciOi...", "code": "123456"}

Response 200 — same shape as POST /auth/session’s successful-login response

StatusDescription
200Logged in — session cookie set
401mfa_token expired/invalid, or code doesn’t match a current TOTP code or an unused recovery code

Clears the session cookie in the calling browser. Other devices signed in to the same account are unaffected — this is a log-out, not a revocation. To end every session at once, change the password (POST /auth/me/password), which revokes all of them except the caller’s own; an administrator can do the same to any account by deactivating it or resetting its password or 2FA. See Session lifetime and revocation.

Response 200

{"detail": "Logged out"}

Resolves the caller’s API key or session cookie into a user/org/role triple. Used by the dashboard to bootstrap identity on page load. Requires any authenticated principal — API-key callers have no user (there’s no human account behind a machine key).

Response 200

{
"user": {"user_id": "u1...", "email": "[email protected]", "name": null, "created_at": "2026-01-01T00:00:00Z", "is_active": true, "two_factor_enabled": false},
"org_id": "8f2a...",
"role": "owner",
"is_superadmin": false,
"organization_locked": false,
"mfa_required": false
}

user is null for an API-key principal. organization_locked reflects platform-admin org locking. mfa_required is true only for a human session whose org requires 2FA and who hasn’t enrolled yet — always false for an API-key principal.

StatusDescription
200Identity resolved
401No valid API key or session

Update the caller’s own profile — currently just name. Requires a human session; 400 for an API-key principal (no backing user document).

Request body

{"name": "Ada Lovelace"}

Response 200 — the updated UserInfo, same shape as GET /auth/me’s user field.

StatusDescription
200Profile updated
400API-key principal
401No session

Change the caller’s own password. Requires the current password — unlike an admin-initiated reset below, the caller proves control of the account itself. Not applicable to SSO-backed accounts (400).

Signs the account out everywhere else: every other browser and device holding a session for it is rejected from its next request onward. The caller stays signed in — the 200 response carries a replacement session cookie, so no re-login is needed on the browser that made the change. See Session lifetime and revocation.

Request body

{"current_password": "old-password", "new_password": "new-password-at-least-8-chars"}
StatusDescription
200Password changed
400API-key principal, or the account authenticates via SSO
401No session, or current_password is wrong
422new_password shorter than 8 characters

Begin TOTP enrollment. Generates a new secret and returns a QR-scannable provisioning URI plus the same secret for manual entry. 2FA is not active yet — call POST /auth/me/2fa/confirm with a code from the authenticator app to finish. Calling this again before confirming replaces the pending secret. Requires a human session on a local account (400 for an API-key principal or an SSO-backed account).

Response 200

{"secret_provisioning_uri": "otpauth://totp/OpenTremor:person%40acme.com?secret=JBSW...&issuer=OpenTremor", "manual_entry_code": "JBSWY3DPEHPK3PXP"}
StatusDescription
200Enrollment started
400API-key principal, or the account authenticates via SSO
401No session

Verify code against the pending secret from POST /auth/me/2fa/enroll. On success, enables 2FA and returns 10 one-time recovery codes — shown once, here, never retrievable again. Store them somewhere safe; each is usable in place of a TOTP code at POST /auth/session/2fa if the authenticator device is lost.

Request body

{"code": "123456"}

Response 200

{"recovery_codes": ["a1b2c3d4e5", "f6a7b8c9d0", "..."]}
StatusDescription
2002FA enabled — recovery codes shown once
400No pending enrollment, API-key principal, or the account authenticates via SSO
401code doesn’t match the pending secret

Turn off two-factor authentication. Requires the current password, same as POST /auth/me/password — turning off a security control needs the same re-proof of account control as changing one.

Request body

{"current_password": "hunter2"}
StatusDescription
2002FA disabled
400API-key principal, or the account authenticates via SSO
401current_password is incorrect

Every organization the caller has a membership in, for the dashboard’s org switcher. Requires a human session — API-key principals are scoped to a single org and have no memberships to list.

Response 200

[
{"org_id": "8f2a...", "name": "Acme Corp", "role": "owner"},
{"org_id": "3f4a...", "name": "Other Org", "role": "member"}
]
StatusDescription
200Membership list
401No session, or an API-key principal

Organization invitations addressed to this account’s email — see Open vs addressed. Already-redeemed, revoked and expired ones are filtered out, so every row is something the caller can still act on. Requires a human session.

This is what makes joining a second organization discoverable rather than dependent on an invite URL reaching someone: this deployment sends no email, so an open invite that never physically arrives may as well not exist.

Response 200

[
{
"invite_id": "3f4a5b6c-...",
"org_name": "Acme Corp",
"role": "member",
"created_at": "2026-01-01T00:00:00Z",
"expires_at": "2026-01-08T00:00:00Z"
}
]

Open invitations are never listed — they have no intended recipient to list them for. Note the absence of token: the caller has already proved who they are by being signed in, so nothing bearer-shaped needs to change hands.

StatusDescription
200Pending invitation list (possibly empty)
401No session, or an API-key principal

POST /auth/me/invitations/{invite_id}/accept

Section titled “POST /auth/me/invitations/{invite_id}/accept”

Redeem an invitation from that list, no token involved. Requires a human session.

Response 200

{"org_id": "8f2a...", "role": "member"}

Also re-scopes the session cookie onto the org just joined, the way POST /auth/session/switch does — otherwise an org-less account would join an organization and still be left belonging to none.

StatusDescription
200Joined; session re-scoped to the new org
401No session, or an API-key principal
404Not on this account’s invitation list — unknown, expired, revoked, or addressed to someone else, deliberately indistinguishable

Re-scopes the session cookie to a different org the caller is a member of. Requires a human session.

Request body

{"org_id": "3f4a..."}

Response 200 — same shape as POST /auth/session

StatusDescription
200Switched — session cookie re-issued for the new org
401No session, or an API-key principal
403Caller is not a member of org_id