Skip to main content

iOS SDK Integration Guide

Documented versions. iOS SDK 1.0.11. This page follows the WIDTH integration guide dated 11 September 2026. Confirm the selected SDK build and its target environment before deployment.

Overview

The Width Identity SDK provides a drop-in identity verification flow for iOS applications. It supports KYC and KYB workflows. You import WidthIdentity and call one entry point; the SDK presents the full capture / liveness / review UI and returns a single result. The public API uses Swift-native types (enums with associated values, structs, closures) and is consumed from Swift.

Requirements

Requirement Minimum
iOS 16.0+ deployment target
Xcode 16+
Dependency manager Swift Package Manager or CocoaPods

Installation

Option A: Swift Package Manager (Recommended)

WidthIdentity-iOS is a private repository — until your GitHub account has been granted access to it, both Xcode and git report it as not found, so make sure that access is in place before you run the steps below.

In Xcode: File > Add Package Dependencies… and enter:

https://github.com/width-com/WidthIdentity-iOS.git

Or in Package.swift:

dependencies: [
    .package(url: "https://github.com/width-com/WidthIdentity-iOS.git", from: "1.0.11"),
],
targets: [
    .target(name: "MyApp", dependencies: [
        .product(name: "WidthIdentity", package: "WidthIdentity-iOS"),
    ]),
]

Option B: CocoaPods

WidthIdentity is published to Width's private Specs repo, so declare that source in your Podfile:

That Specs repo is private as well — pod install can only resolve it once your GitHub account has been granted access to it; without that access the source reads as not found.

source 'https://github.com/width-com/Specs.git'
source 'https://cdn.cocoapods.org/'   # default trunk, for any public pods

target 'YourApp' do
  pod 'WidthIdentity', '~> 1.0.11'
end

Then pod install.

Info.plist Permissions

The SDK token is no longer stored in Info.plist. It is passed at runtime via CredentialsConfig.sdkToken (see Quick Start). Declare the camera, microphone and speech-recognition usage descriptions — all three are required; the photo-library description is optional:

<!-- Required: document capture and liveness -->
<key>NSCameraUsageDescription</key>
<string>Used to capture identity documents and complete the liveness check.</string>

<!-- Required: document verification records background audio (anti-fraud) -->
<key>NSMicrophoneUsageDescription</key>
<string>Used to record background audio during document verification to help prevent fraud.</string>

<!-- Optional: only if you let users pick a document image from the photo
     library. The SDK uses the system PhotosPicker, which does not strictly
     require this key. -->
<key>NSPhotoLibraryUsageDescription</key>
<string>Used to select identity document images.</string>

<!-- Required by App Store ingestion, never requested at runtime (see below) -->
<key>NSSpeechRecognitionUsageDescription</key>
<string>This app does not request speech recognition.</string>

NSCameraUsageDescription and NSMicrophoneUsageDescription are both mandatory: the document-scan flow records background audio as an anti-fraud measure, so the app crashes at runtime without the microphone key. There is no SDK environment Info.plist key.

NSSpeechRecognitionUsageDescription is mandatory for a different reason. The SDK never asks for speech recognition, but AwsLiveness.xcframework statically absorbs Amplify's Predictions plugin, which references SFSpeechRecognizer. App Store ingestion rejects the upload without the key (error ITMS-90683) — after the whole binary has transferred, and regardless of the API never being called. State plainly in the string that the app does not use it; nothing will ever display it, because the permission is never requested.

Bundled frameworks

The package ships eight xcframeworks — WidthIdentity, SecCamera, onnxruntime, AwsLiveness, Veriff, DocumentReader, DocumentReaderCore and RegulaCommon — and embeds all of them automatically. AwsLiveness is self-contained: you do not add any Amplify or AWS dependency of your own.

Quick Start

import WidthIdentity exposes two equivalent entry styles — use whichever you prefer. Both drive the identical verification UI.

Direct input (UIKit)

import UIKit
import WidthIdentity

final class ViewController: UIViewController {
    func startVerification() throws {
        let input = try VerificationInput(
            credentials: CredentialsConfig(
                domain:   "YOUR_DOMAIN",
                sdkToken: "YOUR_SDK_TOKEN"
            ),
            customer: CustomerConfig(
                // Both customerId and email are required and must be non-empty.
                customerId: "user-123",
                email:      "user@example.com"
            )
        )

        WIDTHVerification.present(from: self, input: input) { result in
            switch result {
            case .completed(let id, let decision):  // .approved / .rejected / .review
                print("done:", id, decision)
            case .waiting(let id):                  print("waiting:", id)
            case .dismissed(let id):                print("dismissed:", id ?? "(no execution)")
            case .cancelled(let id):                print("cancelled:", id)
            case .failed(let code, let message, _): print("failed:", code, message)
            }
        }
    }
}

