Passkeys provide native biometric authentication using Face ID, Touch ID, or fingerprint — no browser needed for the passkey step itself. This is an optional enhancement on top of the default email/phone login flow. When enabled, the SDK calls the device’s native passkey APIs directly instead of opening a portal URL.
Prerequisites
Configure your app’s passkey credentials so the OS can associate your app with Para’s domain.
In order for passkeys to work, you need to set up associated domains in your Xcode project linked to the Para domain.Set Up Associated Domains
- Open your project in Xcode
- Select your target and go to “Signing & Capabilities”
- Click ”+ Capability” and add “Associated Domains”
- Add the following domains:
- webcredentials:app.beta.usecapsule.com
- webcredentials:app.usecapsule.com
For additional information on associated domains, refer to the .Important: Your teamId + bundleIdentifier must be registered with the Para team to set up associated domains. For example, if your Team ID is A1B2C3D4E5 and Bundle Identifier is com.yourdomain.yourapp, provide A1B2C3D4E5.com.yourdomain.yourapp to Para. This is required by Apple for passkey security. Note: Allow up to 24 hours for domain propagation.
Getting ready for App Review? See for Para-specific review tips.
Install CocoaPods for native dependencies:
cd ios
bundle install
bundle exec pod install
cd ..
Remember to run pod install after adding new dependencies to your project.
Set Up Digital Asset Links
For Android setup, you need to provide your app’s SHA-256 certificate fingerprint to Para.Quick Testing Option: You can use com.getpara.example.reactnative as your package name for immediate testing. This package name is pre-registered and works with the SHA-256 certificate from the default debug.keystore.
To get your SHA-256 fingerprint:
- For debug builds:
keytool -list -v -keystore ~/.android/debug.keystore
- For release builds:
keytool -list -v -keystore <your_keystore_path>
For production apps, you’ll need to: 1. Upgrade your plan in the Developer Portal 2. Register your actual package name 3. Provide your app’s SHA-256 fingerprint 4. Wait up to 24 hours for the Digital Asset Links to propagate
Device Requirements
To ensure passkey functionality works correctly:
- Enable biometric or device unlock settings (fingerprint, face unlock, or PIN)
- Sign in to a Google account on the device (required for Google Play Services passkey management)
Configure your app.json file to enable passkey functionality and secure communication:{
"expo": {
"ios": {
"bundleIdentifier": "your.app.bundleIdentifier",
"associatedDomains": [
"webcredentials:app.beta.usecapsule.com?mode=developer",
"webcredentials:app.usecapsule.com"
]
}
}
}
Important: Your teamId + bundleIdentifier must be registered with the Para team to set up associated domains. For example, if your Team ID is A1B2C3D4E5 and Bundle Identifier is com.yourdomain.yourapp, provide A1B2C3D4E5.com.yourdomain.yourapp to Para. This is required by Apple for passkey security. Allow up to 24 hours for domain propagation. You can find this setting in the Developer Portal under the ‘Configuration’ tab of the API key label as Native Passkey Configuration.
Prepping for App Review? Check for Sign in with Apple tips, reviewer notes, and account deletion requirements.
For Android setup in Expo, you’ll need to configure your package name and provide your SHA-256 certificate fingerprint.Quick Testing Option: You can use com.getpara.example.expo as your package name in app.json for immediate testing. This package name is pre-registered but only works with the default debug.keystore generated by Expo.
Configure your app.json:{
"expo": {
"android": {
"package": "com.getpara.example.expo" // For testing
// or your actual package name for production
}
}
}
Important: Your SHA-256 certificate fingerprint must be registered with the Para team to set up associated domains. This is required by Google for passkey security. Allow up to 24 hours for domain propagation. You can find this setting in the Developer Portal under the ‘Configuration’ tab of the API key label as Native Passkey Configuration.
State Listener with Native Passkeys
Modify the state listener to intercept passkey states and call registerPasskey() or loginWithPasskey() natively, falling back to portal URLs only for password/PIN users:
import { useEffect, useRef } from "react";
import { para } from "../your-para-client";
import { InAppBrowser } from "react-native-inappbrowser-reborn";
import type { StateSnapshot } from "@getpara/react-native-wallet";
const APP_SCHEME = "your-app-scheme";
function useParaAuthStateListener(isAuthActive: boolean) {
const lastUrlRef = useRef<string | null>(null);
const handledRef = useRef<string | null>(null);
useEffect(() => {
if (!isAuthActive) return;
const unsubscribe = para.onStatePhaseChange(async (snapshot: StateSnapshot) => {
const { authPhase, authStateInfo } = snapshot;
// Native passkey signup — register using the credential ID
if (
authStateInfo.isNewUser &&
authStateInfo.passkeyId &&
handledRef.current !== authStateInfo.passkeyId
) {
handledRef.current = authStateInfo.passkeyId;
await para.registerPasskey(authStateInfo.passkeyId);
return;
}
// Native passkey login — trigger biometric prompt directly
if (
!authStateInfo.isNewUser &&
authStateInfo.hasPasskey &&
authPhase === "awaiting_session_start" &&
handledRef.current !== "login"
) {
handledRef.current = "login";
await para.loginWithPasskey();
return;
}
// Verification URL (basic login verification)
if (authStateInfo.verificationUrl && authStateInfo.verificationUrl !== lastUrlRef.current) {
lastUrlRef.current = authStateInfo.verificationUrl;
InAppBrowser.openAuth(authStateInfo.verificationUrl, APP_SCHEME, {
ephemeralWebSession: false,
showTitle: false,
});
return;
}
// Fallback: password/PIN portal URLs for non-passkey users
const url = authStateInfo.passwordUrl || authStateInfo.pinUrl;
if (url && url !== lastUrlRef.current) {
lastUrlRef.current = url;
InAppBrowser.openAuth(url, APP_SCHEME, {
ephemeralWebSession: false,
showTitle: false,
});
}
});
return () => {
unsubscribe();
lastUrlRef.current = null;
handledRef.current = null;
};
}, [isAuthActive]);
}
import { useEffect, useRef } from "react";
import { para } from "../your-para-client";
import { openAuthSessionAsync } from "expo-web-browser";
import type { StateSnapshot } from "@getpara/react-native-wallet";
const APP_SCHEME = "your-app-scheme";
function useParaAuthStateListener(isAuthActive: boolean) {
const lastUrlRef = useRef<string | null>(null);
const handledRef = useRef<string | null>(null);
useEffect(() => {
if (!isAuthActive) return;
const unsubscribe = para.onStatePhaseChange(async (snapshot: StateSnapshot) => {
const { authPhase, authStateInfo } = snapshot;
// Native passkey signup — register using the credential ID
if (
authStateInfo.isNewUser &&
authStateInfo.passkeyId &&
handledRef.current !== authStateInfo.passkeyId
) {
handledRef.current = authStateInfo.passkeyId;
await para.registerPasskey(authStateInfo.passkeyId);
return;
}
// Native passkey login — trigger biometric prompt directly
if (
!authStateInfo.isNewUser &&
authStateInfo.hasPasskey &&
authPhase === "awaiting_session_start" &&
handledRef.current !== "login"
) {
handledRef.current = "login";
await para.loginWithPasskey();
return;
}
// Verification URL (basic login verification)
if (authStateInfo.verificationUrl && authStateInfo.verificationUrl !== lastUrlRef.current) {
lastUrlRef.current = authStateInfo.verificationUrl;
openAuthSessionAsync(authStateInfo.verificationUrl, APP_SCHEME, {
preferEphemeralSession: false,
});
return;
}
// Fallback: password/PIN portal URLs for non-passkey users
const url = authStateInfo.passwordUrl || authStateInfo.pinUrl;
if (url && url !== lastUrlRef.current) {
lastUrlRef.current = url;
openAuthSessionAsync(url, APP_SCHEME, {
preferEphemeralSession: false,
});
}
});
return () => {
unsubscribe();
lastUrlRef.current = null;
handledRef.current = null;
};
}, [isAuthActive]);
}
Native passkeys use the device’s biometric prompt (Face ID, Touch ID, fingerprint) directly — no browser or portal needed. For users with a password or PIN instead of a passkey, the listener falls back to opening the portal URL.
How It Works
ParaMobile sets isNativePasskey = true internally when initializing the React Native SDK. This causes the SDK to suppress passkeyUrl from auth states. Instead:
- Signup:
passkeyId is provided in the auth state. Pass it to registerPasskey() to trigger native credential creation.
- Login:
hasPasskey is true in the auth state. Call loginWithPasskey() to trigger the device’s native biometric prompt.
These methods interact with the OS passkey APIs directly, so the user sees a native Face ID, Touch ID, or fingerprint prompt rather than a browser-based flow.
Next Steps