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().
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:
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.3.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(
environment: .production,
failurePolicy: .fail,
logLevel: .warn,
collectLocation: true // Enable GPS collection (optional)
)
)
Environments
| Environment | Base URL |
|---|---|
.production | https://api.surt.com |
.sandbox | https://sandbox-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(). 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.
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 | Nonce reuse or App Attest failure | Never reuse a JWT across verify() calls |