iOS
Native iOS integration for the Surt Guardian SDK, distributed via Swift Package Manager. 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
- iOS 14.0+
- Xcode 14+
- Swift 5.7+
1. Authentication
Add your access token to ~/.netrc so Xcode can clone the private repository:
machine github.com
login surt-customer
password <YOUR_TOKEN>
Replace <YOUR_TOKEN> with the token provided by Surt.
If ~/.netrc doesn't exist, create it. Make sure it has restricted permissions:
chmod 600 ~/.netrc
2. Add the Package
- In Xcode, go to File > Add Package Dependencies
- Enter the repository URL:
https://github.com/surtTech/surt-guardian-sdk - Set the version rule to "Up to Next Minor" from 0.5.0
- Click Add Package
- 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:
<key>NSLocationWhenInUseUsageDescription</key>
<string>Your location helps verify your identity and protect your account.</string>
If you enable camera info collection, add this key:
<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.
import SurtGuardianSDK
// In your AppDelegate or App init:
GuardianSDK.initialize(
options: GuardianOptions(
failurePolicy: .fail,
logLevel: .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 NSLocationWhenInUseUsageDescription in Info.plist |
collectWifiInfo | false | WiFi network details |
collectSimCardInfo | false | SIM/carrier information |
collectCameraInfo | false | Camera 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):
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:
// 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 immediately before calling verify(). Each JWT is single-use - reusing one is rejected by the backend.
6. Verify a Transaction
Call at security-sensitive moments (login, payment, etc.). Fetch a JWT from your backend right before each call.
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+):
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:
// 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.
// 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
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
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.
| Type | Use case |
|---|---|
.login | User login |
.signUp | New account creation |
.deposit | Adding funds |
.withdrawal | Withdrawing funds |
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| "Package resolution failed" | Token not configured or expired | Check ~/.netrc has the correct token |
.notInitialized | verify() called before initialize() | Call initialize() at app startup |
.invalidJwt | JWT missing, malformed, or rejected | Ensure your backend mints a fresh JWT per call |
.attestationFailed | A device integrity check failed - usually a reused JWT | Never reuse a JWT across verify() calls |