Identity
Create Session Application Delegation
Mints (or replaces the scope of) a delegation authorizing the calling application to act as the user named by session_token, which the kernel verifies itself -- it is data, never a trusted assertion from the caller. Authenticated as the application (its own bearer token in Authorization), never by X-Acting-As-User: there is no acting-as user until this call succeeds. capabilities must be non-empty. A second call for the same user replaces the existing delegation's scope rather than creating a duplicate.
Create Session Application Delegation › Request Body
Non-empty: a delegation that grants nothing is refused.
session_tokenCreate Session Application Delegation › Responses
The minted or replaced delegation
iduser_idLook Up Application User Profiles
Public profiles of up to 100 users at once, for an application token holding users.read.public. Same projection as the single read. Ids with no account are absent from the answer rather than an error, and duplicates are collapsed. More than 100 distinct ids is refused.
Look Up Application User Profiles › Request Body
idsAt most [MAX_LOOKUP_IDS]; duplicates are fine.
Look Up Application User Profiles › Responses
The profiles that exist
Find User By Linked Account
The kernel user a linked provider account belongs to, for an application token holding users.lookup.oauth (a separate capability from users.read.public). Returns user_id, display_name and language_tag. 404 when no user has linked that account or the user is deleted.
query Parameters
providerThe provider's id, for example discord.
The provider's id, for example discord.
provider_user_idThe user's id at that provider (a Discord snowflake).
The user's id at that provider (a Discord snowflake).
Find User By Linked Account › Responses
The linked user
display_namelanguage_tagThe user's language, for example en-US.
user_idGet Application User Profile
A user's public profile, for an application token holding users.read.public. Returns id, name, avatar_asset_id and deleted_at and nothing else: never an email. A deleted account is still answered, with deleted_at set. Refused with 403 for a user session, a user API token, an acting-as request, and an application without the capability.
path Parameters
idGet Application User Profile › Responses
The user's public profile
idnameThe user's display name, as shown to other users.
avatar_asset_idThe profile picture, addressed through /api/v2/assets/{id}. None
when they never set one.
deleted_atSet when the account was deleted. The profile is still answered so a listing can render "deleted user" rather than "unknown user".
Get My Discovery State
The caller's own {key: seen_at} map of discovery-dot keys (nav/tab/product/ui) they have already seen. An account with no stored row answers {"seen": {}}, never 404 -- everything is simply unseen. Kernel does not know which keys currently exist or which of them are "new"; console derives that from its own manifest, joined against this map and the account's creation date.
Get My Discovery State › Responses
The caller's seen discovery keys
Mark Discovery Keys Seen
Marks up to 20 of the caller's own discovery-dot keys seen, at the moment of the call. Re-marking an already-seen key is a no-op: its original seen_at is kept, never bumped. Refuses the WHOLE call, naming the offending key, if any key fails the <kind>:<path> format (lowercase, [a-z0-9/_-], at most 96 characters, kind one of nav/tab/product/ui) or if more than 20 keys are submitted -- nothing is written in either case. Rate-limited per caller (RateLimitScope::DiscoverySeen, about 30 requests per minute).
Mark Discovery Keys Seen › Request Body
keysMark Discovery Keys Seen › Responses
The keys are recorded as seen
Get Current Identity
Gets the caller's own identity, including the platform permissions it holds. system_permissions is empty for an ordinary customer and non-empty for staff, which is the whole of whether the admin app may be opened -- the kernel has no separate staff flag. This operation is not elevation-gated, so an un-elevated staff session can still read what it holds.
Get Current Identity › Responses
The caller's own identity, as seen on the /v2 console surface.
created_atdiscord_notifications_enabledWhether kernel-generated notifications are relayed to this user as
Discord DMs, on top of the in-app center they always get. Read back
so the toggle PATCH /me writes has somewhere to read its own
current state from.
idis_onboardedlanguageThe caller's own display language, as an unvalidated BCP-47-ish tag
(LanguageCode is a newtype over String and validates nothing).
Published because the onboarding screen and the account page both
need to render the value the account already holds as their default.
console/app/onboarding/page.tsx records the gap this closes: it had
to start the language and billing-country selects empty because
nothing on /v2 reported either field, which meant re-answering
onboarding silently guessed instead of showing what was already
stored.
namesystem_permissionsThe caller's own platform permissions, as
[kernel_core::domain::permissions::SystemPermission]'s wire strings
(view_all_orders, manage_users, ...). Empty for an ordinary
customer, which is nearly every caller.
Holding one of these IS being staff. The kernel has no separate staff flag, so a non-empty list is the whole of "this caller may open the admin app", and each value decides one gated section inside it.
Field-for-field the same answer /v1's
CurrentUserView::system_permissions gave (deleted with /v1 in
88cb24c1; it rendered User::permissions through the same Display).
Nothing on /v2 published it, so admin recovered it by intersecting
GET /api/v2/admin/users/{id}'s roles with GET /api/v2/admin/ permissions' granted_by_roles: two extra round trips per render,
growing with the role count, and both of those routes sit under
/admin/ and so behind require_elevated_session -- meaning an
un-elevated staff session could not learn what it held at all
(ZC-0410, and ZC-0411 which this closes with it). /api/v2/me has no
admin path segment, so it is not elevation-gated and does not
reintroduce that trap.
Empty for a restricted API token, never the owning user's list.
A restricted token's authority is its grants, which
domain::policies::api_token_intersect intersects with these
permissions once per operation; there is no mapping from a
SystemPermission back to the ApiTokenCapability a token would also
have to carry, so the owner's list is not an answer to "what may this
token do", and publishing it would tell a leaked token only that its
owner is staff. /v1 never had to decide this: its current-user
route refused a token caller outright.
avatar_asset_idThe caller's own avatar (ZC-1306), addressed through
/api/v2/assets/{id}. None when the account has never set one
(no OAuth provider avatar and nothing uploaded). An asset id, not a
URL -- see OrganizationResponse::icon_asset_id's own doc comment
for the /v2 convention this follows: the id itself changes on
every replace, uploaded or provider-synced alike, so a consumer
never has to append a cache-busting query parameter.
billing_country_codeThe ISO 3166-1 alpha-2 country the account is billed from, None
until onboarding answers it.
Write-once in practice: UserManagementService::update_user_profile
refuses to change it once is_onboarded is true and a value is
already set, so a client rendering an edit control for it after
onboarding is rendering a control that cannot succeed.
emailResolved through UserEmailService (checks oauth_accounts first,
then password_credentials). None when neither source has an
address for this user -- User itself carries no email field, so
this cannot be filled in by a pure User conversion; see
get_me_handler.
Update Current Identity
Updates the caller's own profile. Every field is optional and an omitted field is left unchanged. billing_country_code is refused once onboarding is complete and a country is already stored, because it decides tax treatment.
Update Current Identity › Request Body
billing_country_codeThe country this account is billed from, ISO 3166-1 alpha-2, case insensitive. Refused with 422 once onboarding is complete and a country is already stored: the value decides tax treatment, so it is deliberately not something a customer can re-answer later.
discord_notifications_enabledWhether to relay notifications to Discord.
languageA new display language tag.
nameA new display name. Moderated by Username::try_new_moderated, so a
name that is too short, too long, wrongly shaped or offensive is
refused rather than stored.
Update Current Identity › Responses
The saved profile, in the same shape GET /me returns
created_atdiscord_notifications_enabledWhether kernel-generated notifications are relayed to this user as
Discord DMs, on top of the in-app center they always get. Read back
so the toggle PATCH /me writes has somewhere to read its own
current state from.
idis_onboardedlanguageThe caller's own display language, as an unvalidated BCP-47-ish tag
(LanguageCode is a newtype over String and validates nothing).
Published because the onboarding screen and the account page both
need to render the value the account already holds as their default.
console/app/onboarding/page.tsx records the gap this closes: it had
to start the language and billing-country selects empty because
nothing on /v2 reported either field, which meant re-answering
onboarding silently guessed instead of showing what was already
stored.
namesystem_permissionsThe caller's own platform permissions, as
[kernel_core::domain::permissions::SystemPermission]'s wire strings
(view_all_orders, manage_users, ...). Empty for an ordinary
customer, which is nearly every caller.
Holding one of these IS being staff. The kernel has no separate staff flag, so a non-empty list is the whole of "this caller may open the admin app", and each value decides one gated section inside it.
Field-for-field the same answer /v1's
CurrentUserView::system_permissions gave (deleted with /v1 in
88cb24c1; it rendered User::permissions through the same Display).
Nothing on /v2 published it, so admin recovered it by intersecting
GET /api/v2/admin/users/{id}'s roles with GET /api/v2/admin/ permissions' granted_by_roles: two extra round trips per render,
growing with the role count, and both of those routes sit under
/admin/ and so behind require_elevated_session -- meaning an
un-elevated staff session could not learn what it held at all
(ZC-0410, and ZC-0411 which this closes with it). /api/v2/me has no
admin path segment, so it is not elevation-gated and does not
reintroduce that trap.
Empty for a restricted API token, never the owning user's list.
A restricted token's authority is its grants, which
domain::policies::api_token_intersect intersects with these
permissions once per operation; there is no mapping from a
SystemPermission back to the ApiTokenCapability a token would also
have to carry, so the owner's list is not an answer to "what may this
token do", and publishing it would tell a leaked token only that its
owner is staff. /v1 never had to decide this: its current-user
route refused a token caller outright.
avatar_asset_idThe caller's own avatar (ZC-1306), addressed through
/api/v2/assets/{id}. None when the account has never set one
(no OAuth provider avatar and nothing uploaded). An asset id, not a
URL -- see OrganizationResponse::icon_asset_id's own doc comment
for the /v2 convention this follows: the id itself changes on
every replace, uploaded or provider-synced alike, so a consumer
never has to append a cache-busting query parameter.
billing_country_codeThe ISO 3166-1 alpha-2 country the account is billed from, None
until onboarding answers it.
Write-once in practice: UserManagementService::update_user_profile
refuses to change it once is_onboarded is true and a value is
already set, so a client rendering an edit control for it after
onboarding is rendering a control that cannot succeed.
emailResolved through UserEmailService (checks oauth_accounts first,
then password_credentials). None when neither source has an
address for this user -- User itself carries no email field, so
this cannot be filled in by a pure User conversion; see
get_me_handler.
Complete Onboarding
Marks the caller's onboarding as complete, which is what opens every layout behind the signed-in shell. Idempotent only in the sense that it refuses a second call with 422 rather than silently succeeding. Returns the saved identity; no fresh access token is issued, because authentication reloads the user from the database on every request and nothing reads the is_onboarded JWT claim (see crates/http/foundation/src/middleware/authentication.rs), so the caller's existing token already reports the new state. /v1's equivalent minted one, which was a credential nobody needed.
Complete Onboarding › Responses
The identity, now onboarded
created_atdiscord_notifications_enabledWhether kernel-generated notifications are relayed to this user as
Discord DMs, on top of the in-app center they always get. Read back
so the toggle PATCH /me writes has somewhere to read its own
current state from.
idis_onboardedlanguageThe caller's own display language, as an unvalidated BCP-47-ish tag
(LanguageCode is a newtype over String and validates nothing).
Published because the onboarding screen and the account page both
need to render the value the account already holds as their default.
console/app/onboarding/page.tsx records the gap this closes: it had
to start the language and billing-country selects empty because
nothing on /v2 reported either field, which meant re-answering
onboarding silently guessed instead of showing what was already
stored.
namesystem_permissionsThe caller's own platform permissions, as
[kernel_core::domain::permissions::SystemPermission]'s wire strings
(view_all_orders, manage_users, ...). Empty for an ordinary
customer, which is nearly every caller.
Holding one of these IS being staff. The kernel has no separate staff flag, so a non-empty list is the whole of "this caller may open the admin app", and each value decides one gated section inside it.
Field-for-field the same answer /v1's
CurrentUserView::system_permissions gave (deleted with /v1 in
88cb24c1; it rendered User::permissions through the same Display).
Nothing on /v2 published it, so admin recovered it by intersecting
GET /api/v2/admin/users/{id}'s roles with GET /api/v2/admin/ permissions' granted_by_roles: two extra round trips per render,
growing with the role count, and both of those routes sit under
/admin/ and so behind require_elevated_session -- meaning an
un-elevated staff session could not learn what it held at all
(ZC-0410, and ZC-0411 which this closes with it). /api/v2/me has no
admin path segment, so it is not elevation-gated and does not
reintroduce that trap.
Empty for a restricted API token, never the owning user's list.
A restricted token's authority is its grants, which
domain::policies::api_token_intersect intersects with these
permissions once per operation; there is no mapping from a
SystemPermission back to the ApiTokenCapability a token would also
have to carry, so the owner's list is not an answer to "what may this
token do", and publishing it would tell a leaked token only that its
owner is staff. /v1 never had to decide this: its current-user
route refused a token caller outright.
avatar_asset_idThe caller's own avatar (ZC-1306), addressed through
/api/v2/assets/{id}. None when the account has never set one
(no OAuth provider avatar and nothing uploaded). An asset id, not a
URL -- see OrganizationResponse::icon_asset_id's own doc comment
for the /v2 convention this follows: the id itself changes on
every replace, uploaded or provider-synced alike, so a consumer
never has to append a cache-busting query parameter.
billing_country_codeThe ISO 3166-1 alpha-2 country the account is billed from, None
until onboarding answers it.
Write-once in practice: UserManagementService::update_user_profile
refuses to change it once is_onboarded is true and a value is
already set, so a client rendering an edit control for it after
onboarding is rendering a control that cannot succeed.
emailResolved through UserEmailService (checks oauth_accounts first,
then password_credentials). None when neither source has an
address for this user -- User itself carries no email field, so
this cannot be filled in by a pure User conversion; see
get_me_handler.
Upload My Avatar
Uploads the caller's avatar. Send as multipart/form-data with the image in a part named file: JPEG, PNG, WebP or static GIF (first frame), identified by magic bytes; anything else, including SVG, is refused. Re-encoded server-side to a 512x512 lossy WebP with EXIF orientation applied and all other metadata stripped. Every upload -- including a replace -- gets a FRESH asset id, so the returned avatar_asset_id always changes. Marks the avatar's source as uploaded: an automatic OAuth avatar sync on a later login will not overwrite it unless the caller explicitly re-selects a provider's avatar via PUT /me/avatar/source. Rate-limited per caller.
Upload My Avatar › Responses
The caller's identity, with its new avatar_asset_id
created_atdiscord_notifications_enabledWhether kernel-generated notifications are relayed to this user as
Discord DMs, on top of the in-app center they always get. Read back
so the toggle PATCH /me writes has somewhere to read its own
current state from.
idis_onboardedlanguageThe caller's own display language, as an unvalidated BCP-47-ish tag
(LanguageCode is a newtype over String and validates nothing).
Published because the onboarding screen and the account page both
need to render the value the account already holds as their default.
console/app/onboarding/page.tsx records the gap this closes: it had
to start the language and billing-country selects empty because
nothing on /v2 reported either field, which meant re-answering
onboarding silently guessed instead of showing what was already
stored.
namesystem_permissionsThe caller's own platform permissions, as
[kernel_core::domain::permissions::SystemPermission]'s wire strings
(view_all_orders, manage_users, ...). Empty for an ordinary
customer, which is nearly every caller.
Holding one of these IS being staff. The kernel has no separate staff flag, so a non-empty list is the whole of "this caller may open the admin app", and each value decides one gated section inside it.
Field-for-field the same answer /v1's
CurrentUserView::system_permissions gave (deleted with /v1 in
88cb24c1; it rendered User::permissions through the same Display).
Nothing on /v2 published it, so admin recovered it by intersecting
GET /api/v2/admin/users/{id}'s roles with GET /api/v2/admin/ permissions' granted_by_roles: two extra round trips per render,
growing with the role count, and both of those routes sit under
/admin/ and so behind require_elevated_session -- meaning an
un-elevated staff session could not learn what it held at all
(ZC-0410, and ZC-0411 which this closes with it). /api/v2/me has no
admin path segment, so it is not elevation-gated and does not
reintroduce that trap.
Empty for a restricted API token, never the owning user's list.
A restricted token's authority is its grants, which
domain::policies::api_token_intersect intersects with these
permissions once per operation; there is no mapping from a
SystemPermission back to the ApiTokenCapability a token would also
have to carry, so the owner's list is not an answer to "what may this
token do", and publishing it would tell a leaked token only that its
owner is staff. /v1 never had to decide this: its current-user
route refused a token caller outright.
avatar_asset_idThe caller's own avatar (ZC-1306), addressed through
/api/v2/assets/{id}. None when the account has never set one
(no OAuth provider avatar and nothing uploaded). An asset id, not a
URL -- see OrganizationResponse::icon_asset_id's own doc comment
for the /v2 convention this follows: the id itself changes on
every replace, uploaded or provider-synced alike, so a consumer
never has to append a cache-busting query parameter.
billing_country_codeThe ISO 3166-1 alpha-2 country the account is billed from, None
until onboarding answers it.
Write-once in practice: UserManagementService::update_user_profile
refuses to change it once is_onboarded is true and a value is
already set, so a client rendering an edit control for it after
onboarding is rendering a control that cannot succeed.
emailResolved through UserEmailService (checks oauth_accounts first,
then password_credentials). None when neither source has an
address for this user -- User itself carries no email field, so
this cannot be filled in by a pure User conversion; see
get_me_handler.
Set My Avatar Source
Re-selects a linked OAuth provider's avatar as the caller's avatar, downloading, normalizing and storing it the same way an automatic login sync would. The one provider must already be linked and must have a stored avatar. Unlike an automatic sync, this always overwrites -- including an uploaded avatar -- because making this call IS the explicit choice that moves the account off an upload.
Set My Avatar Source › Request Body
provider_idThe id of an OAuth provider the caller has already linked an account with. Must have a stored avatar (the provider returned one at some login).
Set My Avatar Source › Responses
The caller's identity, with its new avatar_asset_id
created_atdiscord_notifications_enabledWhether kernel-generated notifications are relayed to this user as
Discord DMs, on top of the in-app center they always get. Read back
so the toggle PATCH /me writes has somewhere to read its own
current state from.
idis_onboardedlanguageThe caller's own display language, as an unvalidated BCP-47-ish tag
(LanguageCode is a newtype over String and validates nothing).
Published because the onboarding screen and the account page both
need to render the value the account already holds as their default.
console/app/onboarding/page.tsx records the gap this closes: it had
to start the language and billing-country selects empty because
nothing on /v2 reported either field, which meant re-answering
onboarding silently guessed instead of showing what was already
stored.
namesystem_permissionsThe caller's own platform permissions, as
[kernel_core::domain::permissions::SystemPermission]'s wire strings
(view_all_orders, manage_users, ...). Empty for an ordinary
customer, which is nearly every caller.
Holding one of these IS being staff. The kernel has no separate staff flag, so a non-empty list is the whole of "this caller may open the admin app", and each value decides one gated section inside it.
Field-for-field the same answer /v1's
CurrentUserView::system_permissions gave (deleted with /v1 in
88cb24c1; it rendered User::permissions through the same Display).
Nothing on /v2 published it, so admin recovered it by intersecting
GET /api/v2/admin/users/{id}'s roles with GET /api/v2/admin/ permissions' granted_by_roles: two extra round trips per render,
growing with the role count, and both of those routes sit under
/admin/ and so behind require_elevated_session -- meaning an
un-elevated staff session could not learn what it held at all
(ZC-0410, and ZC-0411 which this closes with it). /api/v2/me has no
admin path segment, so it is not elevation-gated and does not
reintroduce that trap.
Empty for a restricted API token, never the owning user's list.
A restricted token's authority is its grants, which
domain::policies::api_token_intersect intersects with these
permissions once per operation; there is no mapping from a
SystemPermission back to the ApiTokenCapability a token would also
have to carry, so the owner's list is not an answer to "what may this
token do", and publishing it would tell a leaked token only that its
owner is staff. /v1 never had to decide this: its current-user
route refused a token caller outright.
avatar_asset_idThe caller's own avatar (ZC-1306), addressed through
/api/v2/assets/{id}. None when the account has never set one
(no OAuth provider avatar and nothing uploaded). An asset id, not a
URL -- see OrganizationResponse::icon_asset_id's own doc comment
for the /v2 convention this follows: the id itself changes on
every replace, uploaded or provider-synced alike, so a consumer
never has to append a cache-busting query parameter.
billing_country_codeThe ISO 3166-1 alpha-2 country the account is billed from, None
until onboarding answers it.
Write-once in practice: UserManagementService::update_user_profile
refuses to change it once is_onboarded is true and a value is
already set, so a client rendering an edit control for it after
onboarding is rendering a control that cannot succeed.
emailResolved through UserEmailService (checks oauth_accounts first,
then password_credentials). None when neither source has an
address for this user -- User itself carries no email field, so
this cannot be filled in by a pure User conversion; see
get_me_handler.
List My API Tokens
Lists the caller's own live personal API tokens, newest first, with each token's capability set and last use. Revoked tokens are omitted: this is a credential inventory, not an audit log. No response here ever carries a secret or its digest -- the secret shown once by the create call cannot be recovered from this route or any other. Never returns another account's tokens, and never returns an organization's tokens (see GET /organizations/{id}/api-tokens for those).
List My API Tokens › Responses
The caller's own live personal API tokens
Create My API Token
Mints a personal API token and returns the secret exactly once, in the form zaroz_pat_{tokenId}_{secret}. Only a digest is stored. authorization is the token's grant list; every capability must be one the caller may grant (GET /me/api-token-capabilities). A grant containing an admin-only capability needs admin_authorized: true and an elevated session, otherwise 403 elevation-required. Requires a full signed-in session: no API token may mint another.
Create My API Token › Request Body
The grants the token holds. Every capability must be one the caller
may grant themselves (GET /me/api-token-capabilities).
name1-64 characters after trimming.
admin_authorizedMust be true exactly when the authorization contains an admin-only capability, and then requires an elevated session.
expires_atOmit for a token that never expires on its own. Must be in the future.
Create My API Token › Responses
The token, and its secret, shown once
secretThe full credential, shown exactly once. Only a digest of its secret half is stored.
One of the caller's own personal API tokens (ZC-0512).
Never carries the secret, or even its digest. Only a digest of the
secret half is stored on token_hash, and this response omits that field
entirely: the full credential is returned exactly once, at creation
(POST /me/api-tokens), and no later read -- this one included -- can
reproduce it.
Unlike [crate::contracts::organization_api_tokens::OrganizationApiTokenResponse],
capabilities here is not filtered to resource-scoped grants: a personal
token may hold a global or resource-type-scoped grant as well as a
resource-scoped one, so every capability the token's authorization names
is reported, regardless of scope shape. A token created before restricted
authorization existed carries no authorization at all (a legacy
unrestricted token); it reports an empty list rather than "every
capability there is", since there is no finite list of everything an
unrestricted token can do to render here.
List My API Token Capabilities
The capabilities the caller may put in a token they mint, filtered by what they hold themselves, so a token editor renders from the kernel rather than a hardcoded copy. admin_only marks the ones that need an elevated session. Any signed-in session may read it; a restricted API token needs user_api_tokens.list, because the list reflects the account's staff permissions.
List My API Token Capabilities › Responses
The capabilities this caller may grant
Revoke My API Token
Revokes one of the caller's own personal API tokens immediately. A token id that belongs to another account, to an organization, or to nothing at all all answer the same not found, so this route cannot be used to discover which token ids exist. Revoking an already-revoked token is that same not found. Requires a full signed-in session: no API token, restricted or otherwise, may call this route, even to revoke itself or a sibling token.
path Parameters
token_idRevoke My API Token › Responses
Token revoked
List My Application Delegations
Lists every application holding a live-or-revoked delegation over the caller's own account, newest first: which application, what scope was approved, when it was last used. Never returns another account's delegations.
List My Application Delegations › Responses
The caller's own application delegations
Create My Application Delegation
Approves an application acting as the caller, within the named capability set, using the caller's own signed-in session. A delegation already live for this application is replaced with this scope rather than duplicated -- there is at most one delegation per (application, user) pair. Requires a full session: no API token, restricted or otherwise, may consent on the caller's behalf.
Create My Application Delegation › Request Body
application_idNon-empty: a consent event that grants nothing is refused.
Create My Application Delegation › Responses
Delegation created or replaced
Preview Application Consent
The presentable name of an application the caller is about to approve, for the consent page (consent_url in a delegation-required answer). Requires a full session, like approving. An unknown or revoked application answers 404.
path Parameters
application_idPreview Application Consent › Responses
The application's presentable name
application_idapplication_namePresentable name, the same one MyApplicationDelegationResponse carries.
Revoke My Application Delegation
Revokes one of the caller's own application delegations immediately: the application can no longer act as the caller from the next request onward. Nothing the application already did is undone -- servers it created, tickets it opened, and so on, are untouched. A delegation id belonging to another account, or to nothing at all, answers the same not found, so this route cannot be used to discover which delegation ids exist. Requires a full session, same as revoking a personal API token.
path Parameters
delegation_idRevoke My Application Delegation › Responses
Delegation revoked
List My Notifications
The caller's own notifications, newest first, paginated. unread_count is the caller's TOTAL unread count across every page, not only the page returned -- it is what a bell icon's badge renders. A notification whose organization_id names an organization the caller belongs to is included exactly the same as one with no organization at all; delivery is always to the caller regardless of that field.
query Parameters
pagePage number (1-indexed). Defaults to 1.
Page number (1-indexed). Defaults to 1.
page_sizeNumber of items per page. Defaults to 20, max 100.
Number of items per page. Defaults to 20, max 100.
List My Notifications › Responses
The caller's notifications, newest first
Pagination metadata included in paginated responses
unread_countThe caller's TOTAL unread count, across every page -- not merely how
many of the notifications in data are unread. The bell icon's badge
needs the whole number regardless of which page happens to be open.
Stream My Notifications
Holds a text/event-stream open for the caller's own new notifications, so the bell can push a toast without polling. Every frame is a notification_created event carrying the same fields GET /me/notifications would show for that row except read (always false, since the frame is only ever sent once, at creation) and organization_id (not carried by the underlying event, and never guessed). The server closes the connection after fifteen minutes and the first frame sets the reconnect interval, so a browser EventSource re-opens it by itself.
Stream My Notifications › Responses
An open event stream. The schema describes the data: payload of one notification_created frame, not a single JSON response body.
titlebodycreated_atThe moment this frame was assembled, not a persisted column -- see this type's own doc comment.
idkindThe wire spelling of NotificationKind, converted explicitly at the
transport boundary rather than deriving JsonSchema on the domain type
directly (domain crates do not take a schemars dependency -- see
crates/http/CLAUDE.md's "Transport boundary").
Subscription stays spelled order on the wire. See
NotificationKind::Subscription's own doc comment: it was renamed from
Order in the domain (ZC-0261 phase 9f), but notifications.kind is a
text column written since /v1 and dashboard's notification feed
(lib/notifications.ts) already reads the literal string "order" --
changing the wire value would be a breaking, silent rename of every
existing row's meaning.
hrefMark a Notification Read
Marks exactly one of the caller's own notifications read and returns it. Idempotent: marking an already-read notification read again still answers 200 with read: true. Answers 404, identically, both for an id that does not exist at all and for a real notification belonging to somebody else -- never 403, which would confirm the id names a real row.
path Parameters
idMark a Notification Read › Responses
The notification, now read
titlebodycreated_atidkindThe wire spelling of NotificationKind, converted explicitly at the
transport boundary rather than deriving JsonSchema on the domain type
directly (domain crates do not take a schemars dependency -- see
crates/http/CLAUDE.md's "Transport boundary").
Subscription stays spelled order on the wire. See
NotificationKind::Subscription's own doc comment: it was renamed from
Order in the domain (ZC-0261 phase 9f), but notifications.kind is a
text column written since /v1 and dashboard's notification feed
(lib/notifications.ts) already reads the literal string "order" --
changing the wire value would be a breaking, silent rename of every
existing row's meaning.
readhreforganization_idList My Sessions
Lists the caller's own live sessions (refresh-token families), newest activity first is not guaranteed -- see the response schema for what each row means and why there is no current flag. Only ever the caller's own rows: scoped by user_id at the repository query, not by a check on an already-loaded list.
List My Sessions › Responses
The caller's own live sessions
Revoke My Session
Revokes one of the caller's own sessions immediately, ending every access/refresh token pair issued from that family. A session id that belongs to another account or to nothing at all is silently a no-op, exactly like RefreshTokenService::revoke_session: this route never reveals whether a given id exists at all, only whether the caller's own session list changed. Revoking the session the caller is currently making this request with is allowed, and signs them out: there is no current protection, because nothing on the request lets this route know which session that is (see the response schema's own note on why there is no current flag).
path Parameters
session_idRevoke My Session › Responses
no content
Log In With Password
Starts a password login. A correct email and password earn a one-time code emailed to the address, not a session -- submit it to POST /auth/login/verify to open one.
Log In With Password › Request Body
emailpasswordLog In With Password › Responses
What a password login resolved to.
Today this only ever resolves to MfaRequired:
PasswordAuthService::log_in always emails a one-time code and never
returns a session directly, and there is no configuration flag that
skips the challenge. This stays an enum (rather than a bare
challenge_id field) so a login path that can one day skip the
challenge does not have to break the response shape.
Decision Table
| Variant | Matching Criteria |
|---|---|
| type = object · requires: status, challenge_id |
challenge_idstatusVerify Login Code
Completes a password login by submitting the emailed code, opening a new session. The code expires shortly and the challenge is burned after a handful of wrong attempts.
Verify Login Code › Request Body
challenge_idcodeVerify Login Code › Responses
A new session
refresh_tokensession_idtokenMinimal caller identity carried on a session payload.
Deliberately not the full /console/me shape
(kernel_http_v2_identity::contracts::me::MeResponse): /v2 feature
crates do not depend on one another (see crates/http/CLAUDE.md), so
this is a small, independent projection sized for what a session
response needs, not a shared type.
Refresh Session
Exchanges a refresh token for a new access token and a rotated refresh token.
Refresh Session › Responses
A rotated access/refresh token pair. token is bare, same as
[SessionResponse::token].
refresh_tokensession_idtokenGet Current Session
Reports the caller's own identity when the bearer token names a valid, current session. Fails with 401 otherwise, so this doubles as a lightweight session validity check for the console shell.
Get Current Session › Responses
The caller's own identity, returned only when the bearer token names a valid, current session -- so a successful call doubles as "am I signed in".
Minimal caller identity carried on a session payload.
Deliberately not the full /console/me shape
(kernel_http_v2_identity::contracts::me::MeResponse): /v2 feature
crates do not depend on one another (see crates/http/CLAUDE.md), so
this is a small, independent projection sized for what a session
response needs, not a shared type.
Sign Up With Password
Starts an email and password registration. Always reports the same acceptance whether or not the address already has an account: unlike /v1's equivalent, this response cannot be used to test whether an address is registered. referral_code, if given, is attributed atomically when the account is actually created (at POST /auth/password/verify, not here) -- an unknown, expired, or self-owned code never blocks or slows down signup, and never changes this response, so this route cannot be used to probe which referral codes are real. origin, if given, must be on the exact host of a registered OAuth redirect URL or an active reseller's own hostname, with at most one app mount path (/gaming, /console or /admin; V-0664), e.g. https://zaroz.cloud/gaming (ZC-1425) -- a disallowed or malformed value refuses the whole request with a 4xx, never a silent fallback to the platform dashboard.
Sign Up With Password › Request Body
emailpasswordoriginThe app base the emailed verification link should point back at
(ZC-1425, V-0664): https plus a host, optionally followed by exactly
one app mount path -- https://zaroz.cloud/gaming,
https://zaroz.cloud/console, https://zaroz.cloud/admin, or a
reseller's https://<host>/gaming. A bare origin keeps working and
links to the host root. No other path, no query, no fragment. The link
is <origin><path>/verify-email?token=.... Checked against
the same allowlist OAuth's own redirect_to uses; a disallowed or
malformed origin refuses the whole request with a 4xx, never a
silent fallback. Omit to keep the default: the dashboard.
referral_codeA referral link's slug, if the visitor arrived through one (ZC-1032,
owner ruling 2026-09-17: attribution happens at signup). Never
validated here and never blocks or slows down signup, nor changes
this route's response, whether the code is real, expired, self-owned,
or absent -- see signup_handler's own doc comment for why.
Sign Up With Password › Responses
The outcome of POST /auth/password/signup, deliberately identical
whether or not the address already has an account.
/v1's equivalent (PasswordSignupResponse in
crates/http/identity/src/contracts/password_auth.rs) is a tagged union
with an AlreadyRegistered { has_password } variant, returned verbatim
whenever PasswordAuthService::sign_up resolves to
SignupOutcome::AlreadyRegistered. That makes a signup attempt an
account-existence oracle: posting any address and reading the response
alone tells the caller whether it is registered. V2 does not carry that
forward -- controllers::password::signup_handler collapses both
SignupOutcome variants into this one response, so there is no second
variant here to return by mistake.
acceptedAlways true. Kept as a real typed field, rather than an empty
Success(()), so a generated client gets an actual success body;
there is nothing more to report here without reopening the oracle
described above.
Verify Email Address
Consumes the token emailed after signing up, creating the account (or attaching the password to whoever already owns the address), then signs the caller in.
Verify Email Address › Request Body
tokenVerify Email Address › Responses
A new session
refresh_tokensession_idtokenMinimal caller identity carried on a session payload.
Deliberately not the full /console/me shape
(kernel_http_v2_identity::contracts::me::MeResponse): /v2 feature
crates do not depend on one another (see crates/http/CLAUDE.md), so
this is a small, independent projection sized for what a session
response needs, not a shared type.
Get Password Status
Reports the email and password state of the caller's OWN account: whether it has a password, whether its address is verified, and the provider address a first password would attach to. Answers only about the caller, so it is not a lookup.
Get Password Status › Responses
The email and password state of the signed-in caller's own account, for the security settings screen.
email_verifiedhas_passwordemailThe address password login uses, or the provider address a first
password would be attached to. None when neither exists.
linkable_emailThe provider-proven address POST /auth/password/set would attach a
first password to. None once the account already has one.
Change Password
Changes the caller's password after confirming the current one, then signs every other session out. The caller's own session survives when current_session_id names it; omit that field to be signed out everywhere.
Change Password › Request Body
current_passwordnew_passwordcurrent_session_idThe caller's own session, spared when the change signs every other session out. Omit to be signed out everywhere, including here.
Change Password › Responses
The answer to every request that was accepted for processing and has nothing further to report.
Shared by forgot, reset, change, set and forgot-current rather than given
a per-operation shape, because a per-operation shape is how an oracle
gets built by accident: the moment one of these carries a field that
varies with the account, the response starts describing an account the
caller may not own. Modelled on
[PasswordSignupResponse], which exists for the same reason.
acceptedAlways true. A real typed field rather than an empty body so a
generated client has a success type to name.
Set Initial Password
Sets a first password for a caller who has only ever signed in through a provider. The address is taken from the linked provider, which already proved it, so no verification email is sent and none is accepted from the request. Refused when the account already has a password: changing one requires the current password.
Set Initial Password › Request Body
new_passwordSet Initial Password › Responses
The answer to every request that was accepted for processing and has nothing further to report.
Shared by forgot, reset, change, set and forgot-current rather than given
a per-operation shape, because a per-operation shape is how an oracle
gets built by accident: the moment one of these carries a field that
varies with the account, the response starts describing an account the
caller may not own. Modelled on
[PasswordSignupResponse], which exists for the same reason.
acceptedAlways true. A real typed field rather than an empty body so a
generated client has a success type to name.
Request Own Password Reset
Emails a password reset link to the caller's own address, for the "I forgot my current password" path on the security settings page. Takes no address: the account's own is used, so a signed-in caller cannot aim the mail at somebody else. The optional origin names the app URL the link should point back at (V-0853), under the same allowlist as requestPasswordReset's.
Request Own Password Reset › Request Body
originThe app URL the emailed reset link should point back at, e.g.
https://zaroz.cloud/console. Same shape and validation as
[ForgotPasswordRequest::origin]; a disallowed or malformed value is a
real 4xx refusal. Omitted, the link points at the platform dashboard.
Request Own Password Reset › Responses
The answer to every request that was accepted for processing and has nothing further to report.
Shared by forgot, reset, change, set and forgot-current rather than given
a per-operation shape, because a per-operation shape is how an oracle
gets built by accident: the moment one of these carries a field that
varies with the account, the response starts describing an account the
caller may not own. Modelled on
[PasswordSignupResponse], which exists for the same reason.
acceptedAlways true. A real typed field rather than an empty body so a
generated client has a success type to name.
Request Password Reset
Emails a password reset link. Answers identically -- same status, same body -- whether or not the address has an account, and whether or not it is even a valid address, so this operation cannot be used to discover which addresses are registered. Rate limited per address and per source IP; tripping either limit is also indistinguishable from success. origin, if given, must be the exact origin of a registered OAuth redirect URL or an active reseller's own hostname (ZC-1425), with at most one app mount path (/gaming, /console or /admin; V-0853), so the link lands on the app the request came from; a disallowed or malformed value IS a real 4xx refusal -- unlike the address itself, it is not account-existence information.
Request Password Reset › Request Body
emailoriginThe origin the emailed reset link should point back at (ZC-1425).
Same shape and validation as [PasswordSignupRequest::origin], including the
optional app mount path (https://zaroz.cloud/gaming).
Unlike the address above, a disallowed origin here is NOT hidden
behind the identical-response refusal: it is not account-existence
information, so forgot_handler refuses it with a real 4xx.
Request Password Reset › Responses
Always this, for every address
acceptedAlways true. A real typed field rather than an empty body so a
generated client has a success type to name.
Complete Password Reset
Sets a new password from an emailed reset token, or a first password when the account has none. The token is single use, expiring, and matched by hash; an unknown, expired and already-spent token are one indistinguishable refusal. Succeeding revokes every one of the account's sessions, including the caller's, because whoever holds them may be exactly who the reset is meant to lock out.
Complete Password Reset › Request Body
passwordtokenComplete Password Reset › Responses
The answer to every request that was accepted for processing and has nothing further to report.
Shared by forgot, reset, change, set and forgot-current rather than given
a per-operation shape, because a per-operation shape is how an oracle
gets built by accident: the moment one of these carries a field that
varies with the account, the response starts describing an account the
caller may not own. Modelled on
[PasswordSignupResponse], which exists for the same reason.
acceptedAlways true. A real typed field rather than an empty body so a
generated client has a success type to name.
List OAuth Providers
Lists the OAuth providers the sign-in page should offer, resolved from the host the request arrived on. A reseller host gets the platform providers it has not hidden plus its own; any other host gets the platform providers only. The same answer for every visitor on a host: it describes the site, not any account.
List OAuth Providers › Responses
iddisplay_nameA reseller-chosen label. None means the client should render the
provider's own conventional name from the id.
Complete OAuth Login
Redeems the single-use completion key the callback handed the client, opening a session. The same SessionResponse a password login's second leg returns, so a client stores one shape of session however it was obtained. An unknown, expired or already-redeemed key is one indistinguishable refusal.
Complete OAuth Login › Request Body
keyComplete OAuth Login › Responses
A new session
refresh_tokensession_idtokenMinimal caller identity carried on a session payload.
Deliberately not the full /console/me shape
(kernel_http_v2_identity::contracts::me::MeResponse): /v2 feature
crates do not depend on one another (see crates/http/CLAUDE.md), so
this is a small, independent projection sized for what a session
response needs, not a shared type.
List Own OAuth Accounts
Lists the providers linked to the CALLER'S OWN account. Takes no user id and accepts none: there is no shape of this request that asks about somebody else. Provider access and refresh tokens are stored but never published.
List Own OAuth Accounts › Responses
has_avatarWhether this provider has a stored avatar that could be activated.
The URL itself is not published: it is a provider-side resource the
kernel imports through AvatarImportService, not something a client
should be fetching directly.
provider_idemailThe address the provider reports for this account, when it reports one. Only ever the caller's own.
last_used_atStart OAuth Link
Starts a provider link for the signed-in caller, so the callback attaches the provider to the account they are already using instead of creating a second one. The user is taken from the session, never from the request; reseller_id is ignored here because an existing account already has its attribution.
path Parameters
providerquery Parameters
redirect_toWhere the provider callback should send the browser once it is done.
Validated against the registered redirect-URL allowlist by
OAuthLoginService, so an arbitrary value is refused rather than
honoured; that check is what stops this being an open redirect and a
token-exfiltration path.
Where the provider callback should send the browser once it is done.
Validated against the registered redirect-URL allowlist by
OAuthLoginService, so an arbitrary value is refused rather than
honoured; that check is what stops this being an open redirect and a
token-exfiltration path.
referral_codeA referral link's slug, if the visitor arrived through one (ZC-1032,
owner ruling 2026-09-17: attribution happens at signup). Only
meaningful on login, for the identical reason reseller_id is:
link_start_handler destructures this struct with .. and never
reads it. Never validated here, and never changes this route's
response -- attribution is resolved by callback_login only if and
when it creates a brand new account.
A referral link's slug, if the visitor arrived through one (ZC-1032,
owner ruling 2026-09-17: attribution happens at signup). Only
meaningful on login, for the identical reason reseller_id is:
link_start_handler destructures this struct with .. and never
reads it. Never validated here, and never changes this route's
response -- attribution is resolved by callback_login only if and
when it creates a brand new account.
reseller_idThe reseller whose storefront the visitor started from, when it is not the platform's own. Only meaningful on login: a link flow acts on an account that already exists and already has its attribution.
The reseller whose storefront the visitor started from, when it is not the platform's own. Only meaningful on login: a link flow acts on an account that already exists and already has its attribution.
Start OAuth Link › Responses
Where to send the visitor next.
redirect_toStart OAuth Login
Starts an OAuth login and returns the provider's authorize URL for the client to send the visitor to. redirect_to must be one of the registered redirect URLs; an unregistered value is refused rather than honoured, which is what keeps this from being an open redirect. referral_code, if given, is attributed atomically only if and when the provider's callback actually creates a new account -- an unknown, expired, or self-owned code never blocks or slows down login, and never changes this response.
path Parameters
providerquery Parameters
redirect_toWhere the provider callback should send the browser once it is done.
Validated against the registered redirect-URL allowlist by
OAuthLoginService, so an arbitrary value is refused rather than
honoured; that check is what stops this being an open redirect and a
token-exfiltration path.
Where the provider callback should send the browser once it is done.
Validated against the registered redirect-URL allowlist by
OAuthLoginService, so an arbitrary value is refused rather than
honoured; that check is what stops this being an open redirect and a
token-exfiltration path.
referral_codeA referral link's slug, if the visitor arrived through one (ZC-1032,
owner ruling 2026-09-17: attribution happens at signup). Only
meaningful on login, for the identical reason reseller_id is:
link_start_handler destructures this struct with .. and never
reads it. Never validated here, and never changes this route's
response -- attribution is resolved by callback_login only if and
when it creates a brand new account.
A referral link's slug, if the visitor arrived through one (ZC-1032,
owner ruling 2026-09-17: attribution happens at signup). Only
meaningful on login, for the identical reason reseller_id is:
link_start_handler destructures this struct with .. and never
reads it. Never validated here, and never changes this route's
response -- attribution is resolved by callback_login only if and
when it creates a brand new account.
reseller_idThe reseller whose storefront the visitor started from, when it is not the platform's own. Only meaningful on login: a link flow acts on an account that already exists and already has its attribution.
The reseller whose storefront the visitor started from, when it is not the platform's own. Only meaningful on login: a link flow acts on an account that already exists and already has its attribution.
Start OAuth Login › Responses
Where to send the visitor next.
redirect_toOAuth Login Callback
The URL the provider returns the visitor to. Exchanges the authorization code, resolves or creates the account, and sends the browser back to the client URL the flow started with, carrying a single-use completion key to redeem at POST /auth/oauth/complete. A recoverable failure redirects to that same already validated URL with an error code (email_conflict, email_required, reseller_paused, login_failed) instead of stranding the visitor on an API host. Answers 307 to a browser; a caller that asks for application/json and does not accept HTML gets the same target as a 200 body instead, which is how a non-browser client can observe the outcome without following it.
path Parameters
providerquery Parameters
codestateThe CSRF value minted when the flow started. Checked against the stored authorization session before anything is exchanged.
The CSRF value minted when the flow started. Checked against the stored authorization session before anything is exchanged.
OAuth Login Callback › Responses
Where the browser would have been sent, for a JSON caller
redirect_toGet Sudo Status
Reports whether the caller's own session is currently elevated and until when, which elevation methods this API can verify, and which the caller has enrolled. Never elevation-gated: this is how a caller finds out whether it needs to elevate. Answers only about the bearer token on this request.
Get Sudo Status › Responses
Whether the caller's own session is elevated, and what it could elevate with (ZC-0412).
This route existing at all is the fix for a real defect: admin read the
missing route's 404 as "not elevated", so its gate rendered over sessions
that had been elevated the whole time.
available_methodsThe elevation methods this surface can actually verify.
Was PIN-only before ZC-0716: /v2 ported no challenge-issuing route,
so a client offered webauthn or totp here would have had no way
to produce a secret for either. Now equal to
AdminElevationService::available_methods() (every module registered
in the deployment), because ZC-0716 added the missing enroll/verify
challenge routes for both. See controllers::sudo::V2_ELEVATION_METHODS.
enrolled_methodsEvery method the caller has actually enrolled, unfiltered, which
can include a method available_methods does not list.
That asymmetry is intentional rather than an oversight: a user who
enrolled TOTP while /v1 existed still has that credential, and it is
what makes POST /auth/sudo/setup demand an already-elevated session
before adding a second method (AdminElevationService::enroll). A
client that saw only the filtered list would have no way to explain
that refusal.
is_elevatedWhether the bearer token on THIS request is elevated right now.
Equal to elevated_until.is_some(), and published as its own field so
a client never has to compare a timestamp against its own clock to
answer the only question it usually asks. elevated_until is already
filtered against server time by utils::elevation::elevated_until, so
an expired window reads as false with a null deadline rather than
as a past date.
elevated_untilWhen the current elevation lapses, or null when the caller is not
elevated.
Set Up Sudo PIN
Enrolls the caller in PIN-based sudo elevation and immediately elevates their session. The caller must already be signed in as a real user (not a restricted API token). Adding a second method to an account that already has one requires an already-elevated session. V2 ships PIN only; see this module's doc comment for what /v1's elevation surface is not ported.
Set Up Sudo PIN › Request Body
pinSix ASCII digits. Format is validated by AdminElevationService.
Set Up Sudo PIN › Responses
The caller's newly elevated session
elevated_untiltokenElevate Sudo Session
Verifies the caller's enrolled PIN and returns a newly signed access token carrying a fresh elevation window. The presented token is not revoked and stays valid, un-elevated, until its own expiry; the refresh token is not rotated. A wrong PIN and an account with no PIN enrolled are answered identically, so this cannot be used to discover whether an account has a second factor. Attempts are counted per user and shared with the setup route: five per fifteen minutes, cleared on success.
Elevate Sudo Session › Request Body
pinThe six ASCII digits the caller enrolled. A wrong PIN and an account
with no PIN enrolled are answered identically; see
controllers::sudo::elevate_error_response.
Elevate Sudo Session › Responses
The caller's newly elevated session
elevated_untiltokenDisable Sudo Method
Removes the caller's own enrollment in an elevation method. Requires the caller's session to already be elevated; unlike enrolling or verifying, this is never reachable from an un-elevated session.
Disable Sudo Method › Request Body
methodWhich admin-elevation module a credential belongs to, or a verification
attempt is checked against. AdminElevationModule gives each method
(PIN, WebAuthn, TOTP) a home behind this same enum without touching every
caller that only cares about routing by name. Serializable so callers of
/auth/sudo/* can pick a method by name instead of the HTTP layer
assuming PIN.
Disable Sudo Method › Responses
Method disabled
Exit Sudo Session
Drops the caller's own sudo elevation early, before it naturally expires.
Exit Sudo Session › Responses
The caller's newly de-elevated token
tokenBegin TOTP Enrollment
Generates a TOTP shared secret (QR code URI + manual-entry string) for the caller to confirm with a code from their authenticator app via POST /auth/sudo/totp/enroll. Adding a second method to an account that already has one requires an already-elevated session, the same gate POST /auth/sudo/setup enforces.
Begin TOTP Enrollment › Responses
A pending TOTP enrollment, valid for 5 minutes
accountissuerotpauth_urlotpauth:// URI encoding the same secret, issuer and account name,
for a QR code.
secretBase32-encoded shared secret, for manual entry.
Confirm TOTP Enrollment
Confirms a pending TOTP enrollment with a code from the authenticator app and immediately elevates the caller's session. The pending secret from /auth/sudo/totp/enroll-options must still be within its 5-minute window; an expired or already-consumed-then-abandoned enrollment, or a wrong code, are both answered as an invalid-field-value with a human-readable reason, never as success.
Confirm TOTP Enrollment › Request Body
codeThe current 6-digit code from the authenticator app. Verified against
the secret POST /auth/sudo/totp/enroll-options generated for this
caller, which must still be pending (ENROLL_TTL_SECONDS, 5 minutes)
-- see TotpElevationModule::read_pending_secret.
Confirm TOTP Enrollment › Responses
The caller's newly elevated session
elevated_untiltokenVerify TOTP Code
Verifies a code from the caller's enrolled authenticator app and returns a newly signed access token carrying a fresh elevation window. The presented token is not revoked; the refresh token is not rotated. A wrong code, an already-used code and an account with no TOTP enrolled are answered identically, so this cannot be used to discover whether an account has TOTP enrolled. Attempts are counted per user and shared with the PIN and passkey routes: five per fifteen minutes, cleared on success.
Verify TOTP Code › Request Body
codeThe current 6-digit code from the enrolled authenticator app. A wrong
code, an already-used code and an account with no TOTP enrolled are
answered identically; see controllers::sudo::elevate_error_response.
Verify TOTP Code › Responses
The caller's newly elevated session
elevated_untiltokenBegin Passkey Registration
Generates a WebAuthn passkey registration challenge for the caller to pass to navigator.credentials.create, then confirm via POST /auth/sudo/webauthn/register. Adding a second method to an account that already has one requires an already-elevated session.
Begin Passkey Registration › Responses
WebAuthn creation options
Confirm Passkey Registration
Confirms a passkey registration ceremony against the challenge /auth/sudo/webauthn/register-options issued, and immediately elevates the caller's session. The challenge is single-use (consumed on this call, whether it succeeds or not) and expires after 5 minutes; a stale, replayed or malformed ceremony response is answered as an invalid-field-value, never as success.
Confirm Passkey Registration › Request Body
credentialArbitrary JSON.
Confirm Passkey Registration › Responses
The caller's newly elevated session
elevated_untiltokenBegin Passkey Authentication
Generates a WebAuthn passkey authentication challenge for the caller to pass to navigator.credentials.get, then confirm via POST /auth/sudo/webauthn/authenticate. An account with no passkey enrolled is answered with an invalid-field-value naming that, which is safe here (unlike the collapsed PIN/elevate error) because this route only ever answers about the caller's own account.
Begin Passkey Authentication › Responses
WebAuthn request options
Confirm Passkey Authentication
Verifies a passkey authentication ceremony against the challenge /auth/sudo/webauthn/authenticate-options issued, and elevates the caller's session on success. The challenge is single-use and expires after 5 minutes. No enrollment, a failed assertion, and a stale or malformed response are all answered identically, so this cannot be used to discover whether an account has a passkey enrolled.
Confirm Passkey Authentication › Request Body
credentialThe browser's PublicKeyCredential assertion response (from
navigator.credentials.get).
Confirm Passkey Authentication › Responses
The caller's newly elevated session
elevated_untiltoken