Skip to content

Depot consoleapi

Registers the console routes.

This page is generated from the Depot route table. Every operation carries the permission it needs and the anchor an agent can link to.

POSTpublic facehosteditmspremote

Source: [object Object]

emailChangeRequest mails a confirmation code to an address the caller wants to move their account to.

Required, sent as application/json.

FieldTypeDescription
newEmailrequiredstring
StatusBodyDescription
202application/json, objectAccepted.
400application/problem+json, ProblemDetailsThe request body is not acceptable.
Terminal window
# Sign in first. outpost_console is a session cookie, so send it with every request.
curl -X POST 'https://depot.example.com/api/v1/console/email/change' \
-H 'X-CSRF-Token: <csrf-token>' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-b 'outpost_console=<console-session>' \
--data '{
"newEmail": "string"
}'

POSTpublic facehosteditmspremote

Source: [object Object]

emailChangeConfirm spends the code and moves the account.

Unlike a password change, nothing here is revoked. An email address is not a credential: knowing it does not sign anybody in, and the person who just proved control of the new mailbox is the person holding this session. Ending their sessions would sign them out of the console in the middle of a settings page for no gain, and it would not inconvenience an attacker who by definition still holds the password.

Required, sent as application/json.

FieldTypeDescription
coderequiredstring
StatusBodyDescription
204NoneSuccess, with no body.
400application/problem+json, ProblemDetailsThe request body is not acceptable.
Terminal window
# Sign in first. outpost_console is a session cookie, so send it with every request.
curl -X POST 'https://depot.example.com/api/v1/console/email/confirm' \
-H 'X-CSRF-Token: <csrf-token>' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-b 'outpost_console=<console-session>' \
--data '{
"code": "string"
}'

POSTNo permission neededpublic facehosteditmspremote

Source: [object Object]

handoff exchanges a provisioning handoff token for a console session.

This is what lets a customer who just finished signup land in their own dashboard without signing in to something they have not set a password for yet. It is a DepotInvite row underneath, with a five-minute life instead of a week: single use, stored only as a hash, and scoped to the tenant whose database it was written to, so the tenant router has already decided which account a token can possibly refer to before this handler runs.

The account arrives “invited” and is activated here. That is the whole reason this is not just acceptInvite: the session middleware rejects a non-active user, so a handoff that left the status alone would mint a session that the very next request throws away. Activating without a credential is deliberate and safe in both directions - password login needs a stored hash and finds none, and the first-run wizard is where one gets set.

Required, sent as application/json.

FieldTypeDescription
tokenrequiredstring
StatusBodyDescription
200application/json, objectSuccess.
400application/problem+json, ProblemDetailsThe request body is not acceptable.
Terminal window
# This operation is public. It takes no credentials.
curl -X POST 'https://depot.example.com/api/v1/console/handoff' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
--data '{
"token": "string"
}'

POSTNo permission neededpublic facehosteditmspremote

Source: [object Object]

login verifies a password and mints a session.

Every failure returns the same 401 with the same body. Not for tidiness: distinguishing “no such account” from “wrong password” hands an attacker a membership oracle for the customer’s staff list, and distinguishing “locked” tells them their guessing is working.

Required, sent as application/json.

FieldTypeDescription
emailrequiredstring
passwordrequiredstring
StatusBodyDescription
200application/json, objectSuccess.
400application/problem+json, ProblemDetailsThe request body is not acceptable.
Terminal window
# This operation is public. It takes no credentials.
curl -X POST 'https://depot.example.com/api/v1/console/login' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
--data '{
"email": "string",
"password": "string"
}'

POSTNo permission neededpublic facehosteditmspremote

Source: [object Object]

Break glass

Required, sent as application/json.

FieldTypeDescription
coderequiredstring
emailrequiredstring
passwordrequiredstring
recoveryCoderequiredstring
StatusBodyDescription
200application/json, MeResponseSuccess.
400application/problem+json, ProblemDetailsThe request body is not acceptable.

200 response body

