Android
Native Android integration for the Surt Guardian SDK, distributed via Maven (GitHub Packages). The SDK collects device signals, runs device integrity checks, 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.5.0.
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.5.0'
}
dependencies {
implementation("com.surt.guardian:securitysdk:0.5.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.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(
failurePolicy = FailurePolicy.Fail,
logLevel = Logger.Level.WARN,
collectLocation = true // Enable GPS collection (optional)
)
)
}
}
The SDK always targets production at https://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(). Each JWT is single-use; reusing one is rejected by 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 | Device integrity check failed — usually a reused JWT | Never reuse a JWT across verify() calls |