passkey_verification_failed.
Register a passkey
Registration is session-only and, in practice, a dashboard flow (Settings → Passkeys → Add passkey) since it needs a browser’s WebAuthn API. A session alone isn’t enough: you confirm with your password, plus a code from your authenticator app when 2FA is on (step-up).1
Get registration options
POST /me/passkeys/options with { "password": "…", "code": "123456" }
(code only when 2FA is on) checks the step-up, then returns WebAuthn
creation options; the challenge rides in a signed cookie bound to your
account, its token version and this session. Without step-up there is no
challenge, so register can’t succeed, and asking for options again
replaces the previous challenge.2
Create the credential
The browser calls
navigator.credentials.create() with those options and
the authenticator does its ceremony.3
Register it
POST /me/passkeys/register with the WebAuthn credential (and an
optional name like “MacBook Touch ID”) verifies the attestation against
the challenge cookie and stores the credential. Re-registering the same
authenticator returns 409 passkey_exists. A challenge answers one
attempt, even a failed one (400 invalid_challenge after that), so to
retry, start again from step 1. The request must be application/json:
anything else gets 415 unsupported_media_type before it touches the
challenge, so a forged form post can’t cancel your enrollment.If your password is changed, 2FA is turned on, or an admin resets the
account before you finish, the registration can’t complete: either that
ended your session (401 session_expired), or this browser got a new
session and the challenge belongs to the old one
(400 invalid_challenge). Sign in if needed and start again from
step 1. Only if the change lands while the register request itself is
being processed do you get 409 account_changed, with nothing stored.Sign in with a passkey
email omitted, discoverable credentials let the authenticator pick the
account: true one-tap, usernameless sign-in.
Challenge handling is strict: options and verification must come from the same
client (the challenge cookie is signed), challenges expire quickly
(400 challenge_expired), and each is single-use (400 invalid_challenge).
An assertion from an authenticator the account doesn’t know fails with
401 unknown_passkey.
Manage devices
id, deviceName, transports, createdAt, and
lastUsedAt, enough to audit which devices can access the account and prune
stale ones.
Passkeys survive a password change unless you ask: tick Also remove all my
passkeys (removePasskeys: true on POST /me/password) if someone else may
have had access to your account. When an admin resets your password or 2FA,
or reactivates the account, they decide explicitly whether your passkeys stay
(recovery).