iOS SDK
The iOS SDK (XidentSDK) is a Swift client for the Xident age verification API. It uses async/await, Apple's CryptoKit for HMAC webhook verification, and is fully Sendable-safe for Swift concurrency. Supports iOS 15+ and macOS 12+.
Secret key required: The iOS SDK is a server-side SDK (for Vapor, Hummingbird, or any Swift backend). Use your secret key (sk_live_ / sk_test_) from the dashboard. For mobile apps, call your own backend which uses this SDK -- never embed the secret key in app bundles.
Installation
Swift Package Manager
// Package.swift
dependencies: [
.package(url: "https://github.com/xident-io/ios-sdk", from: "1.0.0")
]
// In your target:
.target(name: "YourApp", dependencies: [
.product(name: "XidentSDK", package: "xident-swift"),
]) Quick Start
import XidentSDK
let xident = Xident(apiKey: "sk_test_xxx")
// 1. Create a verification session
let init = try await xident.verification.initialize(
callbackURL: "https://yoursite.com/webhook",
minAge: 18
)
print("Redirect user to: \(init.verifyURL)")
// 2. After user completes verification, check result
let session = try await xident.verification.getResult(token: init.token)
if session.isVerified {
print("Verified! Age bracket: \(session.ageBracket ?? 0)")
} Configuration
Create a client with just an API key (production defaults apply) or pass a XidentConfig struct for full control.
// Default configuration (production-ready)
let xident = Xident(apiKey: "sk_live_xxx")
// Custom configuration
let xident = Xident(apiKey: "sk_live_xxx", config: XidentConfig(
baseURL: URL(string: "https://staging-api.xident.io")!,
timeout: 60, // seconds
maxRetries: 5,
headers: ["X-Custom": "value"]
))
// Custom URLSession (for testing)
let config = URLSessionConfiguration.ephemeral
config.timeoutIntervalForRequest = 15
let xident = Xident(
apiKey: "sk_live_xxx",
urlSessionConfiguration: config
) | Option | Default | Description |
|---|---|---|
baseURL | https://api.xident.io | API base URL. Override for staging or self-hosted. |
timeout | 30 (seconds) | Request timeout. Minimum 1 second. |
maxRetries | 3 | Max retries on 5xx and network errors. Set to 0 to disable. |
headers | [:] | Extra HTTP headers sent with every request. |
Verification
initialize (Create Session)
Create a verification session. Returns a short-lived token (10-minute TTL) and a URL to redirect the user to. This is an async throws function.
let result = try await xident.verification.initialize(
// Required
callbackURL: "https://yoursite.com/webhook",
// Optional
minAge: 18, // 12, 15, 18, 21, or 25
successURL: "https://yoursite.com/success",
failedURL: "https://yoursite.com/failed",
userId: "user_123", // your internal user ID
theme: "dark", // "light" or "dark"
locale: "de", // "en", "de", "fr", etc.
metadata: "order_456", // up to 500 chars, passed to webhook
livenessDifficulty: "hard", // "easy", "medium", "hard"
purpose: "age_verification"
)
// result.token -> "xit_abc123..." (10-minute TTL)
// result.verifyURL -> "https://verify.xident.io/v/xit_abc123" Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
callbackURL | String | Yes | Webhook URL for verification result |
minAge | Int? | No | Age threshold: 12, 15, 18, 21, or 25 |
successURL | String? | No | Redirect URL on success |
failedURL | String? | No | Redirect URL on failure |
userId | String? | No | Your internal user ID (passed through to webhook) |
theme | String? | No | "light" or "dark" |
locale | String? | No | Widget language: "en", "de", "fr", etc. |
metadata | String? | No | Opaque string (up to 500 chars) passed to webhook |
livenessDifficulty | String? | No | "easy", "medium", or "hard" |
purpose | String? | No | Verification purpose (shown to user) |
getResult (Check Session)
Retrieve the verification result for a token. Never trust URL parameters alone -- always re-verify server-side.
let session = try await xident.verification.getResult(token: "xit_abc123")
print("Status: \(session.status)") // .completed, .failed, .pending, etc.
print("Verified: \(session.isVerified)") // true if completed
print("Failed: \(session.isFailed)") // true if failed
print("Pending: \(session.isPending)") // true if pending or inProgress
print("Terminal: \(session.isTerminal)") // true if no more changes possible
print("Age bracket: \(session.ageBracket ?? 0)") // Int?: 12, 15, 18, 21, 25, or nil
print("Method: \(session.method ?? \"unknown\")") // "ml_fast", "ocr", "self_declaration" Webhook Verification
Xident sends webhooks to your callbackURL when verification sessions complete, fail, or expire. The X-Xident-Signature header uses HMAC-SHA256 with the format t=TIMESTAMP,v1=HMAC_HEX. The SDK uses Apple CryptoKit's HMAC.isValidAuthenticationCode() for constant-time comparison to prevent timing attacks.
// In your server-side webhook endpoint
func handleWebhook(payload: String, signatureHeader: String) throws {
let xident = Xident(apiKey: "sk_live_xxx")
let event = try xident.webhooks.constructEvent(
payload: payload,
signature: signatureHeader,
secret: "whsec_your_webhook_secret",
tolerance: 300 // max age in seconds (default: 300 = 5 min)
)
switch event.type {
case "session.completed":
let sessionId = event.data["session_id"] as? String
print("Verification completed for session: \(sessionId ?? "")")
// Grant access
case "session.failed":
print("Verification failed")
case "session.expired":
print("Verification expired")
default:
print("Unhandled event type: \(event.type)")
}
}
// Verify signature only (returns Bool, throws on invalid)
try xident.webhooks.verifySignature(
payload: payload,
signature: signatureHeader,
secret: "whsec_xxx"
)
// Parse event without signature verification (not recommended)
let event = try xident.webhooks.parseEvent(payload: payload) WebhookEvent Fields
| Field | Type | Description |
|---|---|---|
type | String | Event type: "session.completed", "session.failed" |
data | [String: Any] | Event payload (session details) |
id | String? | Unique event identifier (for deduplication) |
created | Int? | Unix timestamp of event creation |
Error Handling
The SDK throws XidentError, a Swift enum with associated values. Each case carries context for debugging: HTTP status code, error message, and request ID.
do {
let result = try await xident.verification.initialize(
callbackURL: "https://yoursite.com/callback",
minAge: 18
)
print("Token: \(result.token)")
} catch let error as XidentError {
switch error {
case .authenticationFailed(_, let message, _):
// HTTP 401/403 -- invalid or missing API key
print("Auth error: \(message)")
case .validationFailed(_, let message, _):
// HTTP 400/422 -- bad request parameters
print("Validation error: \(message)")
case .notFound(_, let message, _):
// HTTP 404 -- token or resource not found
print("Not found: \(message)")
case .rateLimited(_, let retryAfter, _):
// HTTP 429 -- rate limited
if let seconds = retryAfter {
print("Rate limited. Retry after \(seconds)s")
}
case .serverError(_, let message, _):
// HTTP 5xx -- server error (SDK auto-retries)
print("Server error: \(message)")
case .networkError(let underlying):
// DNS, timeout, no connectivity
print("Network error: \(underlying.localizedDescription)")
case .invalidConfiguration(let reason):
// Empty API key, bad URL, etc.
print("Config error: \(reason)")
case .invalidResponse:
// Response body could not be decoded
print("Invalid API response")
}
} catch {
print("Unexpected error: \(error)")
} Error Cases
| Case | HTTP Status | Description |
|---|---|---|
.authenticationFailed | 401, 403 | Invalid or missing API key |
.validationFailed | 400, 422 | Bad request parameters, invalid webhook |
.notFound | 404 | Token or resource not found |
.rateLimited | 429 | Rate limit exceeded (retryAfter seconds) |
.serverError | 5xx | Server error (auto-retried by SDK) |
.networkError | n/a | DNS, timeout, no connectivity (wraps underlying Error) |
.invalidConfiguration | n/a | Empty API key, bad URL, empty token |
.invalidResponse | n/a | Response body could not be decoded |
Framework Examples
Vapor
// Vapor 4 server-side Swift
import Vapor
import XidentSDK
func routes(_ app: Application) throws {
let xident = Xident(apiKey: Environment.get("XIDENT_SECRET_KEY")!)
let webhookSecret = Environment.get("XIDENT_WEBHOOK_SECRET")!
app.post("verify") { req async throws -> Response in
let result = try await xident.verification.initialize(
callbackURL: "https://example.com/webhook",
minAge: 18,
successURL: "https://example.com/success",
failedURL: "https://example.com/failed"
)
return try await req.respond(json: [
"token": result.token,
"verify_url": result.verifyURL,
])
}
app.post("webhook") { req -> HTTPStatus in
let body = try req.content.decode(String.self)
let signature = req.headers.first(name: "X-Xident-Signature") ?? ""
let event = try xident.webhooks.constructEvent(
payload: body,
signature: signature,
secret: webhookSecret
)
switch event.type {
case "session.completed":
req.logger.info("Verification completed: \(event.data)")
case "session.failed":
req.logger.info("Verification failed")
default:
break
}
return .ok
}
app.get("result", ":token") { req async throws -> Response in
let token = req.parameters.get("token")!
let session = try await xident.verification.getResult(token: token)
return try await req.respond(json: [
"verified": session.isVerified,
"age_bracket": session.ageBracket as Any,
"method": session.method as Any,
])
}
} Polling Pattern
// Poll for verification result until terminal state
func pollForResult(token: String) async throws -> SessionResult {
let xident = Xident(apiKey: "sk_live_xxx")
let maxAttempts = 30
for _ in 0..<maxAttempts {
let session = try await xident.verification.getResult(token: token)
if session.isTerminal {
return session
}
try await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds
}
throw NSError(domain: "Verification", code: -1, userInfo: [
NSLocalizedDescriptionKey: "Verification timed out after polling"
])
} Response Types
SessionResult
| Property | Type | Description |
|---|---|---|
id | String | Unique session identifier |
status | SessionStatus | .pending, .inProgress, .completed, .failed, .canceled, .claimed |
livenessResult | [String: AnyCodable]? | Liveness check outcome |
ageResult | [String: AnyCodable]? | Age verification outcome |
ocrResult | [String: AnyCodable]? | Document OCR outcome |
faceMatchResult | [String: AnyCodable]? | Face matching outcome |
countryCode | String? | ISO 3166-1 alpha-2 country code |
minAge | Int? | Minimum age threshold for this session |
externalUserId | String? | Your user ID (from initialize userId param) |
requiredMethods | [String]? | Verification methods required |
remainingAttempts | Int? | Attempts remaining before failure |
createdAt | String | ISO 8601 timestamp |
completedAt | String? | ISO 8601 timestamp (nil if in progress) |
expiresAt | String? | ISO 8601 timestamp |
Convenience Properties
| Property | Type | Description |
|---|---|---|
isVerified | Bool | True if status is .completed |
isFailed | Bool | True if status is .failed |
isPending | Bool | True if .pending or .inProgress |
isTerminal | Bool | True if completed, failed, canceled, or claimed |
ageBracket | Int? | Verified age bracket (12/15/18/21/25) or nil |
method | String? | Verification method used or nil |
Note: Swift uses computed properties (not methods) for these -- session.isVerified not session.isVerified().
Environment Variables
| Variable | Description |
|---|---|
XIDENT_SECRET_KEY | Your secret API key (sk_live_ or sk_test_) |
XIDENT_WEBHOOK_SECRET | Webhook signing secret (whsec_) |
Related
- JavaScript SDK -- Client-side browser integration
- Android SDK -- Kotlin SDK for Android/JVM
- Go SDK -- Server-side Go alternative
- All SDKs -- Overview of all Xident SDKs
- API Reference -- Full REST API documentation