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 small <queries> list of package names it checks for. It never requests QUERY_ALL_PACKAGES. 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.identify
import kotlinx.coroutines.async
import kotlinx.coroutines.launch

class SignInViewModel(private val app: Application) : AndroidViewModel(app) {
    private val fingerly = viewModelScope.async {
        Fingerly.load(app, apiKey = "fly_pk_us_production_…")
    }

    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.

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 decides the regional API.
  • endpointStringDefault ""
    A custom API origin or first-party proxy. Empty uses the region in the key.
  • scheduleScheduleOptionsDefault budgetMs = 1200
    The collection budget: budgetMs, defaultSourceTimeoutMs, concurrency and tiers.
  • consentConsentStateDefault ConsentState.GRANTED
    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
}

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_us_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 */ }
});