Play Restore Keys · Guides
Google Play restore keys in Capacitor and Ionic apps
From April 2027, Google Play requires phone and tablet apps that sign users in to sign those users back in on a new device without asking. Google's requirement says: "Successful user sign in restoration is determined through the successful restore key retrieval." The calls are Android Kotlin APIs, so a Capacitor and Ionic app needs a small native bridge. When we searched npm on 10 October 2026 we found no Capacitor plugin for restore keys.
This guide has the native code, the bridge and what your server must do. Every API name was checked against Google's implementation guide and the androidx reference pages on 11 October 2026. It is technical help, not a Google Play compliance review.
What you need
- Android 9 or later, Google Play services core 24220000 or later and androidx.credentials 1.5.0 or later (Google's minimums; Google recommends the latest stable library).
- compileSdk 35 or later in your Android build: androidx.credentials 1.5.0 requires it.
- A relying-party server. A restore key is a passkey-style (WebAuthn) key: your server sends the options, the phone creates or signs with the key, and your server checks the result.
- Restore keys are Android only. On iOS the bridge below does nothing.
1. The native calls (Kotlin, the same for every framework)
Add the dependencies to the Android module that holds the code (Kotlin DSL shown; in a Groovy build.gradle use implementation "..."; 1.5.0 or any later version):
dependencies {
implementation("androidx.credentials:credentials:1.5.0")
implementation("androidx.credentials:credentials-play-services-auth:1.5.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
}
Then one class wraps the three calls. Creating the key with cloud backup on is Google's recommendation. Without a Google Account backup or a screen lock that throws E2eeUnavailableException, so the class tries again with cloud backup off.
package com.example.restorekeys
import android.content.Context
import androidx.credentials.ClearCredentialStateRequest
import androidx.credentials.CreateRestoreCredentialRequest
import androidx.credentials.CreateRestoreCredentialResponse
import androidx.credentials.CredentialManager
import androidx.credentials.GetCredentialRequest
import androidx.credentials.GetRestoreCredentialOption
import androidx.credentials.RestoreCredential
import androidx.credentials.exceptions.NoCredentialException
import androidx.credentials.exceptions.restorecredential.E2eeUnavailableException
// The three restore key calls. Every JSON string comes from your server or goes back to it.
class RestoreKeyClient(private val context: Context) {
private val credentialManager = CredentialManager.create(context)
// After sign-in. creationOptionsJson: your server's PublicKeyCredentialCreationOptionsJSON.
// Returns the registration response JSON for your server to verify and store.
suspend fun create(creationOptionsJson: String): String {
val response = try {
credentialManager.createCredential(context, CreateRestoreCredentialRequest(creationOptionsJson, true))
} catch (e: E2eeUnavailableException) {
// no backup or no screen lock: keep the key on the device, a direct transfer still carries it
credentialManager.createCredential(context, CreateRestoreCredentialRequest(creationOptionsJson, false))
}
return (response as CreateRestoreCredentialResponse).responseJson
}
// On first launch. requestOptionsJson: your server's PublicKeyCredentialRequestOptionsJSON.
// Returns the authentication response JSON for your server to verify, or null if there is no restore key.
suspend fun get(requestOptionsJson: String): String? = try {
val request = GetCredentialRequest(listOf(GetRestoreCredentialOption(requestOptionsJson)))
(credentialManager.getCredential(context, request).credential as RestoreCredential).authenticationResponseJson
} catch (e: NoCredentialException) {
null
}
// On sign-out. Deletes the restore key from the device and from the backup.
suspend fun clear() {
credentialManager.clearCredentialState(
ClearCredentialStateRequest(ClearCredentialStateRequest.TYPE_CLEAR_RESTORE_CREDENTIAL)
)
}
}
2. Bridge it to JavaScript
A local Capacitor plugin in your app's android/ project, next to RestoreKeyClient. It works the same in Ionic apps, since Ionic runs on Capacitor. If your Android project does not build Kotlin yet, apply the Kotlin Android Gradle plugin in android/app/build.gradle and add the step 1 dependencies there.
package com.example.restorekeys
import com.getcapacitor.JSObject
import com.getcapacitor.Plugin
import com.getcapacitor.PluginCall
import com.getcapacitor.PluginMethod
import com.getcapacitor.annotation.CapacitorPlugin
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
@CapacitorPlugin(name = "RestoreKeys")
class RestoreKeysPlugin : Plugin() {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private val keys by lazy { RestoreKeyClient(context) }
private fun answer(call: PluginCall, work: suspend () -> String?) {
scope.launch {
try {
val ret = JSObject()
work()?.let { ret.put("json", it) }
call.resolve(ret)
} catch (e: Exception) {
call.reject(e.message ?: "restore key error", e)
}
}
}
@PluginMethod
fun create(call: PluginCall) {
val json = call.getString("json") ?: return call.reject("json is required")
answer(call) { keys.create(json) }
}
@PluginMethod
fun get(call: PluginCall) {
val json = call.getString("json") ?: return call.reject("json is required")
answer(call) { keys.get(json) }
}
@PluginMethod
fun clear(call: PluginCall) = answer(call) { keys.clear(); null }
}
// MainActivity.kt: register it before super.onCreate
// override fun onCreate(savedInstanceState: Bundle?) {
// registerPlugin(RestoreKeysPlugin::class.java)
// super.onCreate(savedInstanceState)
// }
The JavaScript side:
import { Capacitor, registerPlugin } from "@capacitor/core";
const native = registerPlugin("RestoreKeys");
const android = Capacitor.getPlatform() === "android"; // restore keys exist on Android only
export const RestoreKeys = {
create: async (json) => (android ? (await native.create({ json })).json : null),
get: async (json) => (android ? (await native.get({ json })).json ?? null : null),
clear: async () => { if (android) await native.clear(); },
};
The calls, in order:
// After your normal sign-in succeeds (your server's routes; the names are examples)
const options = await api.post("/restore-key/register/options");
const registration = await RestoreKeys.create(JSON.stringify(options));
if (registration) await api.post("/restore-key/register", JSON.parse(registration)); // null on iOS
// On first launch, when no one is signed in
const request = await api.post("/restore-key/login/options");
const assertion = await RestoreKeys.get(JSON.stringify(request));
if (assertion) await api.post("/restore-key/login", JSON.parse(assertion));
// On sign-out
await RestoreKeys.clear();
Create a key after every sign-in, and once at launch for a signed-in user who has none yet, such as someone who signed in before your update. If your app has a BackupAgent, also retrieve the key in onRestoreFinished, as Google's guide recommends.
3. The server
Your server needs four routes: creation options and verification when the key is made, and request options and verification when it is used. The options use the WebAuthn JSON formats PublicKeyCredentialCreationOptionsJSON and PublicKeyCredentialRequestOptionsJSON. The user.id must be a valid WebAuthn user ID or creating the key fails. Any WebAuthn server library can check the results, with two settings for restore keys: do not require user verification, because a restore runs without the user, and store them apart from passkeys so they never show up in a user's passkey list. When the restore response checks out, start a session for that user as your sign-in normally does. A Firebase Authentication app can mint a custom token with the Admin SDK and sign in with it.
Play Restore Keys checks your package name, signing fingerprints and domain for free and lists the server routes to build. It has 10 free checks a day and needs no account. We are also considering a hosted restore-key server for Firebase Auth apps. Nothing is for sale; you can join the waitlist on the Play Restore Keys page.
4. Test it
Android Studio can back up an app and restore it onto an emulator. Follow Google's testing page with both a Device to Device and a Cloud backup. The test passes when the reinstalled app signs in by itself. Our step-by-step test guide covers what the emulator does not show.
Other frameworks: React Native and Expo · Cordova · Flutter