FieldTypeDescription
breakGlassrequiredboolean
breakGlassExpiresAtstring (date-time) or null
displayNamerequiredstring
emailrequiredstring
hasPasswordrequiredboolean
mfaEnrolmentDeadlinestring (date-time) or null
passwordChangeReasonstring or null
permissionsrequiredarray of string
phonerequiredstring
phoneVerifiedrequiredboolean
pioneerAutoApproverequiredboolean
publicIdrequiredstring
requiredActionstring or null
rolesrequiredarray of string
Terminal window
# This operation is public. It takes no credentials.
curl -X POST 'https://depot.example.com/api/v1/console/login/break-glass' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
--data '{
"code": "string",
"email": "string",
"password": "string",
"recoveryCode": "string"
}'

POSTNo permission neededpublic facehosteditmspremote

Source: [object Object]

loginCodeSend mints and delivers a one-time code, and ALWAYS answers 202 with the same body.

Unknown address, disabled account, locked-out account, inside the resend cooldown, over the daily send cap, and a code genuinely on its way all produce byte-identical 202s. Any distinction is a membership oracle for the customer’s staff list, available to anybody who can reach the login page.

This is not about latency. Sending inline would make a real address answer in however long SMTP takes - hundreds of milliseconds to seconds - and an unknown address answer instantly, which hands straight back the oracle the identical body was protecting. A timing difference is a difference. The goroutine takes a BACKGROUND context with its own timeout rather than the request’s, because the request context is cancelled the moment this handler returns, which is immediately.

The cost is that a delivery in flight when the process stops is lost. That is acceptable: the user’s remedy is to press the button again, the cooldown is sixty seconds, and the alternative is holding a request open long enough to be timed.

Required, sent as application/json.

FieldTypeDescription
emailrequiredstring
StatusBodyDescription
202application/json, objectAccepted.
400application/problem+json, ProblemDetailsThe request body is not acceptable.
Terminal window
# This operation is public. It takes no credentials.
curl -X POST 'https://depot.example.com/api/v1/console/login/code' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
--data '{
"email": "string"
}'

POSTNo permission neededpublic facehosteditmspremote

Source: [object Object]

loginCodeVerify spends a code and signs the account in.

Every failure is ONE identical 401: unknown address, disabled account, locked out, no live code, expired code, exhausted attempts, and simply the wrong digits. Distinguishing any of them tells an attacker which addresses exist or how close they are.

It goes through startSession like every other door, and startSession challenges a confirmed enrolment. That is not a courtesy: the code proves control of a mailbox, and an attacker who has the mailbox is exactly the attacker the second factor is for. TestCodeLoginStillRequiresTotpWhenEnrolled pins it, and TestEverySignInPathGoesThroughStartSession pins the mechanism.

Required, sent as application/json.

FieldTypeDescription
coderequiredstring
emailrequiredstring
StatusBodyDescription
200application/json, objectSuccess.
400application/problem+json, ProblemDetailsThe request body is not acceptable.
Terminal window
# This operation is public. It takes no credentials.
curl -X POST 'https://depot.example.com/api/v1/console/login/code/verify' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
--data '{
"code": "string",
"email": "string"
}'

GETNo permission neededpublic facehosteditmspremoteIdempotent

Source: [object Object]

passwordlessAvailable answers the sign-in page’s one question: may it offer “Sign in with a passkey” here? A fact about the organisation and the host, identical for every caller, so it says nothing about any account.

StatusBodyDescription
200application/json, objectSuccess.
Terminal window
# This operation is public. It takes no credentials.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X GET 'https://depot.example.com/api/v1/console/login/passkey' \
-H 'Accept: application/json'

POSTNo permission neededpublic facehosteditmspremote

Source: [object Object]

Begin passwordless

StatusBodyDescription
200application/json, CeremonyResponseSuccess.

200 response body

FieldTypeDescription
ceremonyIdrequiredstring
publicKeyrequiredany
Terminal window
# This operation is public. It takes no credentials.
curl -X POST 'https://depot.example.com/api/v1/console/login/passkey/begin' \
-H 'Accept: application/json'

POSTNo permission neededpublic facehosteditmspremote

