iOS SDK Integration Guide
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 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 |
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.
|
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.