Skip to main content

POST /v1/users/me/passkeys/register/begin

Start a WebAuthn enrollment ceremony. Returns the PublicKeyCredentialCreationOptions that the browser hands to navigator.credentials.create().

::: tip Auth Required: cookie or Bearer. :::

Request

POST /v1/users/me/passkeys/register/begin

HeaderRequiredNotes
Cookie: identsphere_at=... OR Authorization: Bearer ...yes

No request body.

Response

200 OK

A WebAuthn PublicKeyCredentialCreationOptions JSON object (shape as defined by the W3C WebAuthn spec). Example abbreviated:

{
"publicKey": {
"rp": { "id": "app.example.com", "name": "Acme" },
"user": { "id": "...", "name": "alice@example.com", "displayName": "Alice" },
"challenge": "...",
"pubKeyCredParams": [{ "type": "public-key", "alg": -7 }],
"timeout": 60000,
"attestation": "none",
"excludeCredentials": [{ "type": "public-key", "id": "..." }]
}
}

Error responses

StatusCodeWhen
401authentication_requiredNo valid auth credential.
404not_foundUser no longer exists.
409conflictUser already has the maximum 10 passkeys enrolled.
500internal_errorWebAuthn ceremony build failure or DB error.

Example: TypeScript (@identsphere/react)

import { usePasskeyEnroll } from '@identsphere/react';

function AddPasskeyButton() {
const enroll = usePasskeyEnroll();
return (
<button onClick={() => enroll.mutateAsync({ label: 'MacBook Touch ID' })}>
Add a passkey
</button>
);
}

The hook handles beginnavigator.credentials.create()complete in one call. If you're rolling your own client, see the manual flow below.

Example: Manual (browser)

const begin = await fetch('/v1/users/me/passkeys/register/begin', {
method: 'POST',
credentials: 'include',
}).then(r => r.json());

const cred = await navigator.credentials.create({ publicKey: begin.publicKey });

await fetch('/v1/users/me/passkeys/register/complete', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ credential: cred, label: 'MacBook Touch ID' }),
});

Notes

  • Challenge state is persisted server-side in passkey_challenges with a 5-minute TTL.
  • Already-enrolled credentials for this user are listed in excludeCredentials so the authenticator prompts the user to add a NEW credential rather than silently re-registering an existing one.
  • Per-user cap is 10 enrolled passkeys.