Source: [object Object]

finishPasswordless is the passwordless door.

It is a sign-in like login and it keeps every rule login keeps, in the same order and for the same reasons: one answer for every failure before the account is proven, the account’s status and lockout, the network and hour rules after the credential has verified, the failure counter on a bad assertion against a known account, and startSession as the only way a session is minted.

No second factor. The passkey was verified with user verification required (a PIN or a biometric on the authenticator), so it is possession and knowledge or inherence at once; asking for TOTP on top would make the door pointless and is not what the organisation turned on.

No password composition check. That needs the plaintext, which this door never sees, and a password that no longer meets the policy is not in use here; it is caught at the password door the next time it is typed, which is where it can do harm. AN EXPIRED PASSWORD IS STILL ENFORCED, exactly as the code and handoff doors enforce it: the password is still a working credential for this account, and its expiry is about the credential, not the door.

Required, sent as application/json.

FieldTypeDescription
ceremonyIdrequiredstring
credentialrequiredany

Arbitrary JSON.

StatusBodyDescription
200application/json, MeResponseSuccess.
400application/problem+json, ProblemDetailsThe request body is not acceptable.

200 response body

FieldTypeDescription
breakGlassrequiredboolean
breakGlassExpiresAtstring (date-time) or null
displayNamerequiredstring
emailrequiredstring
hasPasswordrequiredboolean
mfaEnrolmentDeadlinestring (date-time) or null
passwordChangeReasonstring or null
permissionsrequiredarray of string
phonerequiredstring
phoneVerifiedrequiredboolean
pioneerAutoApproverequiredboolean
publicIdrequiredstring
requiredActionstring or null
rolesrequiredarray of string
Terminal window
# This operation is public. It takes no credentials.
curl -X POST 'https://depot.example.com/api/v1/console/login/passkey/finish' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
--data '{
"ceremonyId": "string",
"credential": "string"
}'

POSTNo permission neededpublic facehosteditmspremote

Source: [object Object]

loginStart reports which sign-in methods this Depot offers.

IT MUST NEVER TOUCH THE STORE, and TestLoginStartNeverTouchesTheStore fails the day it does.

The obvious design looks the address up and answers [“password”] when it has one and [“code”] when it does not. That is what several large providers do, and it leaks exactly one bit: whether an address has a password. The bit sounds harmless and is not, because the case that matters here is the account with NO credential - the abandoned first-run owner - and that account would be the one answer that is distinguishable from “never heard of this address”.

Worse, a per-address answer is differentiable by TIMING however carefully the branches are written: one path runs a blind-index lookup and the other does not. A response computed from process configuration cannot leak what it never read, and that is the entire argument. The cost is that an account with no password is shown a password field once, which is a moment of confusion against a permanent oracle.

SSO will route on the DOMAIN, and a domain is a per-domain fact, not a per-account one: “everybody at acme.example signs in with Okta” tells an attacker nothing about whether any particular person has an account. So the field is accepted, sanity-checked, and today ignored. Rejecting a value that is not an address keeps the SPA honest about what it is sending.

Required, sent as application/json.

FieldTypeDescription
emailrequiredstring
StatusBodyDescription
200application/json, LoginStartResponseSuccess.
400application/problem+json, ProblemDetailsThe request body is not acceptable.

200 response body

FieldTypeDescription
methodsrequiredarray of string
Terminal window
# This operation is public. It takes no credentials.
curl -X POST 'https://depot.example.com/api/v1/console/login/start' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
--data '{
"email": "string"
}'

POSTNo permission neededpublic facehosteditmspremote

Source: [object Object]

logout revokes the current session.

It is idempotent and always answers 204: a client signing out of a session that is already gone has got what it wanted.

StatusBodyDescription
204NoneSuccess, with no body.
Terminal window
# This operation is public. It takes no credentials.
curl -X POST 'https://depot.example.com/api/v1/console/logout'

GETpublic facehosteditmspremoteIdempotent

Source: [object Object]

me reports the signed-in user, their roles, and their effective permissions, which is what the SPA needs to decide what to render.

