iOS SDK

Available v1.0.0 Swift 5.9+ iOS 15+ / macOS 12+ Sendable

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
callbackURLStringYesWebhook URL for verification result
minAgeInt?NoAge threshold: 12, 15, 18, 21, or 25
successURLString?NoRedirect URL on success
failedURLString?NoRedirect URL on failure
userIdString?NoYour internal user ID (passed through to webhook)
themeString?No"light" or "dark"
localeString?NoWidget language: "en", "de", "fr", etc.
metadataString?NoOpaque string (up to 500 chars) passed to webhook
livenessDifficultyString?No"easy", "medium", or "hard"
purposeString?NoVerification 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
typeStringEvent type: "session.completed", "session.failed"
data[String: Any]Event payload (session details)
idString?Unique event identifier (for deduplication)
createdInt?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
.authenticationFailed401, 403Invalid or missing API key
.validationFailed400, 422Bad request parameters, invalid webhook
.notFound404Token or resource not found
.rateLimited429Rate limit exceeded (retryAfter seconds)
.serverError5xxServer error (auto-retried by SDK)
.networkErrorn/aDNS, timeout, no connectivity (wraps underlying Error)
.invalidConfigurationn/aEmpty API key, bad URL, empty token
.invalidResponsen/aResponse 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
idStringUnique session identifier
statusSessionStatus.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
countryCodeString?ISO 3166-1 alpha-2 country code
minAgeInt?Minimum age threshold for this session
externalUserIdString?Your user ID (from initialize userId param)
requiredMethods[String]?Verification methods required
remainingAttemptsInt?Attempts remaining before failure
createdAtStringISO 8601 timestamp
completedAtString?ISO 8601 timestamp (nil if in progress)
expiresAtString?ISO 8601 timestamp

Convenience Properties

Property Type Description
isVerifiedBoolTrue if status is .completed
isFailedBoolTrue if status is .failed
isPendingBoolTrue if .pending or .inProgress
isTerminalBoolTrue if completed, failed, canceled, or claimed
ageBracketInt?Verified age bracket (12/15/18/21/25) or nil
methodString?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_KEYYour secret API key (sk_live_ or sk_test_)
XIDENT_WEBHOOK_SECRETWebhook signing secret (whsec_)

Related