Several flows — confirming a sign-up, an email-verified password login, changing a temporary password, and resetting a password — finish with a SECURITY_SETUP_REQUIRED challenge instead of tokens:
{
  "success": true,
  "data": {
    "authenticated": false,
    "challenge": "SECURITY_SETUP_REQUIRED",
    "session_token": "3f1c2e8a-9b4d-4e6f-8a1b-2c3d4e5f6a7b"
  }
}
The account is active, but no tokens are issued yet. Before the user can get tokens, they must set up a second factor — a passkey (recommended) or 2FA. You carry the session_token from the challenge through every step below; nothing here needs a bearer token, because the user doesn’t have one yet.

The flow

Pick one of the two paths, then call Finish security setup to exchange the session_token for tokens.
  1. StartPOST /v1/auth/security-setup/passkey/register/start with the session_token. You get back publicKey creation options.
  2. On the device — pass those options to the browser’s navigator.credentials.create() (or the platform credential API on native) so the user creates the passkey with fingerprint, face, or screen lock.
  3. FinishPOST /v1/auth/security-setup/passkey/register/complete with the attestation the device produced.

2FA path

  1. Set upPOST /v1/auth/security-setup/mfa/setup with the session_token. You get back a secret and an otpauth:// qr_uri; render the QR code for the user to scan into their authenticator app.
  2. ConfirmPOST /v1/auth/security-setup/mfa/confirm with the first 6-digit code the app shows.

Then: finish and get tokens

Once a passkey or 2FA is set up, call Finish security setup (POST /v1/auth/security-setup/complete) with the session_token. It returns the full set of tokens and ends the SECURITY_SETUP_REQUIRED state.
Calling complete before either factor is set up returns 403 SECURITY_SETUP_NOT_COMPLETE.
The session_token is short-lived. If it expires you’ll get 410 SESSION_EXPIRED — send the user back through the flow that issued it (for example, log in again) to get a fresh one.