NO CLIENT DATABASE ON THE CONTEXT IS A 404, NOT A 401, and me is the only route in this package that draws that distinction. The SPA’s consoleAuthGuard reads the two apart: a 404 means this face mounts no sign-in surface and the route is admitted, a 401 means signed out and the browser is redirected to /login. A face that has no client database has no account to sign in to either, so answering 401 would park it on a login form whose every POST is refused for the same reason - a dead loop with nothing behind it. That is the pre-bootstrap MSP face, which has no self client yet, and a dev run against a control database where nobody has marked one.

requireUser and signedInUser deliberately keep their 401 for this case. Only me is read as a statement about the FACE rather than about the caller.

StatusBodyDescription
200application/json, MeResponseSuccess.

200 response body

FieldTypeDescription
breakGlassrequiredboolean
breakGlassExpiresAtstring (date-time) or null
displayNamerequiredstring
emailrequiredstring
hasPasswordrequiredboolean
mfaEnrolmentDeadlinestring (date-time) or null
passwordChangeReasonstring or null
permissionsrequiredarray of string
phonerequiredstring
phoneVerifiedrequiredboolean
pioneerAutoApproverequiredboolean
publicIdrequiredstring
requiredActionstring or null
rolesrequiredarray of string
Terminal window
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X GET 'https://depot.example.com/api/v1/console/me' \
-H 'X-CSRF-Token: <csrf-token>' \
-H 'Accept: application/json' \
-b 'outpost_console=<console-session>'

PATCHpublic facehosteditmspremote

Source: [object Object]

Update me

Required, sent as application/json.

FieldTypeDescription
displayNamerequiredstring
StatusBodyDescription
204NoneSuccess, with no body.
400application/problem+json, ProblemDetailsThe request body is not acceptable.
Terminal window
# Sign in first. outpost_console is a session cookie, so send it with every request.
curl -X PATCH 'https://depot.example.com/api/v1/console/me' \
-H 'X-CSRF-Token: <csrf-token>' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-b 'outpost_console=<console-session>' \
--data '{
"displayName": "string"
}'

GETpublic facehosteditmspremoteIdempotent

Source: [object Object]

myActivity lists what has happened to the signed-in account, newest first.

OWN ROWS ONLY BY CONSTRUCTION, not by filtering afterwards: the target is the caller’s own public id and the caller cannot influence it, so there is no parameter to get wrong and no way to ask for somebody else’s history. That is what lets this route sit outside the audit:read permission the full chain needs.

StatusBodyDescription
200application/json, objectSuccess.
Terminal window
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X GET 'https://depot.example.com/api/v1/console/me/activity' \
-H 'X-CSRF-Token: <csrf-token>' \
-H 'Accept: application/json' \
-b 'outpost_console=<console-session>'

POSTNo permission neededpublic facehosteditmspremote

Source: [object Object]

mfa answers the second-factor challenge and promotes the session.

Like the password path, every failure is the same 401: which of “wrong code”, “already used”, and “expired challenge” it was tells an attacker how close they are, and tells a legitimate user nothing they can act on beyond “try again”.

Required, sent as application/json.

FieldTypeDescription
ceremonyIdrequiredstring
coderequiredstring
credentialrequiredany

Arbitrary JSON.

recoveryCoderequiredstring
rememberDevicerequiredboolean
StatusBodyDescription
200application/json, MeResponseSuccess.
400application/problem+json, ProblemDetailsThe request body is not acceptable.

200 response body

FieldTypeDescription
breakGlassrequiredboolean
breakGlassExpiresAtstring (date-time) or null
displayNamerequiredstring
emailrequiredstring
hasPasswordrequiredboolean
mfaEnrolmentDeadlinestring (date-time) or null
passwordChangeReasonstring or null
permissionsrequiredarray of string
phonerequiredstring
phoneVerifiedrequiredboolean
pioneerAutoApproverequiredboolean
publicIdrequiredstring
requiredActionstring or null
rolesrequiredarray of string
Terminal window
# This operation is public. It takes no credentials.
curl -X POST 'https://depot.example.com/api/v1/console/mfa' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
--data '{
"ceremonyId": "string",
"code": "string",
"credential": "string",
"recoveryCode": "string",
"rememberDevice": false
}'