Direct input (SwiftUI)

import SwiftUI
import WidthIdentity

struct ContentView: View {
    @State private var presenting = false
    @State private var customerId = ""
    @State private var email = ""

    var body: some View {
        Button("Verify") { presenting = true }
            .widthVerification(isPresented: $presenting, input: {
                // Lazy closure — built only when the cover is about to present.
                try VerificationInput(
                    credentials: CredentialsConfig(
                        domain: "YOUR_DOMAIN", sdkToken: "YOUR_SDK_TOKEN"
                    ),
                    customer: CustomerConfig(customerId: customerId, email: email)
                )
            }) { result in
                // If the input closure throws, you'll receive .failed(code: "W-INIT-410", ...) here.
            }
    }
}

Handler-style entry (alternative)

WIDTHMobileSDK is an alternative, object-based entry — construct it, set properties / handler closures, then present.

import WidthIdentity

let sdk = WIDTHMobileSDK(accessToken: "YOUR_SDK_TOKEN")   // RiskSec sdkToken
sdk.credentials = .init(domain: "YOUR_DOMAIN")   // appId/apiKey decoded from accessToken
sdk.applicant   = .init(customerId: "user-123", email: "user@example.com")
sdk.dayNightMode = .night                                 // optional; omit to follow the system
sdk.language     = .zh                                     // optional; .en (default) / .zh

sdk.verificationHandler { isApproved in
    print(isApproved ? "approved" : "finally rejected")
}
sdk.onStatusDidChange { sdk, _ in
    print("status:", sdk.status, "exec:", sdk.executionId ?? "-")
    if let fail = sdk.failReason { print("failed:", fail.code, fail.message) }
}
sdk.onEvent { _, event in /* INFO/WARN/ERROR lifecycle log */ }   // optional

sdk.present(from: self)

SwiftUI:

Button("Verify") { presenting = true }
    .widthMobileSDK(isPresented: $presenting) {
        let sdk = WIDTHMobileSDK(accessToken: "YOUR_SDK_TOKEN")
        sdk.credentials = .init(domain: "YOUR_DOMAIN")   // appId/apiKey decoded from accessToken
        sdk.applicant   = .init(customerId: customerId, email: email)
        sdk.verificationHandler { isApproved in /* ... */ }
        return sdk
    }
accessToken carries the sdkToken; the other credentials are set on the client via sdk.credentials / sdk.applicant. Invalid configuration (missing credentials / applicant) surfaces as sdk.status == .failed with sdk.failReason.

Event logging (optional)

Direct-input entry takes an SdkEventLogger to receive SDK lifecycle events (INFO / WARN / ERROR) in your own pipeline (Crashlytics, DataDog, etc.); PII is redacted before the callback fires. The handler-style entry exposes the same stream via sdk.onEvent.

final class MyLogger: SdkEventLogger {
    func onEvent(level: SdkLogLevel, tag: String, message: String, error: Error?) {
        // forward to your logging backend
    }
}

let input = try VerificationInput(credentials: ..., customer: ..., eventLogger: MyLogger())

Configuration Reference

Same input structure as Android: VerificationInput, CredentialsConfig (domain + sdkToken) and CustomerConfig (customerId + email, both required). See Android Configuration Reference for field details. On iOS the VerificationInput initializer is throws and additionally accepts an optional eventLogger.

iOS-specific entry points and theme overrides:

Symbol Purpose
WIDTHVerification.present(from:input:completion:) UIKit native entry point
View.widthVerification(isPresented:input:onResult:) SwiftUI native entry point
WIDTHMobileSDK(accessToken:) + credentials/applicant + present(from:) UIKit handler-style entry
View.widthMobileSDK(isPresented:makeSDK:) SwiftUI handler-style entry
ThemeConfig / ThemeMode Optional theme override
UILanguage Optional UI language (.en default / .zh)

Handling Results

Direct-input VerificationResult

