Client API

register, login, autofill, capability detection, and the low-level ceremony calls.

import { register, login } from 'passkify/client';

Roughly 6.5 kB gzipped, no dependencies, no Node built-ins. Runs in any bundler or straight from a <script type="module">.

The two calls

registermethod

register(input?: RegisterInput): Promise<PasskeyClientResult>

Registers a passkey. Does the whole round trip: fetches options from your server, runs the WebAuthn ceremony, posts the result back, returns the verified user.

usernamestring
displayNamestringdefault username
signalAbortSignal
configClientConfig

Returns

{ verified: true; user; credentialId; isNewUser? }
const { user, isNewUser } = await register({ username: 'ada' });

loginmethod

login(input?: LoginInput): Promise<PasskeyClientResult>

Signs in.

const { user } = await login();              // usernameless, prefer this
const { user } = await login({ username });  // scoped to one account

Call it with no arguments

Conditional UI

signInWithAutofillmethod

signInWithAutofill(input?: AutofillInput): Promise<PasskeyClientResult | null>

Offers passkeys inside the browser's own autofill dropdown, above the saved usernames. This is the best sign-in experience passkeys can give: no button, no picker, one tap.

signInWithAutofill()
  .then((result) => { if (result) location.href = '/dashboard'; })
  .catch(() => {});

Your username input needs the right autocomplete token:

<input autocomplete="username webauthn" />

Why the rejection is safe to swallow

Capability detection

Use these to decide how prominently to offer passkeys, not whether to offer them.

isSupportedmethod

isSupported(): boolean

Whether WebAuthn exists at all. Synchronous, safe to call during render.

isPlatformAuthenticatorAvailablemethod

isPlatformAuthenticatorAvailable(): Promise<boolean>

Whether there is a built-in authenticator: Touch ID, Face ID, Windows Hello, an Android screen lock.

False does not mean 'no passkeys'

isAutofillAvailablemethod

isAutofillAvailable(): Promise<boolean>

Whether conditional mediation is supported. signInWithAutofill calls this itself, so you rarely need it directly.

Configuration

configuremethod

configure(config: ClientConfig): void

Sets defaults for every subsequent call. Optional: the defaults work if you mounted the server at /passkey on the same origin.

baseUrlstringdefault '/passkey'
headersRecord<string, string>
credentialsRequestCredentialsdefault 'same-origin'
fetchtypeof fetch
configure({
  baseUrl: '/api/passkey',
  headers: { 'x-csrf-token': token },
});

Lower-level ceremony calls

For when your backend is not passkify, or your routes do not follow the /register/start convention. These run only the WebAuthn half and hand you a JSON-safe object.

createCredentialmethod

createCredential(options: RegistrationOptionsJSON, signal?: AbortSignal): Promise<RegistrationResponseJSON>
import { createCredential } from 'passkify/client';
 
const options  = await fetch('/my/route').then((r) => r.json());
const response = await createCredential(options);
await fetch('/my/other/route', { method: 'POST', body: JSON.stringify(response) });

getAssertionmethod

getAssertion(options: AuthenticationOptionsJSON, extras?: { signal?: AbortSignal; mediation?: CredentialMediationRequirement }): Promise<AuthenticationResponseJSON>

The login equivalent.

What these save you, even with your own backend

cancelPendingCeremonymethod

cancelPendingCeremony(): void

Aborts whatever ceremony is in flight.

Why it works this way

Errors

Everything throws PasskeyError with a stable code.

import { login, PasskeyError } from 'passkify/client';
 
try {
  await login();
} catch (error) {
  if (error instanceof PasskeyError) {
    if (error.isUserCancellation) return;          // they closed the prompt
    if (error.code === 'unknown_credential') return showSignUpPrompt();
    showMessage(error.message);
  }
}

Browser errors are deliberately vague

The browser will not tell you why a ceremony failed, because a precise answer would let a page probe which credentials a visitor holds. NotAllowedError covers "cancelled", "timed out" and "no matching credential" alike.

passkify maps the DOMException names to codes and writes messages aimed at the developer reading the console:

DOMExceptionCodeUsually means
NotAllowedErrorcancelledDismissed, timed out, or no matching passkey
AbortErrorcancelledSuperseded by another ceremony, or your signal fired
InvalidStateErroralready_registeredThis device already holds a passkey for the account
SecurityErrorinsecure_contextNot HTTPS, or rpID does not match the page
NotSupportedErrorunsupportedNo authenticator supports the requested algorithms
ConstraintErrornot_allowedUV or a discoverable credential was required, none available

React

'use client';
 
import { useEffect, useState } from 'react';
import { register, login, signInWithAutofill, configure, PasskeyError } from 'passkify/client';
 
configure({ baseUrl: '/api/passkey' });
 
export function SignIn() {
  const [status, setStatus] = useState('');
 
  useEffect(() => {
    const controller = new AbortController();
    signInWithAutofill({ signal: controller.signal })
      .then((result) => { if (result) window.location.assign('/'); })
      .catch(() => {});
    return () => controller.abort();
  }, []);
 
  const signIn = async () => {
    try {
      await login();
      window.location.assign('/');
    } catch (error) {
      if (error instanceof PasskeyError && error.isUserCancellation) return;
      setStatus(error instanceof Error ? error.message : 'Something went wrong.');
    }
  };
 
  return (
    <>
      <input autoComplete="username webauthn" />
      <button onClick={signIn}>Sign in with a passkey</button>
      {status && <p role="alert">{status}</p>}
    </>
  );
}

The AbortController in the cleanup matters: without it, a pending autofill request survives unmount and blocks the next ceremony.