POSTNo permission neededpublic facehosteditmspremote

Source: [object Object]

beginMFAPasskey starts the passkey challenge for a session that has proven its first factor.

The allow list is the account’s passkeys on this host.

StatusBodyDescription
200application/json, CeremonyResponseSuccess.

200 response body

FieldTypeDescription
ceremonyIdrequiredstring
publicKeyrequiredany
Terminal window
# This operation is public. It takes no credentials.
curl -X POST 'https://depot.example.com/api/v1/console/mfa/passkey' \
-H 'Accept: application/json'

GETpublic facehosteditmspremoteIdempotent

Source: [object Object]

List my passkeys

StatusBodyDescription
200application/json, PasskeyListResponseSuccess.

200 response body

FieldTypeDescription
acceptedrequiredboolean
availablerequiredboolean
maxrequiredinteger
passkeysrequiredarray of PasskeyView
passkeys[].backupStaterequiredboolean
passkeys[].createdAtrequiredstring (date-time)
passkeys[].lastUsedAtstring (date-time) or null
passkeys[].nicknamestring or null
passkeys[].publicIdrequiredstring
passkeys[].rpIdrequiredstring
passkeys[].transportsrequiredarray of string
passkeys[].usableHererequiredboolean
passwordlessrequiredboolean
rpIdrequiredstring
Terminal window
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X GET 'https://depot.example.com/api/v1/console/passkeys' \
-H 'X-CSRF-Token: <csrf-token>' \
-H 'Accept: application/json' \
-b 'outpost_console=<console-session>'

PATCH /api/v1/console/passkeys/{passkeyId}

Section titled “PATCH /api/v1/console/passkeys/{passkeyId}”

PATCHpublic facehosteditmspremote

Source: [object Object]

Rename my passkey

NameInTypeRequiredDescription
passkeyIdpathstringYes

Required, sent as application/json.

FieldTypeDescription
nicknamerequiredstring
StatusBodyDescription
200application/json, PasskeyViewSuccess.
400application/problem+json, ProblemDetailsThe request body is not acceptable.
404application/problem+json, ProblemDetailsNo resource matches that id.

200 response body

FieldTypeDescription
backupStaterequiredboolean
createdAtrequiredstring (date-time)
lastUsedAtstring (date-time) or null
nicknamestring or null
publicIdrequiredstring
rpIdrequiredstring
transportsrequiredarray of string
usableHererequiredboolean
Terminal window
# Sign in first. outpost_console is a session cookie, so send it with every request.
curl -X PATCH 'https://depot.example.com/api/v1/console/passkeys/<passkeyId>' \
-H 'X-CSRF-Token: <csrf-token>' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-b 'outpost_console=<console-session>' \
--data '{
"nickname": "string"
}'

DELETE /api/v1/console/passkeys/{passkeyId}

Section titled “DELETE /api/v1/console/passkeys/{passkeyId}”

DELETEpublic facehosteditmspremoteIdempotent

Source: [object Object]

Remove my passkey

NameInTypeRequiredDescription
passkeyIdpathstringYes

Required, sent as application/json.

The body is any.

StatusBodyDescription
204NoneSuccess, with no body.
400application/problem+json, ProblemDetailsThe request body is not acceptable.
404application/problem+json, ProblemDetailsNo resource matches that id.
Terminal window
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X DELETE 'https://depot.example.com/api/v1/console/passkeys/<passkeyId>' \
-H 'X-CSRF-Token: <csrf-token>' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-b 'outpost_console=<console-session>' \
--data '"string"'

POST /api/v1/console/passkeys/registration/begin

Section titled “POST /api/v1/console/passkeys/registration/begin”

POSTpublic facehosteditmspremote

Source: [object Object]

Begin passkey registration

Required, sent as application/json.

The body is any.

