Add, rename, and remove passkeys
A signed-in user manages passkeys under /account/passkeys. Every change requires a CSRF token in the RequestVerificationToken header and a fresh ReAuth proof. Send credentials: 'include' on every request. To create a new account with a passkey, see Register users instead.
| Task | Endpoint | ReAuth | CSRF |
|---|---|---|---|
| Get creation options | POST /account/passkeys/creationOptions | Yes | Yes |
| Add a passkey | POST /account/passkeys/ | Yes | Yes |
| List passkeys | GET /account/passkeys/ | No | No |
| Rename a passkey | PATCH /account/passkeys/ | Yes | Yes |
| Remove a passkey | DELETE /account/passkeys/{credentialIdUrl} | Yes | Yes |
Add a passkey
- Complete step-up. See Require step-up before sensitive actions.
- Get creation options from
POST /account/passkeys/creationOptions. The options are for the signed-in user. - Call
navigator.credentials.create. - Send
POST /account/passkeys/with{ "credentialJson", "name"? }. AuthEndpoints trimsname. A name longer than 200 characters returns a400validation problem with the keyName.const headers = { 'Content-Type': 'application/json', 'RequestVerificationToken': csrfToken }; const options = await fetch('/account/passkeys/creationOptions', { method: 'POST', credentials: 'include', headers }).then(r => r.json()); const credential = await navigator.credentials.create({ publicKey: PublicKeyCredential.parseCreationOptionsFromJSON(options) }); const added = await fetch('/account/passkeys/', { method: 'POST', credentials: 'include', headers, body: JSON.stringify({ credentialJson: JSON.stringify(credential), name: 'Work laptop' }) }).then(r => r.json()); // { credentialId, displayName, createdAt } - Check the response:
200 { "credentialId", "displayName", "createdAt" }: the passkey is stored.400validation problemUserMismatch: the passkey belongs to a different user.400validation problemInvalidPasskeyState: no ceremony was underway. Start again from step 2.400with a detail that starts with "Could not add the passkey": the attestation failed.
List passkeys
Send GET /account/passkeys/. The response is { "passkeys": [{ "credentialId", "displayName", "createdAt" }] }. credentialId is Base64Url.
Rename a passkey
Complete step-up, then send PATCH /account/passkeys/ with { "id": "<credentialId>", "newName": "..." }. The response is 200 with an empty body. An unknown id returns 404. An id that is not valid Base64Url returns a 400 validation problem InvalidCredentialId.
Remove a passkey
Complete step-up, then send DELETE /account/passkeys/<credentialId>. The response is 200 with an empty body. An unknown id returns 404.
This endpoint does not check whether the passkey is the user's last sign-in method. Before you remove a passkey from an account with no password and no external login, warn the user.