Android SDK (Kotlin)
Complete setup guide for the VerifyStack Android SDK — Gradle installation, initialization, signal collection, and API reference.
The VerifyStack Android SDK is a native Kotlin library that collects device intelligence, behavioral biometrics, sensor fingerprints, and integrity signals on Android devices. It works with API 24+ (Android 7.0) and integrates via Gradle.
Requirements
| Requirement | Minimum |
|---|---|
| Android | API 24 (Android 7.0 Nougat) |
| Kotlin | 1.9+ |
| compileSdk | 34 |
| Gradle | 8.0+ |
| Android Gradle Plugin | 8.2+ |
Installation
Gradle (Kotlin DSL)
dependencies {
implementation("io.verifystack:sdk:1.0.0")
}Gradle (Groovy DSL)
dependencies {
implementation 'io.verifystack:sdk:1.0.0'
}Maven Repository
dependencyResolutionManagement {
repositories {
mavenCentral()
maven { url = uri("https://maven.verifystack.io") }
}
}Quick Start (3 Steps)
Step 1: Initialize in Application
import io.verifystack.sdk.VerifyStack
class MyApplication : Application() {
lateinit var verifyStack: VerifyStack
override fun onCreate() {
super.onCreate()
verifyStack = VerifyStack.Builder(this)
.apiKey("pk_live_xxxxxxxxx")
.endpoint("https://verifystack.io")
.build()
}
}Step 2: Start Tracking
class MainActivity : AppCompatActivity() {
private val vs by lazy { (application as MyApplication).verifyStack }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
vs.startTracking()
}
// Important: Forward touch events for behavioral biometrics
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
vs.onTouchEvent(event)
return super.dispatchTouchEvent(event)
}
override fun onDestroy() {
vs.stopTracking()
super.onDestroy()
}
}Step 3: Make a Decision Request
lifecycleScope.launch {
try {
val decision = vs.decide(
userId = "user_123",
action = VSAction.LOGIN
)
when (decision.decision) {
"allow" -> {
// Proceed with login
Log.d("VS", "Allowed — score: ${decision.score}")
}
"challenge" -> {
// Show 2FA or CAPTCHA
presentChallenge(decision.requestId)
}
"deny" -> {
// Block the attempt
showBlockedMessage(decision.reasons)
}
}
} catch (e: VSError) {
// Handle specific error types
Log.e("VS", "Decision error: ${e.message}")
}
}API Key — Which Key to Use
| Key Type | Prefix | Use in Android SDK? |
|---|---|---|
| Publishable | pk_live_xxxxxxxxx | ✅ Yes — always use this in the SDK |
| Secret | sk_live_xxxxxxxxx | ❌ Never — use only on your backend server |
Builder Configuration
val verifyStack = VerifyStack.Builder(context)
.apiKey("pk_live_xxxxxxxxx") // ← Always use your PUBLISHABLE key (pk_)
.endpoint("https://verifystack.io")
.timeout(10_000L) // Request timeout in ms (default: 10000)
.maxRetries(3) // Retry on transient failures (default: 3)
.enableSensors(true) // Gyroscope/accelerometer fingerprinting
.enableBehavior(true) // Touch and keystroke biometrics
.enableIntegrity(true) // Root/hooking/debugger detection
.enableAdvanced(true) // GPU fingerprint, deep identity
.cacheTTL(30_000L) // Signal cache duration in ms (default: 30000)
.debug(false) // Logcat logging (default: false)
.build()All API Methods
| Method | Description |
|---|---|
| vs.startTracking() | Begin passive behavior + sensor data collection |
| vs.stopTracking() | Stop all passive tracking |
| vs.onTouchEvent(event) | Forward touch events for biometric analysis |
| suspend vs.collect(): VSCollectionResult | Collect all signals |
| suspend vs.decide(userId, action, metadata?): VSDecideResponse | Collect signals + get fraud decision |
| suspend vs.analyze(userId?, action?): VSDecideResponse | Lightweight analysis (no auth required) |
| suspend vs.feedback(requestId, wasActuallyFraud, notes?) | Submit ground truth feedback |
| vs.destroy() | Release all resources and stop tracking |
Signal Collection (7 Phases)
| Phase | Collector | Signals | Spoofing Resistance |
|---|---|---|---|
| 1 | Device | Build fingerprint, CPU, memory, screen, battery, disk, MediaDRM ID, emulator detection | High |
| 2 | Network | WiFi/cellular, VPN/proxy detection, DNS latency, carrier, bandwidth | Medium |
| 3 | Visitor ID | EncryptedSharedPreferences-backed persistent identifier | High |
| 4 | Behavior | Touch pressure/size, swipe velocity, keystroke timing, Hurst exponent | Very High |
| 5 | Sensors | Gyroscope bias, accelerometer spectral analysis, magnetometer offset | Extreme |
| 6 | Integrity | Root detection (su/Magisk), Xposed/Frida hooking, Play Integrity API | High |
| 7 | Advanced | OpenGL renderer, thermal profile, MEMS composite, Dalvik fingerprint, Titan ID | Extreme |
Decision Response
val response = vs.decide("user_123", VSAction.PURCHASE)
response.decision // "allow" | "challenge" | "deny"
response.score // 0–100 (higher = riskier)
response.riskLevel // "low" | "medium" | "high" | "critical"
response.requestId // Unique request ID for feedback
response.reasons // listOf("new_device", "velocity_anomaly")
response.processingTimeMs // Server-side latency
// Convenience helpers
response.isAllowed // true if decision == "allow"
response.isDenied // true if decision == "deny"
response.isChallenged // true if decision == "challenge"Supported Actions
VSAction.LOGIN // User login
VSAction.SIGNUP // New account registration
VSAction.PURCHASE // Payment / checkout
VSAction.TRANSFER // Money transfer
VSAction.WITHDRAWAL // Funds withdrawal
VSAction.PASSWORD_RESET // Password reset request
VSAction.ACCOUNT_UPDATE // Profile changes
VSAction.CARD_ADD // Adding payment method
VSAction.PAGE_VIEW // Screen view
VSAction.CUSTOM // Custom actionError Handling
try {
val result = vs.decide("user_123", VSAction.LOGIN)
} catch (e: VSError) {
when (e) {
is VSError.NetworkError ->
Log.e("VS", "Network error: ${e.message}")
is VSError.ServerError ->
Log.e("VS", "Server error ${e.statusCode}: ${e.message}")
is VSError.AuthError ->
Log.e("VS", "Auth error: ${e.message}") // Invalid API key
is VSError.Timeout ->
Log.e("VS", "Request timed out")
is VSError.InvalidResponse ->
Log.e("VS", "Invalid server response")
is VSError.RateLimited ->
Log.e("VS", "Rate limited — retry after ${e.retryAfterSeconds}s")
is VSError.SdkNotInitialized ->
Log.e("VS", "SDK not initialized")
}
}Feedback Loop
lifecycleScope.launch {
vs.feedback(
requestId = decision.requestId,
wasActuallyFraud = true,
notes = "Confirmed chargeback"
)
}Jetpack Compose Integration
@Composable
fun LoginScreen(vs: VerifyStack) {
var result by remember { mutableStateOf("") }
var loading by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
// Start tracking when composable enters composition
DisposableEffect(Unit) {
vs.startTracking()
onDispose { vs.stopTracking() }
}
Column(
modifier = Modifier.fillMaxSize().padding(16.dp),
verticalArrangement = Arrangement.Center
) {
Button(
onClick = {
loading = true
scope.launch {
try {
val decision = vs.decide("user_123", VSAction.LOGIN)
result = "Decision: ${decision.decision} (score: ${decision.score})"
} catch (e: VSError) {
result = "Error: ${e.message}"
} finally {
loading = false
}
}
},
enabled = !loading
) {
Text(if (loading) "Checking..." else "Sign In")
}
if (result.isNotEmpty()) {
Text(result, style = MaterialTheme.typography.bodySmall)
}
}
}Permissions
The SDK automatically declares required permissions in its AndroidManifest.xml. These merge into your app automatically:
<!-- Required -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Optional — enhances signal quality -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />ProGuard / R8
ProGuard rules are included automatically via consumer-rules.pro. No additional configuration is needed. If you encounter issues, add:
-keep class io.verifystack.sdk.** { *; }
-keepclassmembers class io.verifystack.sdk.** { *; }Troubleshooting
| Problem | Solution |
|---|---|
| Unresolved reference: VerifyStack | Sync Gradle files. Ensure the dependency is in the correct build.gradle.kts (app module, not root). |
| Sensor signals are null | Verify enableSensors(true) in builder. Emulators have limited/no sensor hardware. |
| Touch biometrics are empty | You must override dispatchTouchEvent() in your Activity and call vs.onTouchEvent(event). |
| Play Integrity verdict is null | Ensure Google Play Services is available. Add your app's SHA-256 fingerprint in the Play Console. |
| Decision returns timeout | Increase timeout in builder config. Check network connectivity. |
| 401 Unauthorized — API key rejected | The SDK must use a publishable key (pk_live_xxxxxxxxx). If you accidentally used a secret key (sk_live_xxxxxxxxx), replace it with your pk_ key. Find both keys in Dashboard → Settings → API Keys. |
| 403 Forbidden on /feedback | The /feedback endpoint requires a secret key (sk_). You cannot call it from the SDK. Send feedback from your backend server using sk_live_xxxxxxxxx. |
| R8/ProGuard stripping SDK classes | Add the ProGuard rules shown above. Consumer rules should apply automatically. |
| Visitor ID changes on app reinstall | This is expected. Android does not persist SharedPreferences across installs. Use device fingerprinting for cross-install identity. |
Migration from v0.x
If you're upgrading from a pre-release version, the main changes are: (1) Builder pattern replaces constructor, (2) decide() is now a suspend function requiring coroutine scope, (3) VSAction is now an enum class.