A passkey is a passwordless credential stored on the user’s device (or password manager). The user signs in with the same biometric or PIN they use to unlock their device — no password, nothing to remember.
Passkeys are a sign-in method, not a sign-up method. A user cannot create an account with a passkey. The order is always:
- The user signs up with another method (email/SMS OTP, social, wallet).
- While authenticated, they register a passkey.
- On a later visit, they sign in with that passkey.
registerPasskey requires an authenticated user; signInWithPasskey requires an already-registered passkey. Build your UI around that sequence.
Prerequisites
Before this: create and initialize a Dynamic client (see Creating a Dynamic Client, Initializing the Dynamic Client).
Enable Passkey in your Dynamic dashboard. Passkeys use WebAuthn, which requires a secure context (HTTPS, or localhost in development).
Register a passkey
Call registerPasskey while the user is signed in — for example, in an “enable faster sign in” prompt right after sign-up, or from an account-security screen. It triggers the browser’s passkey-creation dialog, then stores the credential against the user.
Wire the call to your “enable faster sign in” button. Disable it while the browser dialog is open so the user can’t trigger two prompts at once.import { registerPasskey } from '@dynamic-labs-sdk/client';
const button = document.querySelector<HTMLButtonElement>('#add-passkey')!;
button.addEventListener('click', async () => {
button.disabled = true;
try {
// The browser prompts the user to create a passkey (Face ID, fingerprint, etc.)
await registerPasskey();
// showConfirmation is your implementation.
showConfirmation('Passkey added.');
} finally {
button.disabled = false;
}
});
import { useRegisterPasskey, useGetPasskeys } from '@dynamic-labs-sdk/react-hooks';
function AddPasskeyButton() {
const { mutate: registerPasskey, isPending } = useRegisterPasskey();
const { refetch } = useGetPasskeys();
return (
<button
disabled={isPending}
onClick={() => registerPasskey(undefined, { onSuccess: () => refetch() })}
>
{isPending ? 'Follow the prompt…' : 'Set up a passkey'}
</button>
);
}
Sign in with a passkey
For returning users who already registered a passkey, a single button is enough. signInWithPasskey opens the browser’s passkey picker and, on success, authenticates the user.
import { signInWithPasskey } from '@dynamic-labs-sdk/client';
const button = document.querySelector<HTMLButtonElement>('#passkey-sign-in')!;
button.addEventListener('click', async () => {
// Opens the browser's passkey picker; resolves with the authenticated user.
const { user } = await signInWithPasskey();
// renderApp is your implementation.
renderApp(user);
});
import { useSignInWithPasskey, useUser } from '@dynamic-labs-sdk/react-hooks';
function PasskeySignInButton() {
const { data: user } = useUser();
const { mutate: signInWithPasskey, isPending, error } = useSignInWithPasskey();
if (user) return <p>Signed in as {user.email}</p>;
return (
<>
<button disabled={isPending} onClick={() => signInWithPasskey()}>
Sign in with a passkey
</button>
{error && <p role="alert">Couldn't sign in with a passkey. Try another method.</p>}
</>
);
}
Offer “Sign in with a passkey” alongside your other sign-in options rather than as the only choice — a returning user might be on a device where they haven’t registered one yet.
List and remove passkeys
Let users see and manage their registered passkeys from an account-security screen. getPasskeys returns the user’s passkeys; deletePasskey removes one by id. Each passkey has an optional user-assigned alias, plus createdAt and the origin it was registered on.
Render the user’s passkeys into a list, each with a “Remove” button that calls deletePasskey and re-renders.import { getPasskeys, deletePasskey } from '@dynamic-labs-sdk/client';
const list = document.querySelector<HTMLUListElement>('#passkey-list')!;
async function renderPasskeys() {
const passkeys = await getPasskeys();
list.replaceChildren();
for (const passkey of passkeys) {
const item = document.createElement('li');
const added = new Date(passkey.createdAt).toLocaleDateString();
item.textContent = `${passkey.alias ?? 'Passkey'} · added ${added} `;
const remove = document.createElement('button');
remove.textContent = 'Remove';
remove.addEventListener('click', async () => {
await deletePasskey({ passkeyId: passkey.id });
renderPasskeys();
});
item.append(remove);
list.append(item);
}
}
renderPasskeys();
import { useGetPasskeys, useDeletePasskey } from '@dynamic-labs-sdk/react-hooks';
function PasskeyList() {
const { data: passkeys, isLoading, refetch } = useGetPasskeys();
const { mutate: deletePasskey } = useDeletePasskey();
if (isLoading) return <p>Loading passkeys…</p>;
if (!passkeys?.length) return <p>No passkeys yet.</p>;
return (
<ul>
{passkeys.map((passkey) => (
<li key={passkey.id}>
{passkey.alias ?? 'Passkey'} · added{' '}
{new Date(passkey.createdAt).toLocaleDateString()}
<button
onClick={() =>
deletePasskey({ passkeyId: passkey.id }, { onSuccess: () => refetch() })
}
>
Remove
</button>
</li>
))}
</ul>
);
}
Removing a passkey is a sensitive action, so it may require the user to re-authenticate (step-up). See Step-up authentication for how to handle that prompt.
Handling errors
| Error | Cause | Resolution |
|---|
NoWebAuthNSupportError | The browser or device doesn’t support WebAuthn. | Hide the passkey option and fall back to another sign-in method. |
NoPasskeyCredentialsFoundError | Sign-in was attempted but the user has no passkey for this app. | Prompt them to sign in another way, then register a passkey. |
| Browser cancellation | The user dismissed the passkey dialog, or it timed out. | The call rejects — leave the user on the current screen and let them retry. |
import {
signInWithPasskey,
NoWebAuthNSupportError,
NoPasskeyCredentialsFoundError,
} from '@dynamic-labs-sdk/client';
try {
await signInWithPasskey();
} catch (error) {
if (error instanceof NoWebAuthNSupportError) {
// This device can't use passkeys — offer email or social sign-in instead.
} else if (error instanceof NoPasskeyCredentialsFoundError) {
// No passkey registered yet — send the user through another method first.
} else {
// User cancelled the prompt, or it timed out — let them try again.
}
}
See also