Android
Native Android integration for the Surt Guardian SDK, distributed via Maven (GitHub Packages). The SDK collects device signals, performs platform-native attestation via the Play Integrity API, and returns the backend's risk decision.
The SDK never holds a Surt API key. Your backend holds the sp_live_* key (server-side only) and exchanges it for a short-lived JWT that the app passes to verify().
This guide covers SDK v0.3.0, which also fixes VPN detection.
Requirements
- Android SDK 24+ (Android 7.0+)
- Kotlin 1.9+
- Gradle 8+
- Java 17
1. Authentication
Add your access token to your project's gradle.properties (or ~/.gradle/gradle.properties for all projects):
SURT_GITHUB_TOKEN=<YOUR_TOKEN>
Replace <YOUR_TOKEN> with the token provided by Surt.
Do not commit gradle.properties with tokens to version control. Add it to .gitignore, or use ~/.gradle/gradle.properties instead.
2. Add the Repository
In your project's settings.gradle (or settings.gradle.kts), add the Surt Maven repository:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven {
url = uri("https://maven.pkg.github.com/surtTech/surt-guardian-sdk")
credentials {
username = "surt-customer"
password = settings.ext.find("SURT_GITHUB_TOKEN")
?: System.getenv("SURT_GITHUB_TOKEN") ?: ""
}
}
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven {
url = uri("https://maven.pkg.github.com/surtTech/surt-guardian-sdk")
credentials {
username = "surt-customer"
password = providers.gradleProperty("SURT_GITHUB_TOKEN").orNull
?: System.getenv("SURT_GITHUB_TOKEN")
}
}
}
}
3. Add the Dependency
In your app's build.gradle (or build.gradle.kts):
dependencies {
implementation 'com.surt.guardian:securitysdk:0.3.0'
}
dependencies {
implementation("com.surt.guardian:securitysdk:0.3.0")
}
Core Library Desugaring
If your app targets minSdk < 26, ensure core library desugaring is enabled in your app's build.gradle:
android {
compileOptions {
coreLibraryDesugaringEnabled true
}
}
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
}
4. AndroidManifest Permissions
If you enable location collection, add these permissions to your AndroidManifest.xml:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
If you enable SIM card info collection, add this permission:
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
The SDK automatically requests location permission during verification when collectLocation is enabled. The standard Android permission dialog appears automatically - no additional code is needed.
5. Initialize the SDK
Call once at app startup (typically in your Application class). No API key is required in the SDK - your server-side API key never ships to the device.
import android.app.Application
import com.surt.guardian.GuardianSDK
import com.surt.guardian.core.Environment
import com.surt.guardian.core.FailurePolicy
import com.surt.guardian.core.GuardianOptions
import com.surt.guardian.utils.Logger
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
GuardianSDK.initialize(
context = this,
options = GuardianOptions(
environment = Environment.Production,
failurePolicy = FailurePolicy.Fail,
logLevel = Logger.Level.WARN,
collectLocation = true // Enable GPS collection (optional)
)
)
}
}
Environments
| Environment | Base URL |
|---|---|
Environment.Production | https://api.surt.com |
Environment.Sandbox | https://sandbox-api.surt.com |
Data Collection Options
| Option | Default | Description |
|---|---|---|
collectLocation | false | GPS coordinates - requires location permissions in manifest |
collectWifiInfo | false | WiFi network details |
collectSimCardInfo | false | SIM/carrier information - requires READ_PHONE_STATE permission |
collectCameraInfo | false | Camera count/info |
When collectLocation is enabled, the SDK automatically prompts the user for location permission the first time verify() is called. If the user denies, the SDK continues without GPS data - the backend scores the transaction with less confidence.
6. Activity Reference (for automatic permission requests)
For the SDK to show the permission dialog, it needs a reference to the current Activity. Call setActivity() in your Activity lifecycle:
override fun onResume() {
super.onResume()
GuardianSDK.getInstance().setActivity(this)
}
override fun onPause() {
super.onPause()
GuardianSDK.getInstance().setActivity(null)
}
If you don't call setActivity(), the SDK still works but cannot request permissions automatically. You would need to request permissions yourself before calling verify().
7. Mint a JWT from Your Backend
Before each verify() call, your app must fetch a short-lived GeolocationJwt from your own backend. Your backend calls POST /geolocation/preflight with its server-side API key and the transaction context, then returns the JWT to the app.
Example backend call (your server, not the app):
POST /geolocation/preflight
Authorization: Bearer sp_live_xxx
Content-Type: application/json
{
"customer_id": "user_abc123",
"transaction_type": "login",
"transaction_name": "User Login",
"name": "John Doe",
"email": "john@example.com"
}
The preflight response returns the token your app needs:
{
"data": {
"token": "<jwt>"
}
}
Your app fetches the JWT from your own backend endpoint:
suspend fun fetchVerifyJwt(): String {
// Call your OWN backend endpoint, not Surt directly
val response = httpClient.post("https://your-api.com/geolocation-jwt")
return response.body<YourJwtResponse>().jwt
}
Always fetch a fresh JWT immediately before calling verify(). Play Integrity nonces are single-use and bound to the JWT's attestation_challenge. Reusing a JWT causes attestation failure on the backend.
8. Verify a Transaction
Call at security-sensitive moments (login, payment, etc.). Fetch a JWT from your backend right before each call.
val jwt = fetchVerifyJwt()
val result = GuardianSDK.getInstance().verifySuspend(jwt = jwt)
if (result.allowed) {
// Proceed with the transaction
} else {
// Handle denied transaction (result.riskLevel has details)
}
Or with a callback:
val jwt = fetchVerifyJwt()
GuardianSDK.getInstance().verify(jwt = jwt) { result ->
result.onSuccess { verification ->
if (verification.allowed) { /* proceed */ }
}
result.onFailure { error ->
// Handle SurtError
}
}
Per-call location override:
// Force GPS on for this call, regardless of init default
val result = GuardianSDK.getInstance().verifySuspend(
jwt = jwt,
collectLocation = true
)
9. Collect (Server-to-Server, Optional)
collect() returns an encrypted payload for your backend to forward to Surt. No direct SDK-to-Surt calls happen during collect.
// Without JWT - no Surt network calls, no IP in payload
val result = GuardianSDK.getInstance().collectSuspend()
// With JWT - resolves device public IP (GET /geolocation/client-ip),
// embeds it in payload (best-effort)
val jwtForCollect = fetchCollectJwt()
val result = GuardianSDK.getInstance().collectSuspend(jwt = jwtForCollect)
// Send result.payload to your backend
The JWT for collect() is optional. Pass it only to embed the device public IP. Unlike verify(), a JWT for collect() may be reused or omitted - collect() generates its own nonce internally and makes no Surt network call when no JWT is provided.
Verification Result
data class VerificationResult(
val allowed: Boolean, // Backend decision - true = proceed
val riskLevel: RiskLevel, // LOW / MEDIUM / HIGH / BLOCKED / UNKNOWN
val sessionId: String, // Transaction ID for support reference
val errors: List<String>?, // Backend error messages, if any
val timestamp: Long // Response timestamp (ms)
)
Transaction Types
These values are set by your backend in the preflight transaction_type field. They are not passed to verify() in the app.
| Type | Use case |
|---|---|
TransactionType.LOGIN | User login |
TransactionType.SIGN_UP | New account creation |
TransactionType.DEPOSIT | Adding funds |
TransactionType.WITHDRAWAL | Withdrawing funds |
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| "Could not resolve com.surt.guardian:securitysdk" | Token not configured or repo not added | Check gradle.properties and settings.gradle |
| "401 Unauthorized" from maven.pkg.github.com | Token expired or invalid | Get a new token from Surt |
.notInitialized | verify() called before initialize() | Call initialize() in Application.onCreate() |
.invalidJwt | JWT missing, malformed, or rejected | Ensure your backend mints a fresh JWT per call |
.attestationFailed | Nonce reuse or Play Integrity failure | Never reuse a JWT across verify() calls |