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

# Password and passkeys

> Change your password; register and revoke WebAuthn credentials

Both surfaces are on `https://dash.square1.dev` and accept either the
`ws-session` cookie or a Bearer API key.

## Change your password

`POST /api/user/me/password` — **5 requests/minute.**

```bash theme={null}
curl -X POST https://dash.square1.dev/api/user/me/password \
  -H "Authorization: Bearer $SQUARE1_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"currentPassword": "...", "newPassword": "..."}'
```

```json theme={null}
{ "success": true }
```

camelCase body. The new password must be **at least 8 characters**; there is
no other composition rule.

<Warning>
  **Success revokes every session on the account** — including this one —
  and then immediately issues a replacement, returned as a fresh
  `ws-session` cookie on the same response. Half the point of changing a
  password is cutting off a stolen session, so nothing else survives.

  If you called this with a Bearer key rather than a cookie, you are logged
  out of every browser and receive a session cookie you probably don't want.
</Warning>

**Your API keys are untouched.** They are a separate credential, and quietly
revoking them would kill every hardcoded client at once. Rotate them
explicitly from [API keys](/account/api-keys) if that's what you want.

### Errors

| HTTP | `code`             | Meaning                                                                               |
| ---- | ------------------ | ------------------------------------------------------------------------------------- |
| 401  | —                  | Not authenticated, **or** `currentPassword` is wrong                                  |
| 403  | —                  | Guest account — guests don't use passwords                                            |
| 409  | `password_not_set` | This account has no password. Set one by re-verifying through the dashboard, not here |
| 400  | —                  | `newPassword` is shorter than 8 characters                                            |

<Note>
  The `409` fires **after** the current-password check has already run
  against a dummy hash, so the response time doesn't reveal whether a
  password exists. Both success and failure are written to the account's
  audit trail.
</Note>

## Passkeys

Four endpoints under `/api/user/me/passkeys`, holding up to **5** WebAuthn
credentials per account.

<Warning>
  **Passkeys are behind an operator switch.** When it's off, all four
  endpoints answer `404` with code `feature_disabled` — the surface reports
  itself as nonexistent rather than as forbidden:

  ```json theme={null}
  {
    "error": {
      "message": "요청한 기능을 찾을 수 없습니다",
      "type": "invalid_request_error",
      "code": "feature_disabled"
    }
  }
  ```

  Check `passkeyCount` on [`GET /api/user/me`](/account/profile) — it reads
  `0` whenever the feature is off — before assuming a `404` means "no
  passkeys".
</Warning>

### List — `GET /api/user/me/passkeys`

```json theme={null}
{
  "success": true,
  "data": {
    "passkeys": [
      {
        "id": "AQIDBAUGBwgJCgsMDQ4PEA",
        "label": "iCloud Keychain",
        "deviceType": "multiDevice",
        "createdAt": "2026-08-21T04:10:00.000Z",
        "lastUsedAt": "2026-08-29T08:02:11.000Z"
      }
    ],
    "max": 5
  }
}
```

camelCase. Oldest first, revoked credentials excluded. `id` is the WebAuthn
credential id. When you didn't name a passkey, `label` falls back to a
recognized device name for its authenticator. `deviceType` is the
authenticator's WebAuthn device type (`singleDevice` or `multiDevice`), and
is an empty string when the authenticator didn't report one.

### Register — two steps

Registration is the standard WebAuthn ceremony split across two calls.

**1. `POST /api/user/me/passkeys/options`**

```json theme={null}
{ "success": true, "data": { "optionsJSON": { "...": "..." } } }
```

Pass `optionsJSON` straight to `navigator.credentials.create()`. The
response also sets a **`ws-pk` cookie** — a browser-session key, separate
from `ws-session`, that binds the challenge to this tab. It lives 10
minutes; the challenge behind it expires after **5**.

A `409` (no `code`) means the challenge could not be started — most often
because you already hold `max` passkeys. The cap is enforced here, at the
start of the ceremony, not at the finish.

**2. `POST /api/user/me/passkeys`**

```bash theme={null}
-d '{"response": { /* the authenticator response */ }, "label": "MacBook"}'
```

```json theme={null}
{ "success": true, "data": { "label": "MacBook" } }
```

`label` is optional. Both a missing `response` and a missing/expired `ws-pk`
cookie return `400 passkey_challenge_invalid`, as does a response that fails
verification.

<Note>
  Because the challenge is keyed to the `ws-pk` cookie rather than to your
  account, two browser tabs can register independently without one
  overwriting the other's challenge. It also means the two calls must come
  from the same cookie jar — this ceremony is not scriptable with a Bearer
  key alone.
</Note>

### Revoke — `DELETE /api/user/me/passkeys/:id`

```json theme={null}
{ "success": true }
```

`:id` is the credential id from the list. `404` if it isn't yours or is
already revoked. Revocations are audited.

<Warning>
  Nothing stops you from revoking your last passkey. Make sure you still
  have a working password or API key first.
</Warning>
