Skip to main content
Version: Guardian v0.5.0

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().

Version

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):

gradle.properties
SURT_GITHUB_TOKEN=<YOUR_TOKEN>

Replace <YOUR_TOKEN> with the token provided by Surt.

warning

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:

settings.gradle
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") ?: ""
}
}
}
}
settings.gradle.kts
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):

build.gradle
dependencies {
implementation 'com.surt.guardian:securitysdk:0.5.0'
}
build.gradle.kts
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:

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:

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:

AndroidManifest.xml
<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.

MyApplication.kt
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

OptionDefaultDescription
collectLocationfalseGPS coordinates - requires location permissions in manifest
collectWifiInfofalseWiFi network details
collectSimCardInfofalseSIM/carrier information - requires READ_PHONE_STATE permission
collectCameraInfofalseCamera 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:

MainActivity.kt
override fun onResume() {
super.onResume()
GuardianSDK.getInstance().setActivity(this)
}

override fun onPause() {
super.onPause()
GuardianSDK.getInstance().setActivity(null)
}
note

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):

Backend preflight request
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:

Preflight response
{
"data": {
"token": "<jwt>"
}
}

Your app fetches the JWT from your own backend endpoint:

Fetch JWT from your backend
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 per call

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.

Coroutines (recommended)
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:

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:

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.

Collect payload
// 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
JWT is optional for collect

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

VerificationResult
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.

TypeUse case
TransactionType.LOGINUser login
TransactionType.SIGN_UPNew account creation
TransactionType.DEPOSITAdding funds
TransactionType.WITHDRAWALWithdrawing funds

Troubleshooting

ErrorCauseFix
"Could not resolve com.surt.guardian:securitysdk"Token not configured or repo not addedCheck gradle.properties and settings.gradle
"401 Unauthorized" from maven.pkg.github.comToken expired or invalidGet a new token from Surt
.notInitializedverify() called before initialize()Call initialize() in Application.onCreate()
.invalidJwtJWT missing, malformed, or rejectedEnsure your backend mints a fresh JWT per call
.attestationFailedDevice integrity check failed — usually a reused JWTNever reuse a JWT across verify() calls