SDKs

Android

Identify Android devices with the native Kotlin SDK. One dependency, two install-time permissions, hardware-backed attestation, and verdicts on the device.

The Android SDK is written in Kotlin. Its only dependency is Kotlin coroutines, nothing from Google Play services, and the two permissions it declares are granted at install time without a prompt.

Requirements

  • Android 5.0 (API 21) or newer.
  • Java 17 toolchain, Android Gradle Plugin 8.11 or newer.
  • A public key.

Install

dependencies {
    implementation("io.fingerly:fingerly:0.1.0")
}

The SDK's manifest merges into yours. It declares INTERNET and ACCESS_NETWORK_STATE, and a <queries> list of 17 root, hooking and cloning packages it checks for. It never requests QUERY_ALL_PACKAGES, and it reports other apps only as yes or no, never by name. To leave a package out, remove its entry with tools:node="remove"; see package visibility. Its R8 rules are bundled.

Identify a device

SignInViewModel.kt
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_production_…", 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)
    }
}

load and identify are suspend functions. Collection runs on Dispatchers.IO. Hold one client for the life of the process. consent defaults to PENDING, which collects nothing: load with PENDING and call setConsent(ConsentState.GRANTED) from your consent tool, or pass GRANTED as here once you have consent.

Send requestId to your backend with the action, and decide there after reading the stored event with a secret key. See server-side verification.

Configuration

Fingerly.load

  • contextContextrequired
    Any context. The SDK keeps the application context.
  • apiKeyStringrequired
    Your public key. Its prefix names the environment it submits to.
  • endpointStringDefault ""
    A custom API origin, to which /api/v1/ingest is appended. Empty uses https://api.fingerly.io. Proxy integrations serve the browser SDK, not apps.
  • scheduleScheduleOptionsDefault budgetMs = 1200
    The collection budget: budgetMs, defaultSourceTimeoutMs, concurrency and tiers.
  • consentConsentStateDefault ConsentState.PENDING
    GRANTED, PENDING or DENIED. Until it is GRANTED, identify and collect read and send nothing and throw ConsentException. Change it with setConsent() from any thread. See consent.

identify

  • tagString?
    Your own reference for this identification.
  • tiersList<SourceTier>Default SourceTier.ALL
    SourceTier.FAST, SourceTier.DEFERRED, or both.
  • submitBooleanDefault true
    Set false to collect and compute verdicts without sending anything.

The result

IdentifyResult

  • requestIdString
    Identifies this identification. Send it to your server with the action it protects.
  • visitorIdString
    The stable identifier the server resolved for this device.
  • visitorIsNewBoolean
    Whether your organization is seeing this visitor for the first time.
  • visitorConfidenceInt
    How sure the identification is, from 0 to 100.
  • identifiableBoolean
    false when the device gave too little to identify anyone.
  • duplicateBoolean
    true when the server had already answered this request.
  • stateString
    enriched, or unavailable when the network lookup could not run.
  • suspectScoreInt?
    The server's weighted score. null when nothing was scored, which is not the same as 0.
  • suspectLevelString?
    low, medium or high.
  • triggersList<IdentifyTrigger>
    The signals the server scored: signal, group, weight and confidence.
  • verdictsVerdicts
    Local, advisory verdicts computed on the device.
  • reportSignalReport
    The report that was sent.

Verdicts

VerdictWhat it means
rootThe device is rooted.
emulatorThe app is running in an emulator or a virtualised Android.
appClonerThe app is running inside a cloning framework.
instrumentationAn instrumentation toolkit is attached to the app.
mitmSomething is intercepting the app's encrypted traffic.
automationA debugger, ADB or a test runner is driving the app.
tamperingMethods are hooked or the app's signature has changed.
farmThe device looks mass-provisioned or freshly reset.
Verdicts.kt
import io.fingerly.sdk.core.Confidence

if (result.verdicts.root.value && result.verdicts.root.confidence == Confidence.HIGH) {
    // add friction here, and let your server make the final decision
}

The install ID

The SDK keeps one small record in a file in your app's noBackupFilesDir: a random install ID, when it was first seen, the launch count and the signing certificate digest. Auto Backup and device-to-device transfer never copy it, and it is removed when your app is uninstalled, so a reinstall starts with a new install ID and a new visitor ID. A record an earlier version kept in shared preferences is moved there once. It is read and written only while consent is granted.

The server recognises the device by this install ID and nothing else. The device signals are used for the verdicts and the score, never to identify a device or a user.

Deleting the stored identity

DeleteAccount.kt
fingerly.setConsent(ConsentState.DENIED) // if nothing should be collected afterwards
val deleted = Fingerly.deleteStoredIdentity(context) // true when no record remains

Deletes the install record, for erasure requests and account deletion. It needs no client and no consent, sends nothing, and is synchronous and thread-safe; it is a small file delete, so call it off the main thread if StrictMode matters to you. The next identification made with consent starts a new install ID that nothing links to the old one. See account deletion and erasure.

Google Play

  • The Data safety answers for what the SDK collects are in App Store and Google Play privacy.
  • Consent defaults to PENDING, so the SDK honours your app's consent decision, as Google Play's guidance on using SDKs safely asks.
  • Do not ship it in apps directed at children.

Hardware-backed attestation

On Android 7.0 (API 24) and newer, the SDK requests a one-time challenge from Fingerly and has the device keystore attest to it. The server verifies the attestation, including that it was produced for your app, and a failed check raises the tampering signal. If the challenge cannot be fetched, the SDK attests to a challenge it generates itself and identification continues; that attestation cannot prove it is fresh. There is nothing to configure. See mobile app attestation.

Errors

identify throws TransportError with status and retryable. The SDK retries IO failures, 429 and 5xx up to three attempts with one idempotency key. identify and collect throw ConsentException when consent is not GRANTED; otherwise collect() never throws.

Java

SignInActivity.java
FingerlyBridge.configure(context, "fly_pk_production_…", "", "android");

FingerlyBridge.identify("sign-in", true, new BridgeCallback() {
    @Override public void onSuccess(String json) { /* parse the result */ }
    @Override public void onError(String code, String message) { /* code is the HTTP status */ }
});

boolean deleted = FingerlyBridge.deleteStoredIdentity(context);