StatusBodyDescription
200application/json, CeremonyResponseSuccess.
400application/problem+json, ProblemDetailsThe request body is not acceptable.

200 response body

FieldTypeDescription
ceremonyIdrequiredstring
publicKeyrequiredany
Terminal window
# Sign in first. outpost_console is a session cookie, so send it with every request.
curl -X POST 'https://depot.example.com/api/v1/console/passkeys/registration/begin' \
-H 'X-CSRF-Token: <csrf-token>' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-b 'outpost_console=<console-session>' \
--data '"string"'

POST /api/v1/console/passkeys/registration/finish

Section titled “POST /api/v1/console/passkeys/registration/finish”

POSTpublic facehosteditmspremote

Source: [object Object]

Finish passkey registration

Required, sent as application/json.

FieldTypeDescription
ceremonyIdrequiredstring
credentialrequiredany

Arbitrary JSON.

nicknamerequiredstring
StatusBodyDescription
201application/json, PasskeyViewCreated.
400application/problem+json, ProblemDetailsThe request body is not acceptable.

201 response body

FieldTypeDescription
backupStaterequiredboolean
createdAtrequiredstring (date-time)
lastUsedAtstring (date-time) or null
nicknamestring or null
publicIdrequiredstring
rpIdrequiredstring
transportsrequiredarray of string
usableHererequiredboolean
Terminal window
# Sign in first. outpost_console is a session cookie, so send it with every request.
curl -X POST 'https://depot.example.com/api/v1/console/passkeys/registration/finish' \
-H 'X-CSRF-Token: <csrf-token>' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-b 'outpost_console=<console-session>' \
--data '{
"ceremonyId": "string",
"credential": "string",
"nickname": "string"
}'

POSTpublic facehosteditmspremote

Source: [object Object]

setPassword sets or replaces the signed-in user’s own password.

Step one of the first-run wizard, and the ordinary change-my-password path afterwards. The account that arrived through a handoff has no credential at all, which is why the FIRST set does not ask for the current one: there would be nothing to ask for.

WHEN THE CURRENT PASSWORD IS ASKED FOR, AND WHEN IT IS NOT

Section titled “WHEN THE CURRENT PASSWORD IS ASKED FOR, AND WHEN IT IS NOT”

An ordinary change asks. The session is not enough on its own: a session is what an attacker has after a stolen laptop, a borrowed unlocked screen, or an XSS that never needed the cookie’s value, and re-proving the password is what stops all three from silently becoming permanent, because changing the password is precisely what locks the real owner out. Same reasoning as totpDisable asking for a current code.

TWO CASES ARE EXEMPT, and both are exempt because there is nothing to ask:

  • The account has no stored credential. The abandoned first-run owner and the handoff arrival both land here, and demanding a password they have never had would make them unrecoverable.
  • The session was minted by a one-time CODE. That is the forgot-password flow, and it is the whole reason the code flow exists: the person cannot remember the password, which is why they proved control of the mailbox instead. Proof of the mailbox is a STRONGER claim on the account than the password is, since the mailbox is where a new code goes. Refusing here would leave a locked-out user with the reset flow ending one step short of resetting anything, which is what TestForgotPasswordIsCodeLoginThenSetPassword pins.

The exempt branches still burn the same PBKDF2 work, so the two do not answer at measurably different speeds and the response does not say whether the account has a password.

Every other session for the account is revoked and the caller gets a fresh one. Changing a password that leaves an attacker’s session alive has not changed anything, and issuing new cookies here is what keeps the person who made the change signed in.

A REFUSAL IS RECORDED, AND ONE KIND OF REFUSAL IS MAILED

Section titled “A REFUSAL IS RECORDED, AND ONE KIND OF REFUSAL IS MAILED”

Every branch that says no writes verbPasswordChangeFailed with a reason. Before that, a person holding a stolen session could sit on this form probing the password and leave the chain looking exactly as it does when nothing happened at all.

Only bad_current_password raises a notice. A weak or reused choice is the account’s own owner picking badly - they are already reading the refusal on screen, and mailing it as a security alert is how a person learns to skip the one message in the list that means somebody else is typing.

