[{"data":1,"prerenderedAt":198},["ShallowReactive",2],{"doc:\u002Fdocs\u002Fdevice-fingerprinting-android":3},{"page":4,"toc":174,"updated":197},{"path":5,"title":6,"seoTitle":7,"description":8,"blocks":9},"\u002Fdocs\u002Fdevice-fingerprinting-android","Add device fingerprinting to an Android app (Kotlin)","Android Device Fingerprinting in Kotlin: Step-by-Step Guide","Add device fingerprinting to an Android app in Kotlin: add the Maven dependency, identify at sign-in from a ViewModel, and verify on your server.",[10,13,18,24,27,97,100,115,117,120,122,125,129,131,134,136,139,141,144,146,149,151],{"type":11,"text":12},"p","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.",{"type":14,"level":15,"text":16,"id":17},"heading",2,"Before you start","before-you-start",{"type":19,"items":20},"list",[21,22,23],"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 `consent` is `GRANTED`.",{"type":14,"level":15,"text":25,"id":26},"Add the SDK and verify the result","add-the-sdk-and-verify-the-result",{"type":28,"steps":29},"steps",[30,35,55,74,87,92],{"title":31,"blocks":32},"Create your keys",[33],{"type":11,"text":34},"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](\u002Fdocs\u002Fapi-keys#native-apps).",{"title":36,"blocks":37},"Add the Maven repository and the dependency",[38,40,48,53],{"type":11,"text":39},"The SDK is a compiled library on Fingerly's Maven repository. Add the repository where your project declares its repositories:",{"type":41,"samples":42},"code",[43],{"label":44,"lang":45,"code":46,"check":47},"settings.gradle.kts","kotlin","dependencyResolutionManagement {\n    repositories {\n        google()\n        mavenCentral()\n        maven { url = uri(\"https:\u002F\u002Fmaven.fingerly.io\") }\n    }\n}",false,{"type":41,"samples":49},[50],{"label":51,"lang":45,"code":52,"check":47},"build.gradle.kts","dependencies {\n    implementation(\"io.fingerly:fingerly:0.2.0\")\n}",{"type":11,"text":54},"The SDK's manifest merges into yours. It declares `INTERNET` and `ACCESS_NETWORK_STATE`, and its R8 rules are bundled.",{"title":56,"blocks":57},"Load the client and identify at sign-in",[58,60,65,67,72],{"type":11,"text":59},"`load` and `identify` are `suspend` functions, and collection runs on `Dispatchers.IO`. Hold one client for the life of the process.",{"type":41,"samples":61},[62],{"label":63,"lang":45,"code":64,"check":47},"SignInViewModel.kt","import android.app.Application\nimport androidx.lifecycle.AndroidViewModel\nimport androidx.lifecycle.viewModelScope\nimport io.fingerly.sdk.Fingerly\nimport io.fingerly.sdk.core.ConsentState\nimport io.fingerly.sdk.identify\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.launch\n\nclass SignInViewModel(private val app: Application) : AndroidViewModel(app) {\n    private val fingerly = viewModelScope.async {\n        \u002F\u002F consent = GRANTED only once you have consent (or another lawful basis)\n        Fingerly.load(app, apiKey = \"fly_pk_development_…\", consent = ConsentState.GRANTED)\n    }\n\n    fun signIn(email: String, password: String) = viewModelScope.launch {\n        val result = fingerly.await().identify(tag = \"sign-in\")\n        api.signIn(email, password, requestId = result.requestId)\n    }\n}",{"type":11,"text":66},"If identification fails, carry on without a request ID and let your server treat it as missing evidence:",{"type":41,"samples":68},[69],{"label":70,"lang":45,"code":71,"check":47},"SignIn.kt","val requestId = runCatching { fingerly.identify(tag = \"sign-in\").requestId }.getOrNull()\napi.signIn(email, password, requestId)",{"type":11,"text":73},"Using Java? `FingerlyJava` is the same client with callbacks in place of `suspend` functions. See the [Android SDK page](\u002Fdocs\u002Fsdks\u002Fandroid#java).",{"title":75,"blocks":76},"Verify on your server",[77,79,85],{"type":11,"text":78},"A phone is a device you do not control, so the decision is never made in the app. Send the `requestId` to your backend with the action, read the stored event with your secret key, and check it belongs to this action and is recent.",{"type":41,"samples":80},[81],{"label":82,"lang":83,"code":84,"check":47},"Sign-in handler (Node.js)","ts","import { FingerlyAPIError, load } from '@fingerly\u002Fnode'\n\nconst fingerly = load({ secretKey: process.env.FINGERLY_SECRET_KEY! })\n\nexport async function afterPasswordCheck(requestId: unknown) {\n  if (typeof requestId !== 'string' || requestId === '') return 'second-factor' \u002F\u002F missing evidence\n\n  try {\n    const event = await fingerly.events.get(requestId)\n    if (event.tag !== 'sign-in') return 'second-factor'\n    if (Date.now() - Date.parse(event.occurred_at) > 2 * 60 * 1000) return 'second-factor'\n    return event.suspect_level === 'high' ? 'second-factor' : 'allow'\n  } catch (error) {\n    if (error instanceof FingerlyAPIError && error.code === 'event_not_found') return 'second-factor'\n    throw error \u002F\u002F still pending: answer with a retry, never a refusal\n  }\n}",{"type":11,"text":86},"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](\u002Fdocs\u002Fserver-side-verification) explains each check, and the [use-case recipes](\u002Fdocs\u002Fuse-cases) include a helper that does all of them.",{"title":88,"blocks":89},"Gate collection behind consent",[90],{"type":11,"text":91},"`consent` defaults to `PENDING`, which collects nothing. Load with `PENDING` and call `setConsent(ConsentState.GRANTED)` from your consent tool, or pass `GRANTED` as above once you have consent or another lawful basis. See [consent tools](\u002Fdocs\u002Fconsent-tools#mobile-apps).",{"title":93,"blocks":94},"Run it on a device",[95],{"type":11,"text":96},"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_emulator` signal, which is expected there.",{"type":14,"level":15,"text":98,"id":99},"What the visitor ID survives","what-the-visitor-id-survives",{"type":101,"columns":102,"rows":105},"table",[103,104],"Event","Effect on the visitor ID",[106,109,112],[107,108],"The app is closed and reopened, or updated","Unchanged.",[110,111],"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.",[113,114],"`Fingerly.deleteStoredIdentity(context)` is called","The next identification made with consent gets a new install ID and a new visitor ID.",{"type":11,"text":116},"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](\u002Fdocs\u002Fvisitor-identification#in-apps).",{"type":14,"level":15,"text":118,"id":119},"Hardware-backed attestation","hardware-backed-attestation",{"type":11,"text":121},"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](\u002Fdocs\u002Fmobile-attestation).",{"type":14,"level":15,"text":123,"id":124},"Common questions","common-questions",{"type":14,"level":126,"text":127,"id":128},3,"How do I get a unique device ID on Android?","how-do-i-get-a-unique-device-id-on-android",{"type":11,"text":130},"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`.",{"type":14,"level":126,"text":132,"id":133},"Which permissions does the SDK need?","which-permissions-does-the-sdk-need",{"type":11,"text":135},"Two, `INTERNET` and `ACCESS_NETWORK_STATE`, both granted at install time without a prompt. It never requests `QUERY_ALL_PACKAGES`. See [package visibility](\u002Fdocs\u002Fapp-store-privacy#package-visibility) for the packages it checks for.",{"type":14,"level":126,"text":137,"id":138},"Does it need Google Play services?","does-it-need-google-play-services",{"type":11,"text":140},"No. The only dependency is Kotlin coroutines, and attestation needs no Google Play services, so it also works on devices without them.",{"type":14,"level":126,"text":142,"id":143},"What happens after a reinstall?","what-happens-after-a-reinstall",{"type":11,"text":145},"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.",{"type":14,"level":126,"text":147,"id":148},"Can I call it from Java?","can-i-call-it-from-java",{"type":11,"text":150},"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.",{"type":152,"columns":15,"cards":153},"cards",[154,159,164,169],{"title":155,"text":156,"href":157,"logo":158},"Android SDK","Configuration, result, attestation and errors.","\u002Fdocs\u002Fsdks\u002Fandroid","android",{"title":160,"text":161,"href":162,"icon":163},"Mobile app attestation","Proof a report comes from your genuine app.","\u002Fdocs\u002Fmobile-attestation","shield",{"title":165,"text":166,"href":167,"icon":168},"Emulators, root and jailbreak","What the SDKs report about modified devices.","\u002Fdocs\u002Fdetect-emulators-root-jailbreak","mobile",{"title":170,"text":171,"href":172,"icon":173},"Google Play privacy","The Data safety answers for the SDK.","\u002Fdocs\u002Fapp-store-privacy","lock",[175,176,177,179,181,183,185,187,189,190,191,192,193,194,195,196],{"id":17,"text":16,"level":15},{"id":26,"text":25,"level":15},{"id":178,"text":31,"level":126},"step-create-your-keys",{"id":180,"text":36,"level":126},"step-add-the-maven-repository-and-the-dependency",{"id":182,"text":56,"level":126},"step-load-the-client-and-identify-at-sign-in",{"id":184,"text":75,"level":126},"step-verify-on-your-server",{"id":186,"text":88,"level":126},"step-gate-collection-behind-consent",{"id":188,"text":93,"level":126},"step-run-it-on-a-device",{"id":99,"text":98,"level":15},{"id":119,"text":118,"level":15},{"id":124,"text":123,"level":15},{"id":128,"text":127,"level":126},{"id":133,"text":132,"level":126},{"id":138,"text":137,"level":126},{"id":143,"text":142,"level":126},{"id":148,"text":147,"level":126},"2026-11-12T14:07:07.000Z",1794493376750]