Skip to main content
This guide covers how to register passkeys, sign in with them, use them in a step-up, and manage them in your mobile application. Make sure you have configured passkeys on your backend before proceeding. Passkeys serve two roles in the SDK:
  • Primary-factor sign-in: the user signs in with only a passkey, with no OTP and no password. Driven by loginWithPasskey. Requires login_enabled in the passkey configuration.
  • Step-up factor: the user already has a session and proves a passkey to acquire a sensitive scope. Driven by continueStepUpWithPasskey.
For the concepts (ceremony shape, security model, webhooks), see the Passkey page. Pick your platform once: tabs across this section stay in sync.

Requirements

Passkeys need a more recent OS version than the rest of the SDK, and the SDK version that added them: On a device that cannot run a passkey ceremony, the passkey methods fail with a “passkey not supported” error (see Errors). Offer another sign-in method in that case.

Platform setup

Before any passkey ceremony, the operating system verifies that your app is associated with the relying-party host (the rp_id of your passkey configuration). The association has two sides, and both are required: the server authorizes the app, and the app declares the host.

Authorize your apps in the passkey configuration

Add your native apps to the passkey configuration with the Management API. Prelude then serves the association documents (/.well-known/apple-app-site-association and /.well-known/assetlinks.json) on your app’s Prelude domain: <APP_ID>.session.prelude.dev, or your custom domain. The operating system fetches these documents from the rp_id host. When your rp_id is your app’s Prelude domain, nothing more is needed. When it is another host, serve the same two documents on that host yourself.
This request replaces the passkey configuration. It is not a partial update. If your app already has a passkey configuration, read it first with GET on the same URL, then send every field again with its current value: rp_id, rp_name, allowed_origins, user_verification, attestation_preference, and granted_scopes. A field that you omit goes back to its default, and granted_scopes becomes empty. Changing rp_id breaks the passkeys that are already registered. See Passkey Authentication for all fields.
Only ios_app_ids, android_apps, login_enabled, aaguid_allowlist, and aaguid_blocklist keep their stored value when you omit them.
Add the fingerprint of every certificate that signs a build your users install. A debug build and a Play Store build are usually signed with different certificates.

Declare the relying-party host in your app

Add the Associated Domains capability to your app target, with one entry for the relying-party host:

Register a passkey

A passkey is never the first credential of a user. Registration runs inside an authenticated session and requires the prld:passkey:write scope, which a step-up grants. Complete the step-up first, then register.
The step-up that grants prld:passkey:write must use the grant mode session-bound or profile-bound. Registration verifies the scope against the session, so a single-use grant is not honored.
A user can register several passkeys. Each registration consumes the prld:passkey:write scope: run a new step-up before the next registration, and before you list, rename, or delete passkeys. When the device already holds a passkey for this user, a new registration on Android (also through Flutter and React Native) throws the “passkey already registered” error (see Errors). alreadyRegistered is true only when the authenticator returns a credential that Prelude already stores.

Sign in with a passkey

loginWithPasskey signs a user in with the platform authenticator. The app sends no identifier: the authenticator offers the passkeys it holds for the relying party, and the server resolves the user from the one the user picks. On success the SDK persists the session tokens in the platform’s secure store. Requires login_enabled in the passkey configuration.
The server returns the same “unauthorized” error for an unknown user and for a bad signature, so an attacker cannot use it to find valid users.

AutoFill (iOS)

On iOS, the autofill option offers matching passkeys in the keyboard’s AutoFill row instead of a modal sheet, when the user focuses a username field. The request shows no sheet and stays active, so start it when your sign-in screen appears. Android always shows the Credential Manager sheet.

Use a passkey in a step-up

When a step-up challenge has verify_passkey as its current step, advance it with the platform authenticator instead of a code. continueStepUpWithPasskey has the same contract as submitStepUpOTP: it returns the next challenge, or nothing when the step-up is complete.
To let users without a passkey complete the step-up, configure a fallback step: see Fall back to OTP for users without a passkey.

Manage registered passkeys

Build a “Manage your passkeys” screen with three methods. All three require prld:passkey:write, the same scope as registration, so run the step-up before you open the screen. A registration consumes the scope, so run the step-up again after the user adds a passkey from this screen. Listing returns an empty list when the user has no passkey. Pass an empty nickname to clear the label.
Each credential carries its identifier, the optional nickname, the transports reported by the authenticator, the backupState (whether the passkey is synced), and the creation and last-use times.

Errors

Each SDK names these errors in its own convention, for example PreludeAuthError.passkeyNotSupported (iOS), PreludeAuthError.PasskeyNotSupported (Android), PasskeyNotSupportedException (Flutter), and PasskeyNotSupportedError (React Native). If a ceremony fails although the code is correct, check the platform setup first: a missing association on either side makes the operating system refuse the ceremony before it reaches Prelude.