Required, sent as application/json.

FieldTypeDescription
currentPasswordrequiredstring
passwordrequiredstring
StatusBodyDescription
200application/json, objectSuccess.
204NoneSuccess, with no body.
400NoneThe request was refused.
Terminal window
# Sign in first. outpost_console is a session cookie, so send it with every request.
curl -X POST 'https://depot.example.com/api/v1/console/password' \
-H 'X-CSRF-Token: <csrf-token>' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-b 'outpost_console=<console-session>' \
--data '{
"currentPassword": "string",
"password": "string"
}'

GETpublic facehosteditmspremoteIdempotent

Source: [object Object]

passwordRulesRead answers GET /api/v1/console/password/rules for the caller’s own account.

Session required and nothing else, exactly like the rest of the account security surface: everything it reports is about the caller, and a technician who may not manage anybody still has to be able to change their own password.

It is a separate round trip from the page’s other reads rather than a field on /me because the rules are per-tenant policy that can move under a running session, and folding them into the identity document would cache them for as long as somebody stays signed in.

StatusBodyDescription
200application/json, PasswordRulesResponseSuccess.

200 response body

FieldTypeDescription
changedAtstring (date-time) or null
hasPasswordrequiredboolean
historyCountrequiredinteger
maxAgeDaysrequiredinteger
minLengthrequiredinteger
requireDigitrequiredboolean
requireSymbolrequiredboolean
requireUpperrequiredboolean
Terminal window
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X GET 'https://depot.example.com/api/v1/console/password/rules' \
-H 'X-CSRF-Token: <csrf-token>' \
-H 'Accept: application/json' \
-b 'outpost_console=<console-session>'

DELETEpublic facehosteditmspremoteIdempotent

Source: [object Object]

Phone remove

Required, sent as application/json.

FieldTypeDescription
coderequiredstring
phonerequiredstring
StatusBodyDescription
200application/json, objectSuccess.
202application/json, objectAccepted.
400application/problem+json, ProblemDetailsThe request body is not acceptable.
Terminal window
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X DELETE 'https://depot.example.com/api/v1/console/phone' \
-H 'X-CSRF-Token: <csrf-token>' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-b 'outpost_console=<console-session>' \
--data '{
"code": "string",
"phone": "string"
}'

POSTpublic facehosteditmspremote

Source: [object Object]

Phone confirm

Required, sent as application/json.

FieldTypeDescription
coderequiredstring
phonerequiredstring
StatusBodyDescription
200application/json, objectSuccess.
202application/json, objectAccepted.
400application/problem+json, ProblemDetailsThe request body is not acceptable.
Terminal window
# Sign in first. outpost_console is a session cookie, so send it with every request.
curl -X POST 'https://depot.example.com/api/v1/console/phone/confirm' \
-H 'X-CSRF-Token: <csrf-token>' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-b 'outpost_console=<console-session>' \
--data '{
"code": "string",
"phone": "string"
}'

POSTpublic facehosteditmspremote

Source: [object Object]

Phone start

Required, sent as application/json.

FieldTypeDescription
coderequiredstring
phonerequiredstring
StatusBodyDescription
200application/json, objectSuccess.
202application/json, objectAccepted.
400application/problem+json, ProblemDetailsThe request body is not acceptable.
Terminal window
# Sign in first. outpost_console is a session cookie, so send it with every request.
curl -X POST 'https://depot.example.com/api/v1/console/phone/start' \
-H 'X-CSRF-Token: <csrf-token>' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-b 'outpost_console=<console-session>' \
--data '{
"code": "string",
"phone": "string"
}'

GETpublic facehosteditmspremoteIdempotent

Source: [object Object]

totpStatus reports whether the second factor is on, how many recovery codes remain, and how many the current set had, so the console can say “7 of 10” without assuming the size of a set.

A user burning through them is worth showing.

StatusBodyDescription
200application/json, objectSuccess.
Terminal window
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X GET 'https://depot.example.com/api/v1/console/totp' \
-H 'X-CSRF-Token: <csrf-token>' \
-H 'Accept: application/json' \
-b 'outpost_console=<console-session>'

