This tutorial adds device fingerprinting to a Kotlin Android app with the Fingerly Android SDK. It has one dependency, Kotlin coroutines, nothing from Google Play services, and two install-time permissions that need no prompt. The server recognises the device by a random install ID the SDK keeps, and your backend decides what the result means.
Before you start
- Android 5.0 (API 21) or newer.
- A public key for the development environment, and a secret key for your server.
- A decision about consent: the SDK collects nothing until
consentisGRANTED.
Add the SDK and verify the result
Step 1: Create your keys
In the Fingerly dashboard, open Integration > SDK keys and create a public key in the development environment, and a secret key in the same environment for your server. Development keys run the same detection as production and cost nothing. A production key must also name your app, an Android package or an Apple app ID, as in API keys and environments.
Step 2: Add the Maven repository and the dependency
The SDK is a compiled library on Fingerly's Maven repository. Add the repository where your project declares its repositories:
dependencyResolutionManagement { repositories { google() mavenCentral() maven { url = uri("https://maven.fingerly.io") } } }dependencies { implementation("io.fingerly:fingerly:0.2.0") }The SDK's manifest merges into yours. It declares
INTERNETandACCESS_NETWORK_STATE, and its R8 rules are bundled.Step 3: Load the client and identify at sign-in
loadandidentifyaresuspendfunctions, and collection runs onDispatchers.IO. Hold one client for the life of the process.import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import io.fingerly.sdk.Fingerly import io.fingerly.sdk.core.ConsentState import io.fingerly.sdk.identify import kotlinx.coroutines.async import kotlinx.coroutines.launch class SignInViewModel(private val app: Application) : AndroidViewModel(app) { private val fingerly = viewModelScope.async { // consent = GRANTED only once you have consent (or another lawful basis) Fingerly.load(app, apiKey = "fly_pk_development_…", consent = ConsentState.GRANTED) } fun signIn(email: String, password: String) = viewModelScope.launch { val result = fingerly.await().identify(tag = "sign-in") api.signIn(email, password, requestId = result.requestId) } }If identification fails, carry on without a request ID and let your server treat it as missing evidence:
val requestId = runCatching { fingerly.identify(tag = "sign-in").requestId }.getOrNull() api.signIn(email, password, requestId)Using Java?
FingerlyJavais the same client with callbacks in place ofsuspendfunctions. See the Android SDK page.Step 4: Verify on your server
A phone is a device you do not control, so the decision is never made in the app. Send the
requestIdto your backend with the action, read the stored event with your secret key, and check it belongs to this action and is recent.import { FingerlyAPIError, load } from '@fingerly/node' const fingerly = load({ secretKey: process.env.FINGERLY_SECRET_KEY! }) export async function afterPasswordCheck(requestId: unknown) { if (typeof requestId !== 'string' || requestId === '') return 'second-factor' // missing evidence try { const event = await fingerly.events.get(requestId) if (event.tag !== 'sign-in') return 'second-factor' if (Date.now() - Date.parse(event.occurred_at) > 2 * 60 * 1000) return 'second-factor' return event.suspect_level === 'high' ? 'second-factor' : 'allow' } catch (error) { if (error instanceof FingerlyAPIError && error.code === 'event_not_found') return 'second-factor' throw error // still pending: answer with a retry, never a refusal } }Also check the request ID has not been used before, and answer a pending event with a retry rather than a refusal. Server-side verification explains each check, and the use-case recipes include a helper that does all of them.
Step 5: Gate collection behind consent
consentdefaults toPENDING, which collects nothing. Load withPENDINGand callsetConsent(ConsentState.GRANTED)from your consent tool, or passGRANTEDas above once you have consent or another lawful basis. See consent tools.Step 6: Run it on a device
Run the app, sign in, then open Identification > Events in the dashboard to see the visitor, score and signals. In an emulator the event records the
android_emulatorsignal, which is expected there.
What the visitor ID survives
| Event | Effect on the visitor ID |
|---|---|
| The app is closed and reopened, or updated | Unchanged. |
| The app is uninstalled and installed again | A new install ID and a new visitor ID. The record lives in noBackupFilesDir, so it is removed with the app and never restored from a backup. |
Fingerly.deleteStoredIdentity(context) is called | The next identification made with consent gets a new install ID and a new visitor ID. |
Because a reinstall starts fresh, treat a new visitor ID on Android as a new install rather than proof of a new person. Link accounts to a visitor ID as evidence and not as an identity. See visitor identification.
Hardware-backed attestation
On Android 7.0 (API 24) and newer, the SDK has the device keystore attest to a one-time challenge from Fingerly, and the server verifies it. It works without configuration. To also have the server check the attestation names your app, list your package names and signing certificates on the key. See mobile app attestation.
Common questions
How do I get a unique device ID on Android?
Call identify() and read visitorId. It is a stable identifier Fingerly issues for the install, not an Android hardware identifier. The SDK never reads one such as ANDROID_ID.
Which permissions does the SDK need?
Two, INTERNET and ACCESS_NETWORK_STATE, both granted at install time without a prompt. It never requests QUERY_ALL_PACKAGES. See package visibility for the packages it checks for.
Does it need Google Play services?
No. The only dependency is Kotlin coroutines, and attestation needs no Google Play services, so it also works on devices without them.
What happens after a reinstall?
A reinstall gets a new visitor ID, because the install ID is removed with the app. Tell your server the difference between a new install and a new person by joining the visitor ID with what you already know.
Can I call it from Java?
Yes. FingerlyJava offers the same calls with callbacks, delivered on the main thread, and each call returns a Cancellation you can use when the screen goes away.