POST /v1/users/me/passkeys/register/complete
Finalize a WebAuthn enrollment ceremony. Persists the new credential and
flips passkey_enabled = true on the user record.
::: tip Auth Required: cookie or Bearer. :::
Request
POST /v1/users/me/passkeys/register/complete
| Header | Required | Notes |
|---|---|---|
Cookie: identsphere_at=... OR Authorization: Bearer ... | yes | — |
Content-Type: application/json | yes | — |
Body
{
"credential": { /* PublicKeyCredential from navigator.credentials.create() */ },
"label": "MacBook Touch ID"
}
| Field | Type | Required | Notes |
|---|---|---|---|
credential | object | yes | The browser-built PublicKeyCredential response. Pass through unmodified. |
label | string | null | no | User-friendly label. Shown in the passkey list UI. |
Response
200 OK
{
"id": "a8f3c2d1-...",
"label": "MacBook Touch ID",
"aaguid": null,
"transport": null,
"created_at": "2026-05-28T12:00:00+00:00",
"last_used_at": null
}
Error responses
| Status | Code | When |
|---|---|---|
| 400 | invalid_input | No matching pending registration challenge for this user, OR the credential failed WebAuthn validation. |
| 401 | authentication_required | No valid auth credential. |
| 404 | not_found | User no longer exists. |
| 409 | conflict | This credential is already registered (likely to a different user). |
| 500 | internal_error | Serialization or DB failure. |
Notes
- The challenge row in
passkey_challengesis deleted regardless of whether the ceremony succeeded — defeats replay attempts. - The full
Passkeystruct (COSE public key, sign counter, flags) is serialized to JSON, base64-encoded, and stored inuser_passkeys.public_key. - An audit entry (
auth.passkey.added) is recorded.