POSTpublic facehosteditmspremote

Source: [object Object]

totpConfirm proves the secret was scanned, turns TOTP on, and returns the recovery codes.

This is the only time the recovery codes are ever shown.

Required, sent as application/json.

FieldTypeDescription
ceremonyIdrequiredstring
coderequiredstring
credentialrequiredany

Arbitrary JSON.

recoveryCoderequiredstring
rememberDevicerequiredboolean
StatusBodyDescription
200application/json, objectSuccess.
400application/problem+json, ProblemDetailsThe request body is not acceptable.
Terminal window
# Sign in first. outpost_console is a session cookie, so send it with every request.
curl -X POST 'https://depot.example.com/api/v1/console/totp/confirm' \
-H 'X-CSRF-Token: <csrf-token>' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-b 'outpost_console=<console-session>' \
--data '{
"ceremonyId": "string",
"code": "string",
"credential": "string",
"recoveryCode": "string",
"rememberDevice": false
}'

POSTpublic facehosteditmspremote

Source: [object Object]

totpDisable turns the second factor off.

It requires a current code, because “turn off MFA” from a hijacked session would otherwise be the easiest way to make a stolen session permanent.

Required, sent as application/json.

FieldTypeDescription
ceremonyIdrequiredstring
coderequiredstring
credentialrequiredany

Arbitrary JSON.

recoveryCoderequiredstring
rememberDevicerequiredboolean
StatusBodyDescription
204NoneSuccess, with no body.
400application/problem+json, ProblemDetailsThe request body is not acceptable.
Terminal window
# Sign in first. outpost_console is a session cookie, so send it with every request.
curl -X POST 'https://depot.example.com/api/v1/console/totp/disable' \
-H 'X-CSRF-Token: <csrf-token>' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-b 'outpost_console=<console-session>' \
--data '{
"ceremonyId": "string",
"code": "string",
"credential": "string",
"recoveryCode": "string",
"rememberDevice": false
}'

POSTpublic facehosteditmspremote

Source: [object Object]

totpEnrol issues a secret and the URI an authenticator app scans.

It does NOT turn TOTP on: the secret is unconfirmed until a code proves the user actually scanned it, so a half-finished enrolment cannot lock anyone out.

StatusBodyDescription
200application/json, objectSuccess.
Terminal window
# Sign in first. outpost_console is a session cookie, so send it with every request.
curl -X POST 'https://depot.example.com/api/v1/console/totp/enrol' \
-H 'X-CSRF-Token: <csrf-token>' \
-H 'Accept: application/json' \
-b 'outpost_console=<console-session>'

POSTpublic facehosteditmspremote

Source: [object Object]

regenerateRecoveryCodes issues a fresh set of ten recovery codes and voids the old ones, for the signed-in account.

It asks for a CURRENT TOTP CODE, which is the “recent second factor” the operation needs and the same step-up totpDisable takes: a hijacked session that could mint itself recovery codes would have a permanent second factor of its own. The step is recorded so the code cannot be replayed.

The new codes are returned exactly once, here.

Required, sent as application/json.

FieldTypeDescription
ceremonyIdrequiredstring
coderequiredstring
credentialrequiredany

Arbitrary JSON.

recoveryCoderequiredstring
rememberDevicerequiredboolean
StatusBodyDescription
200application/json, objectSuccess.
400application/problem+json, ProblemDetailsThe request body is not acceptable.
Terminal window
# Sign in first. outpost_console is a session cookie, so send it with every request.
curl -X POST 'https://depot.example.com/api/v1/console/totp/recovery-codes' \
-H 'X-CSRF-Token: <csrf-token>' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-b 'outpost_console=<console-session>' \
--data '{
"ceremonyId": "string",
"code": "string",
"credential": "string",
"recoveryCode": "string",
"rememberDevice": false
}'

Content is licensed CC BY 4.0. Code samples are MIT. Outpost and the Outpost mark are trademarks of Outpost Business Solutions, PBC and are not covered by either license.