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
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
contextContextrequiredAny context. The SDK keeps the application context.apiKeyStringrequiredYour public key. Its prefix decides the regional API.endpointStringDefault""A custom API origin or first-party proxy. Empty uses the region in the key.scheduleScheduleOptionsDefaultbudgetMs = 1200The collection budget:budgetMs,defaultSourceTimeoutMs,concurrencyandtiers.consentConsentStateDefaultConsentState.GRANTEDGRANTED,PENDINGorDENIED. Until it isGRANTED,identifyandcollectread and send nothing and throwConsentException. Change it withsetConsent()from any thread. See consent.
identify
tagString?Your own reference for this identification.tiersList<SourceTier>DefaultSourceTier.ALLSourceTier.FAST,SourceTier.DEFERRED, or both.submitBooleanDefaulttrueSetfalseto collect and compute verdicts without sending anything.
The result
IdentifyResult
requestIdStringIdentifies this identification. Send it to your server with the action it protects.visitorIdStringThe stable identifier the server resolved for this device.visitorIsNewBooleanWhether your organization is seeing this visitor for the first time.visitorConfidenceIntHow sure the identification is, from 0 to 100.identifiableBooleanfalsewhen the device gave too little to identify anyone.duplicateBooleantruewhen the server had already answered this request.stateStringenriched, orunavailablewhen the network lookup could not run.suspectScoreInt?The server's weighted score.nullwhen nothing was scored, which is not the same as0.suspectLevelString?low,mediumorhigh.triggersList<IdentifyTrigger>The signals the server scored:signal,group,weightandconfidence.verdictsVerdictsLocal, advisory verdicts computed on the device.reportSignalReportThe report that was sent.
Verdicts
| Verdict | What it means |
|---|---|
root | The device is rooted. |
emulator | The app is running in an emulator or a virtualised Android. |
appCloner | The app is running inside a cloning framework. |
instrumentation | An instrumentation toolkit is attached to the app. |
mitm | Something is intercepting the app's encrypted traffic. |
automation | A debugger, ADB or a test runner is driving the app. |
tampering | Methods are hooked or the app's signature has changed. |
farm | The device looks mass-provisioned or freshly reset. |
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
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 */ }
});