Five Swift enum cases (mirrors Android's five variants):

Case When
.completed(executionId, .approved) Server approved
.completed(executionId, .rejected) Server rejected
.completed(executionId, .review) Submission is under manual review out-of-band
.waiting(executionId) Await server notification for the final decision
.dismissed(executionId?) User exited intending to return later (resumable); executionId is nil if the workflow had not started
.cancelled(executionId) User explicitly discarded; execution destroyed, not resumable
.failed(code, message, executionId?) Internal error
Dismissed vs Cancelled .dismissed means the user paused and can resume; .cancelled means the user discarded and the server execution is destroyed (not resumable). executionId may be nil if a failure occurred before an execution was created; when present, include it in support tickets for faster troubleshooting.

Handler-style entry

verificationHandler { (isApproved: Bool) } fires on the final approve/reject decision. sdk.status (WIDTHStatus): .approved, .finallyRejected, .pending (manual review), .incomplete (mid-flow exit / waiting, resumable), .cancelled (destroyed), .failed. sdk.failReason (WIDTHException) when .failed: one of .initialization, .network, .api, .permission, .workflow, .unknown — each exposing code and message.

Theme Customization

The verification UI follows the system appearance by default. Force a mode with dayNightMode; omit it to follow the system.

let sdk = WIDTHMobileSDK(accessToken: "YOUR_SDK_TOKEN")
sdk.dayNightMode = .night   // optional; omit to follow the system

Resume & Lifecycle

Same behavior as Android. On iOS, the swipe-to-dismiss gesture on the presented sheet is disabled during an active workflow to prevent accidental progress loss.

User Choice Behavior Result Resumable?
Pause & Exit (keep progress) Server execution kept alive .dismissed Yes
End Verification (discard) Server cancel, execution destroyed .cancelled No

To resume, present the verification again with the same customerId. The SDK auto-detects in-progress workflows.

Permissions

Permission Info.plist Key Required Purpose
Camera NSCameraUsageDescription Yes Document capture, liveness
Microphone NSMicrophoneUsageDescription Yes The document-verification flow records background audio as an anti-fraud measure
Photo Library NSPhotoLibraryUsageDescription No Only if users pick a document image from the photo library (system PhotosPicker does not strictly require it)
Speech recognition NSSpeechRecognitionUsageDescription Yes Checked by App Store ingestion. The permission is never requested at runtime, so the user never sees this string
Camera and microphone are both mandatory. The document-scan step records background audio as an anti-fraud measure, so a missing NSMicrophoneUsageDescription causes a hard crash at launch. NSSpeechRecognitionUsageDescription is mandatory as well, but for App Store ingestion rather than at runtime (see Installation). There is no SDK environment Info.plist key (no location key, no token key). Network: HTTPS via URLSession, no NSAppTransportSecurity exceptions needed.

App Thinning: XCFramework supports App Thinning. Bitcode is not embedded (deprecated in Xcode 14). If you encounter issues with release builds, verify that Strip Swift Symbols (STRIP_SWIFT_SYMBOLS) is set to its default value (YES) in your app target.

Troubleshooting

Issue Solution
No such module 'WidthIdentity' SPM: File > Packages > Resolve Package Versions. Confirm the WidthIdentity library product is added under Target > General > Frameworks, Libraries, and Embedded Content. CocoaPods: declare the private source 'https://github.com/width-com/Specs.git', run pod install, and open .xcworkspace (not .xcodeproj).
.failed on launch (e.g. W-INIT-410) The VerificationInput initializer throws on a blank domain / sdkToken, or a blank customerId / email; a thrown input closure surfaces as .failed. Verify all four are non-empty.
Camera not working Ensure NSCameraUsageDescription is present in Info.plist with a non-empty description. If the user previously denied access, iOS will not re-prompt — the SDK detects this and shows a guidance screen directing the user to Settings > YourApp > Camera.
Theme colors not applied Verify hex values, case-sensitive tokens, correct theme map
SwiftUI sheet not presenting Attach .widthVerification / .widthMobileSDK to a view in the hierarchy, not conditionally
UIKit: "not in window hierarchy" Present from a view controller after viewDidAppear
Liveness screen is in the wrong language The AWS liveness screen follows the device language, not the SDK's language setting. The rest of the flow still honours sdk.language.
Upgrading: bump the version constraint (the SPM from: value or the Podfile version) and resolve packages.

Integration notes

Use the package access and installation steps above. Keep the SDK build and issued credentials aligned with the intended WIDTH environment.