> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aropay.aro.media/llms.txt
> Use this file to discover all available pages before exploring further.

# Passkeys

> Phishing-resistant, passwordless sign-in with WebAuthn: registration, usernameless login, and device management.

Passkeys are WebAuthn credentials: Touch ID, Windows Hello, hardware keys, or
a password manager's synced passkeys. They're phishing-resistant (bound to the
AroPay origin), involve no shared secret, and **count as strong MFA on their
own**: a passkey login never asks for a TOTP code.

## Register a passkey

Registration is **session-only** and, in practice, a dashboard flow
(**Settings → Security → Add passkey**) since it needs a browser's WebAuthn
API:

<Steps>
  <Step title="Get registration options">
    `POST /me/passkeys/options` returns WebAuthn creation options; the
    challenge rides in a signed, account-bound cookie.
  </Step>

  <Step title="Create the credential">
    The browser calls `navigator.credentials.create()` with those options and
    the authenticator does its ceremony.
  </Step>

  <Step title="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`.
  </Step>
</Steps>

## Sign in with a passkey

```bash theme={null}
# 1. Options (email optional; omit it for usernameless login)
curl -s "$BASE/auth/passkey/options" \
  -H "content-type: application/json" \
  -d '{"email":"you@company.com"}'

# 2. Browser: navigator.credentials.get(options)

# 3. Verify the assertion (mints the session cookie)
curl -s "$BASE/auth/passkey/verify" -c cookies.txt \
  -H "content-type: application/json" \
  -d '{"credential":{ …WebAuthn assertion… }}'
```

With `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

```bash theme={null}
# List (works with session or API key)
curl -s "$BASE/me/passkeys" -H "Authorization: Bearer $KEY"

# Remove (session-only)
curl -s -X DELETE "$BASE/me/passkeys/$PASSKEY_ID" -b cookies.txt
```

Each entry shows `id`, `deviceName`, `transports`, `createdAt`, and
`lastUsedAt`, enough to audit which devices can access the account and prune
stale ones.

<Tip>
  Register at least two passkeys (e.g. laptop + phone) so losing one device
  never locks you out. Passwords keep working alongside passkeys, so you always
  have a fallback path; protect it with
  [2FA](/security/two-factor-authentication).
</Tip>
