Skip to main content

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:
pod install

Manual XCFramework

  1. Download SmartComplySDK.xcframework from the GitHub Releases page.
  2. Drag it into your Xcode project under Frameworks, Libraries, and Embedded Content.
  3. Set Embed to Embed & Sign.

Platform Setup

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))
        }
    }
}

Forward MediaPipe results to the face engine

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

ActionEnumDescription
BlinkChallengeAction.BLINKClose and open both eyes
Turn leftChallengeAction.TURN_LEFTRotate head to the left
Turn rightChallengeAction.TURN_RIGHTRotate head to the right
Turn headChallengeAction.TURN_HEADAny horizontal head rotation
Open mouthChallengeAction.OPEN_MOUTHOpen 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
ParameterTypeDefaultDescription
apiKeyStringYour Adhere API key (required)
clientIdStringUnique per-user verification ID (required)
environmentEnvironment.SANDBOXAPI environment
requestTimeoutMsLong30000Timeout for API calls in milliseconds
uploadTimeoutMsLong60000Timeout for video upload in milliseconds
maxUploadRetriesInt3Retry 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

ErrorCauseResolution
401 UnauthorizedInvalid or expired API keyVerify your key in the Adhere Dashboard
Session expired30-minute session timeoutGenerate a new clientId and restart
Failed to capture autoshotCamera not ready when captureAutoshot() calledEnsure camera is running before calling
Submission failedNetwork error during uploadcanRetry = 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.