Zum Hauptinhalt springen
Version: Guardian v0.3.1

iOS

Native iOS integration for the Surt Guardian SDK, distributed via Swift Package Manager. The SDK collects device signals, performs platform-native attestation via App Attest, 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

  • iOS 14.0+
  • Xcode 14+
  • Swift 5.7+

1. Authentication

Add your access token to ~/.netrc so Xcode can clone the private repository:

~/.netrc
machine github.com
login surt-customer
password <YOUR_TOKEN>

Replace <YOUR_TOKEN> with the token provided by Surt.

tipp

If ~/.netrc doesn't exist, create it. Make sure it has restricted permissions:

chmod 600 ~/.netrc

2. Add the Package

  1. In Xcode, go to File > Add Package Dependencies
  2. Enter the repository URL: https://github.com/surtTech/surt-guardian-sdk
  3. Set the version rule to "Up to Next Minor" from 0.3.0
  4. Click Add Package
  5. Select the SurtGuardianSDK library and add it to your target

3. Configure Info.plist Permissions

If you enable location collection, add this key to your Info.plist:

Info.plist
<key>NSLocationWhenInUseUsageDescription</key>
<string>Your location helps verify your identity and protect your account.</string>

If you enable camera info collection, add this key:

Info.plist
<key>NSCameraUsageDescription</key>
<string>Camera access helps verify device authenticity.</string>

The SDK automatically requests the relevant permission during verification when the matching collection option is enabled. The system dialog appears automatically - no additional code is needed.

4. Initialize the SDK

Call once at app startup, before any other SDK method. No API key is required in the SDK - your server-side API key never ships to the device.

AppDelegate.swift
import SurtGuardianSDK

// In your AppDelegate or App init:
GuardianSDK.initialize(
options: GuardianOptions(
environment: .production,
failurePolicy: .fail,
logLevel: .warn,
collectLocation: true // Enable GPS collection (optional)
)
)

Environments

EnvironmentBase URL
.productionhttps://api.surt.com
.sandboxhttps://sandbox-api.surt.com

Data Collection Options

OptionDefaultDescription
collectLocationfalseGPS coordinates - requires NSLocationWhenInUseUsageDescription in Info.plist
collectWifiInfofalseWiFi network details
collectSimCardInfofalseSIM/carrier information
collectCameraInfofalseCamera count/info - requires NSCameraUsageDescription in Info.plist

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.

5. 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
// In your app - call your OWN backend, not Surt directly
func fetchVerifyJwt() async throws -> String {
let response = try await URLSession.shared.data(
for: URLRequest(url: URL(string: "https://your-api.com/geolocation-jwt")!)
)
// Parse and return the JWT string from your backend response
let body = try JSONDecoder().decode(YourJwtResponse.self, from: response.0)
return body.jwt
}
Always fetch a fresh JWT per call

Always fetch a fresh JWT immediately before calling verify(). App Attest nonces are single-use and bound to the JWT's attestation_challenge. Reusing a JWT causes attestation failure on the backend.

6. Verify a Transaction

Call at security-sensitive moments (login, payment, etc.). Fetch a JWT from your backend right before each call.

async/await (iOS 15+)
let jwt = try await fetchVerifyJwt()

let result = try await GuardianSDK.shared.verify(jwt: jwt)

if result.allowed {
// Proceed with the transaction
} else {
// Handle denied transaction (result.riskLevel has details)
}

Or with a completion handler (iOS 14+):

Completion handler (iOS 14+)
let jwt = try await fetchVerifyJwt()

GuardianSDK.shared.verify(jwt: jwt) { result in
switch result {
case .success(let verification):
if verification.allowed { /* proceed */ }
case .failure(let error):
// Handle SurtError
}
}

Per-call location override:

Per-call location override
// Force GPS on for this call, regardless of init default
let result = try await GuardianSDK.shared.verify(
jwt: jwt,
collectLocation: true
)

7. 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
let result = try await GuardianSDK.shared.collect()

// With JWT - resolves device public IP (GET /geolocation/client-ip),
// embeds it in payload (best-effort)
let jwtForCollect = try await fetchCollectJwt()
let result = try await GuardianSDK.shared.collect(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
struct VerificationResult {
let allowed: Bool // Backend decision - true = proceed
let riskLevel: RiskLevel // .low / .medium / .high / .blocked / .unknown
let sessionId: String // Transaction ID for support reference
let errors: [String]? // Backend error messages, if any
let timestamp: TimeInterval // Response timestamp
}

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
.loginUser login
.signUpNew account creation
.depositAdding funds
.withdrawalWithdrawing funds

Troubleshooting

ErrorCauseFix
"Package resolution failed"Token not configured or expiredCheck ~/.netrc has the correct token
.notInitializedverify() called before initialize()Call initialize() at app startup
.invalidJwtJWT missing, malformed, or rejectedEnsure your backend mints a fresh JWT per call
.attestationFailedNonce reuse or App Attest failureNever reuse a JWT across verify() calls