<!-- section: Auth API · status: building · source: docs/auth-api/08-passkeys.md -->

> WebAuthn sign-in, bound to your domain rather than ours — and what that constrains.


# Passkeys

Sign in with a fingerprint, a face, or a security key. No password to phish.

## Passkeys are bound to your domain, not ours

That shapes the whole setup. A browser only accepts a credential whose relying
party is your own site, so we derive it from the **first browser origin you
register**:

```json
{ "rp": { "name": "Acme", "id": "app.acme.com" } }
```

The consequence to know before you build on it: **a passkey works on the domain
it was created for and nowhere else.** A project serving two unrelated domains
can have passkeys on one of them. That is WebAuthn's rule rather than ours, and
there is no way around it — the browser enforces it, not our server.

A project with no registered origins cannot use passkeys at all, which is already
true of every other browser flow.

## The flow

Standard WebAuthn. The browser's own `navigator.credentials` does the hard part;
these three calls are the server half. All take a **publishable** key.

```
GET  /v1/auth/passkey/generate-register-options   → options carrying your rp
POST /v1/auth/passkey/verify-registration         → stores the credential
POST /v1/auth/sign-in/passkey                     → a session
```

## GET /v1/auth/passkey/generate-register-options

Signed in, so the credential attaches to a known account.

```bash Request
curl "$BASE/v1/auth/passkey/generate-register-options" \
  -H "authorization: Bearer $PK" \
  -b "session cookie"
```

Pass the response to `navigator.credentials.create()`.

## POST /v1/auth/passkey/verify-registration

Post back what the browser returned. On success the passkey is stored and usable
immediately.

## POST /v1/auth/sign-in/passkey

Returns a session exactly as a password sign-in does — same cookie, same shape.
A passkey satisfies both factors at once, so an account with two-factor enabled
is not asked for a code as well.

## Errors

| Status | Code | Means |
|---|---|---|
| `400` | — | No registered origin, so there is no relying party to bind to |
| `400` | — | The credential was created for a different domain |
| `401` | — | Unknown or revoked credential |
| `429` | — | Rate limited; see [limits](/docs/auth-api/limits) |
