Documentation Index
Fetch the complete documentation index at: https://docs.smartcomply.com/llms.txt
Use this file to discover all available pages before exploring further.
Adhere iOS SDK
The Adhere iOS SDK is built on Kotlin Multiplatform and distributed as an XCFramework. The shared business logic — session management, onboarding verification, liveness orchestration, action detection, and API communication — runs from a single Kotlin codebase. Your Swift code supplies the camera feed and UI.
Features
- Shared liveness engine — the same MediaPipe-based action detection (blink, turn, open mouth) used on Android
IosLivenessOrchestrator — Kotlin class exposed to Swift for full liveness lifecycle management
- Identity onboarding — BVN, NIN, Passport, National IDs via the
OnboardingModule
- Session management — automatic session creation and token propagation
- Face-to-ID matching — optionally compares liveness selfie against submitted document
Installation
CocoaPods
The XCFramework and its MediaPipe dependency are distributed via CocoaPods. Add to your Podfile:
platform :ios, '15.0'
target 'YourApp' do
use_frameworks!
pod 'SmartComplySDK', '~> 1.0.0'
pod 'MediaPipeTasksVision', '~> 0.10.14'
end
Then run:
Manual XCFramework
- Download
SmartComplySDK.xcframework from the GitHub Releases page.
- Drag it into your Xcode project under Frameworks, Libraries, and Embedded Content.
- Set Embed to
Embed & Sign.
Camera permission
In Info.plist:
<key>NSCameraUsageDescription</key>
<string>Camera access is required for identity liveness verification.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Microphone access is required for liveness video recording.</string>
Minimum deployment target
iOS 15.0 or later is required.
Quick Start
1. Initialise the SDK
import SmartComplySDK
let config = SDKConfig(
apiKey: "your_live_api_key",
clientId: "unique-client-id-per-user", // one-time use per verification
environment: .PRODUCTION // .SANDBOX for local dev
)
let sdk = SmartComply(config: config)
2. Create a session and fetch channels
Task {
do {
let session = try await sdk.createSession()
let sdkConfig = try await sdk.initialize()
// sdkConfig.channels is a [String: [ChannelItem]] keyed by country name
let nigeriaChannels = sdkConfig.channels["nigeria"] ?? []
let bvnChannel = nigeriaChannels.first { $0.name == "BVN" }
print("Brand:", sdkConfig.brandName)
} catch {
print("Setup failed:", error)
}
}
3. Verify identity
guard let channel = bvnChannel else { return }
let identity = try await sdk.onboarding.verify(
channelId: channel.id,
fields: ["bvn": "12345678912"]
)
print("Verification status:", identity.status)
// identity.identityCheckId is passed to liveness for face-to-ID matching
4. Start liveness
import SmartComplySDK
let params = LivenessStartParams(
identifier: "12345678912",
identifierType: "bvn",
country: "NG",
idImageBytes: nil, // optional: document photo bytes
identityCheckId: identity.identityCheckId // enables face-to-ID match
)
let orchestrator = IosLivenessOrchestrator(
livenessModule: sdk.liveness,
params: params,
actions: [.BLINK, .TURN_LEFT, .OPEN_MOUTH]
)
orchestrator.start(
onStateChanged: { state in
DispatchQueue.main.async {
self.handleState(state)
}
},
onComplete: { result, error in
if let result = result {
print("Liveness passed — entry ID:", result.id)
} else if let error = error {
print("Liveness failed:", error.message)
}
}
)
Camera Integration
The iOS SDK does not manage the camera directly. Your UIViewController or SwiftUI view is responsible for opening AVCaptureSession and feeding frames to the orchestrator.
Feed frames to the face engine
Implement AVCaptureVideoDataOutputSampleBufferDelegate and forward buffers:
extension VerificationViewController: AVCaptureVideoDataOutputSampleBufferDelegate {
func captureOutput(
_ output: AVCaptureOutput,
didOutput sampleBuffer: CMSampleBuffer,
from connection: AVCaptureConnection
) {
// Forward to video recorder (for the liveness submission video)
orchestrator.videoRecorder.appendSampleBuffer(sampleBuffer: sampleBuffer)
// Convert to bitmap and run face detection
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
let ciImage = CIImage(cvPixelBuffer: pixelBuffer)
let context = CIContext()
guard let cgImage = context.createCGImage(ciImage, from: ciImage.extent) else { return }
let uiImage = UIImage(cgImage: cgImage)
// Feed into MediaPipe FaceLandmarker (you configure this separately)
let mpImage = try? MPImage(uiImage: uiImage)
if let mpImage = mpImage {
try? faceLandmarker.detectAsync(image: mpImage, timestampInMilliseconds: Int(Date().timeIntervalSince1970 * 1000))
}
}
}
In your FaceLandmarkerLiveStreamDelegate:
func faceLandmarker(
_ faceLandmarker: FaceLandmarker,
didFinishDetection result: FaceLandmarkerResult?,
timestampInMilliseconds: Int,
error: Error?
) {
guard let result = result else { return }
// Convert and forward to the Kotlin face engine
orchestrator.faceEngine.onDetectionResult(result: result)
}
Capture the autoshot
Once the camera is ready (before liveness actions begin), capture a JPEG snapshot and pass it to the orchestrator:
func captureAutoshot() {
guard let pixelBuffer = currentPixelBuffer,
let jpegData = UIImage(ciImage: CIImage(cvPixelBuffer: pixelBuffer)).jpegData(compressionQuality: 0.9)
else { return }
orchestrator.captureAutoshot(jpegBytes: jpegData)
}
Liveness States
Handle LivenessState in your onStateChanged callback to update your UI:
func handleState(_ state: LivenessState) {
switch state {
case is LivenessState.InitializingCamera:
showMessage("Starting camera...")
case let detecting as LivenessState.DetectingFace:
instructionLabel.text = detecting.instruction
case let waiting as LivenessState.WaitingForAction:
instructionLabel.text = waiting.instruction
progressView.setProgress(
Float(waiting.completedCount) / Float(waiting.totalCount),
animated: true
)
case is LivenessState.Uploading:
showMessage("Uploading...")
case is LivenessState.Verifying:
showMessage("Verifying...")
case let success as LivenessState.Success:
showSuccess(entryId: success.response.id)
case let failed as LivenessState.Failed:
showError(message: failed.error.message ?? "Liveness check failed")
if failed.canRetry {
showRetryButton()
}
default:
break
}
}
Supported Liveness Actions
| Action | Enum | Description |
|---|
| Blink | ChallengeAction.BLINK | Close and open both eyes |
| Turn left | ChallengeAction.TURN_LEFT | Rotate head to the left |
| Turn right | ChallengeAction.TURN_RIGHT | Rotate head to the right |
| Turn head | ChallengeAction.TURN_HEAD | Any horizontal head rotation |
| Open mouth | ChallengeAction.OPEN_MOUTH | Open jaw wide |
Challenge actions are configured in your Adhere Dashboard and fetched automatically when you call sdk.initialize().
SDK Configuration
// Available environments
Environment.SANDBOX // → http://192.168.1.5:8000 (local dev)
Environment.PRODUCTION // → https://adhere-api.smartcomply.com
| Parameter | Type | Default | Description |
|---|
apiKey | String | — | Your Adhere API key (required) |
clientId | String | — | Unique per-user verification ID (required) |
environment | Environment | .SANDBOX | API environment |
requestTimeoutMs | Long | 30000 | Timeout for API calls in milliseconds |
uploadTimeoutMs | Long | 60000 | Timeout for video upload in milliseconds |
maxUploadRetries | Int | 3 | Retry attempts on failed uploads |
Cleanup
Always call cleanup() when the verification view is dismissed to release camera, recorder, and MediaPipe resources:
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
orchestrator.cleanup()
captureSession.stopRunning()
}
Error Handling
| Error | Cause | Resolution |
|---|
401 Unauthorized | Invalid or expired API key | Verify your key in the Adhere Dashboard |
Session expired | 30-minute session timeout | Generate a new clientId and restart |
Failed to capture autoshot | Camera not ready when captureAutoshot() called | Ensure camera is running before calling |
Submission failed | Network error during upload | canRetry = true — prompt user to retry |
Notes
- The KMP framework exposes Kotlin classes to Swift via Objective-C interop. All
suspend fun methods are accessible as async Swift functions via async/await.
ChallengeAction enum cases are accessed in Swift as ChallengeAction.BLINK, ChallengeAction.TURN_LEFT, etc.
LivenessState sealed class subclasses are accessed via is type checks in Swift switch statements.
- The iOS SDK does not include a pre-built SwiftUI verification flow screen — you build your own UI using the state callbacks from
IosLivenessOrchestrator.