Set up passkeys in your Android and iOS apps
Find the SHA-256 of your signing certificate, the Team ID and bundle id, host assetlinks.json and apple-app-site-association, and add Associated Domains.
Passkeys in an Android or iOS app are the same passkeys as on your website: made for your domain and checked by the same server calls. What the app adds is proof that it belongs to the domain, through a file you host on the domain. Do the website guide first, or at least its step 1, so the dashboard knows your domain.
The SignOps phone SDKs (Android, iOS, React Native, Flutter) make the passkey calls. They are not on Maven Central, Swift Package Manager, npm or pub.dev yet; the API reference lists the calls they make.
Android
1. Find your package name and certificate fingerprint
- Package name: the
applicationIdinapp/build.gradle.kts, such ascom.kiranaexpress.app. - SHA-256 of the signing certificate, for each way the app is signed:
- Google Play: Play Console, your app, App integrity, App signing, the SHA-256 under App signing key certificate. This is the one on users’ phones.
- Debug builds from Android Studio:
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
# or, in your project:
./gradlew signingReport
Copy the line that starts SHA256:, such as 14:6D:E9:83:C5:73:...:44:E5.
2. Add the app in the dashboard
Passkeys, Setup, Android and iOS apps: type the package name and the SHA-256, and choose Add. Add one row per certificate (Play and debug). The dashboard then shows an assetlinks.json.
3. Host assetlinks.json
Put the file the dashboard shows at:
https://kiranaexpress.in/.well-known/assetlinks.json
On the domain from step 1 of the website guide (not a subdomain), over https, as application/json, with no redirect. In most frameworks that means a public/.well-known/assetlinks.json file. Check it with Google’s Statement List Generator and Tester.
4. Call the SDK
// minSdk 28. The registration token comes from your server, as for the web.
val signops = SignOps(context, "pk_live_...")
lifecycleScope.launch {
signops.passkeys.register(this@MainActivity, registrationToken) // add a passkey
val r = signops.passkeys.signIn(this@MainActivity) // sign in
api.signIn(r.signopsToken) // your server checks it with POST /v1/passkeys/verify
}
The error no_passkey means the user has none for your domain on this phone; offer your other sign-in. cancelled means they closed the prompt.
iOS
1. Find your Team ID and bundle id
- Team ID: developer.apple.com/account, Membership details.
- Bundle id: Xcode, your app target, General, Bundle Identifier.
2. Add the app in the dashboard
Passkeys, Setup, Android and iOS apps: type the Team ID and the bundle id, and choose Add. The dashboard then shows an apple-app-site-association file.
3. Host apple-app-site-association
https://kiranaexpress.in/.well-known/apple-app-site-association
The name has no .json extension, and it must still be served as application/json, over https, with no redirect. Static hosts usually need a header rule for that one path. Apple fetches the file through its own cache, so a change can take time to reach phones.
4. Add Associated Domains in Xcode
Your app target, Signing and Capabilities, + Capability, Associated Domains, then add:
webcredentials:kiranaexpress.in
While developing, use webcredentials:kiranaexpress.in?mode=developer and turn on Associated Domains Development under Settings, Developer on the iPhone. The phone then reads the file from your site directly instead of Apple’s cache. Remove ?mode=developer before you ship.
5. Call the SDK
// iOS 16 and later
let signops = SignOps(key: "pk_live_...")
let made = try await signops.passkeys.register(token: registrationToken, anchor: view.window!)
let r = try await signops.passkeys.signIn(anchor: view.window!, autofill: true)
try await api.signIn(r.signopsToken) // your server checks it with POST /v1/passkeys/verify
With autofill: true, the passkey is offered in the keyboard on a text field with textContentType = .username.
React Native and Flutter
The same two steps (dashboard, then the hosted file and Associated Domains) apply. In code:
// React Native, @signops/react-native
await signops.passkeys.register(registrationToken);
const { signopsToken } = await signops.passkeys.signIn();
// Flutter, signops
await signops.passkeys.register(registrationToken);
final signedIn = await signops.passkeys.signIn(); // signedIn.signopsToken to your server
Flutter’s MainActivity must extend FlutterFragmentActivity, and both need Android minSdk 28 and iOS 16.
Your server does not change
The registration token and the check after sign-in are the same calls as for the website. One server endpoint serves the browser, the Android app and the iOS app.