Pular para o conteúdo principal
Versão: Guardian v0.3.1

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

Version

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

gradle.properties
SURT_GITHUB_TOKEN=<YOUR_TOKEN>

Replace <YOUR_TOKEN> with the token provided by Surt.

aviso

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.3.0'
}
build.gradle.kts
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:

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

EnvironmentBase URL
Environment.Productionhttps://api.surt.com
Environment.Sandboxhttps://sandbox-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)
}
observação

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

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
.attestationFailedNonce reuse or Play Integrity failureNever reuse a JWT across verify() calls