> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.withpersona.com/2020-05-18/ios-sdk-v2-integration-guide/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.withpersona.com/_mcp/server.
# iOS Integration Guide
> Integrate Persona identity verification into an iOS application with UIKit or SwiftUI.
The Persona iOS SDK lets you run identity verification flows in your iOS app from UIKit or SwiftUI. The SDK takes care of the hard parts of identity capture: selfies, government ID scanning and classification, NFC passport reads, document uploads, and silent network authentication. Each flow is configured from a template you manage in the Persona Dashboard.
## Integration
The SDK requires a minimum deployment target of iOS 15.0 or later. If your app needs to support iOS versions below 15.0, please continue using the latest [v2.x release](https://github.com/persona-id/inquiry-ios-2/releases).
### Installation
#### Swift Package Manager
To install the SDK with [Swift Package Manager](https://swift.org/package-manager/):
1. Select your project’s Swift Packages tab.
2. Click on the `+` to add a package.
3. Add the repository URL `https://github.com/persona-id/inquiry-ios-2.git`, and click Next.
4. Choose the package options version rule you want to use. We recommend the default (up to next major version), and click Next.
5. Check that the package is being added to the right target and click Finish.
#### Manual Framework
The SDK is released as an XCFramework which makes manually installing the SDK a straightforward process:
1. Go to the [Persona Inquiry SDK Releases page](https://github.com/persona-id/inquiry-ios-2/releases).
2. Find the latest release.
3. Expand the `Assets` triangle and download the `PersonaSDK.xcframework.zip` file.
4. Unarchive the zip file and drag the `Persona2.xcframework` folder into your Xcode project.
5. A dialog prompt will pop up asking to choose options for adding these files. Please ensure that `Destination` has `Copy items if needed` ticked and that it will be added to the correct target.
6. Click on `Finish`.
#### CocoaPods (v2.x only)
CocoaPods is no longer published from version 3.0 onward. If you need to integrate the Persona Inquiry SDK via CocoaPods, please continue using the latest [v2.x release](https://github.com/persona-id/inquiry-ios-2/releases).
### Permissions
In addition to importing the dependency, you also need to modify your `Info.plist` and add the required permissions:
1. Navigate to your project's settings in Xcode and click the `Info` tab.
2. Add a new "Privacy - Camera Usage Description" (`NSCameraUsageDescription`) entry (if not already present) to enable camera access.
3. Add a new "Privacy - Location When In Use Usage Description" (`NSLocationWhenInUseUsageDescription`) entry (if not already present) to enable GPS access. Unfortunately, Apple does not provide tools to differentiate when the API is in use. Therefore, even if your app or inquiry flow does not utilize the GPS functionality, the usage string must be included because the Persona SDK supports the functionality.
4. \[Optional] If using our support for video verifications, add a new "Privacy - Microphone Usage Description" (`NSMicrophoneUsageDescription`) entry (if not already present) to enable microphone access.
5. \[Optional] If using our support for NFC verifications, "Privacy - NFC Scan Usage Description" (`NFCReaderUsageDescription`) entry (if not already present) to enable NFC access.
### Privacy Configuration
This SDK collects a user’s [IDFV](https://developer.apple.com/documentation/uikit/uidevice/1620059-identifierforvendor) for fraud prevention purposes. In [App Store Connect](https://appstoreconnect.apple.com/) > Your App > App Privacy, if you haven’t already add in a “Device Identifier,” and fill out the questionnaire with the following answers:
* **Usage**: App Functionality (covers fraud prevention)
* **Are the device IDs collected from this app linked to the user’s identity?** Yes
* **Do you or your third-party partners use device IDs for tracking purposes?** No
Be sure to also update your privacy manifest according to the features you are making use of from the SDK. See our [iOS Privacy Manifest](/ios-privacy-manifest) instructions for more information.
## Usage
> **Note**
>
> Starting an Inquiry from a [Persona Relay](/relay) session access token? See the [Relay iOS SDK guide](/relay-ios-sdk).
### Build and Launch an Inquiry
> **Warning**
>
> #### Persona recommends creating inquiries via API when possible
>
> Please refer to [Creating inquiries](/creating-inquiries) for more information. After getting up and running consider moving inquiry creation to your backend for security reasons.
Start an inquiry with a template ID, inquiry ID, or one-time link code. The samples below use a template ID — replace `itmpl_EXAMPLE` with your own. You can find your Inquiry Template ID in the Persona Dashboard.
When the flow is presented, the SDK takes control of the user interface. Once the flow completes, control returns to your app and the appropriate result handler is called.
#### UIKit
Build the inquiry with `Inquiry.from(templateId:delegate:)` and call `start(from:)` with the presenting view controller.
```swift
class MyViewController: UIViewController {
// This is hooked up to a button which starts the flow
@objc
private func buttonTapped(_ sender: UIButton) {
// Build the inquiry with the view controller as delegate
let inquiry = Inquiry.from(templateId: "itmpl_EXAMPLE", delegate: self)
.build()
.start(from: self) // start inquiry with view controller as presenter
}
}
```
#### SwiftUI
Attach the `personaInquiry` view modifier to any view and drive presentation with a `Bool` binding.
```swift
struct MyView: View {
@State private var showInquiry = false
var body: some View {
Button("Start verification") {
showInquiry = true
}
.personaInquiry(
isPresented: $showInquiry,
inquiryTemplate: "itmpl_EXAMPLE",
onResult: { result in
// Handle the result of the inquiry
}
)
}
}
```
### Handle Results
> **Warning**
>
> #### Do not rely on callbacks for critical business logic
>
> SDK callbacks are intended for coordination between your app's UI and Persona's UI (e.g. opening and closing the flow UI). They do NOT guarantee that data are up-to-date, and cannot be reliably used to guarantee data integrity. Webhooks should be used for logic that depends on Inquiry state.
>
> For more information, see [Accessing Inquiry status and data](/accessing-inquiry-status#webhooks-vs-sdk-callbacks).
#### UIKit
To receive the inquiry result, implement the `InquiryDelegate` protocol. For example:
```swift
extension MyViewController: InquiryDelegate {
func inquiryComplete(inquiryId: String, status: String, fields: [String : InquiryField]) {
// Inquiry completed
}
func inquiryCanceled(inquiryId: String?, sessionToken: String?) {
// Inquiry cancelled by user
}
func inquiryError(_ error: PersonaError) {
// Inquiry errored
}
// Optional: called periodically throughout the flow when events occur.
// Use for logging or analytics. The inquiry flow remains active during this callback.
func inquiryEventOccurred(event: InquiryEvent) {
switch event {
case .start(let start):
// Flow has completed initial network load
// start.inquiryId, start.sessionToken
case .pageChange(let pageChange):
// Current page changed
// pageChange.name (step name), pageChange.path (page within step)
}
}
}
```
#### SwiftUI
When presenting the inquiry from SwiftUI, handle the result with the `onResult` closure on the `personaInquiry` view modifier. It receives a `PersonaResult` value with the outcome of the flow.
```swift
SomeView()
.personaInquiry(
isPresented: $showInquiry,
inquiryTemplate: "itmpl_EXAMPLE",
onEvent: { event in
// Optional: called periodically throughout the flow when events occur.
// Use for logging or analytics. The inquiry flow remains active during this callback.
switch event {
case .start(let start):
// Flow has completed initial network load
// start.inquiryId, start.sessionToken
case .pageChange(let pageChange):
// Current page changed
// pageChange.name (step name), pageChange.path (page within step)
}
},
onResult: { result in
switch result {
case .complete(let data):
// Inquiry completed
// data.inquiryId, data.status, data.fields
case .canceled(let data):
// Inquiry cancelled by user
// data.inquiryId, data.sessionToken
case .errored(let error):
// Inquiry errored
}
}
)
```
### Handle Errors
In rare cases, the Persona Inquiry SDK can encounter client-side errors that cannot be resolved. When that happens, the SDK ends the flow and reports a typed `PersonaError` value to your app. The most common reasons include unrecoverable networking errors, misconfigured templates (should only be encountered during development), or failing to establish a connection to the device camera. `PersonaError` conforms to `LocalizedError`, so its `errorDescription` returns a human-readable string suitable for display or logging.
#### UIKit
Handle errors in your `InquiryDelegate.inquiryError(_:)` implementation.
```swift
extension MyViewController: InquiryDelegate {
func inquiryError(_ error: PersonaError) {
switch error {
case .networking:
// Show a retry prompt
case .misconfigured:
// Log a template-configuration error during development
default:
// Surface a generic error to the user
}
}
}
```
#### SwiftUI
Handle errors by switching on the `.errored` case of `PersonaResult` in the `onResult` closure.
```swift
SomeView()
.personaInquiry(
isPresented: $showInquiry,
inquiryTemplate: "itmpl_EXAMPLE",
onResult: { result in
guard case .errored(let error) = result else { return }
switch error {
case .networking:
// Show a retry prompt
case .misconfigured:
// Log a template-configuration error during development
default:
// Surface a generic error to the user
}
}
)
```
## Configuration
The builder API exposes a number of optional methods that let you tailor an inquiry to your integration — link it to a known user, pre-fill data, control which environment it runs in, and more. Chain any of the following on top of `Inquiry.from(...)` (or pass them through `personaInquiry`'s `builder:` closure in SwiftUI).
### Linking to a Persona Account
> **Note**
>
> `accountId` and `referenceId` are mutually exclusive. Setting an account ID clears any value previously set via `referenceId`.
If your integration uses [Persona accounts](https://docs.withpersona.com/managing-accounts) to associate the same user across multiple inquiries, pass the account ID (prefixed with `act_`) to the inquiry.
#### UIKit
```swift
let inquiry = Inquiry.from(templateId: "itmpl_EXAMPLE", delegate: delegate)
.accountId("act_ABC123")
.build()
```
#### SwiftUI
```swift
SomeView()
.personaInquiry(
isPresented: $showInquiry,
inquiryTemplate: "itmpl_EXAMPLE",
builder: { $0.accountId("act_ABC123") }
)
```
### Linking by Reference ID
> **Note**
>
> `accountId` and `referenceId` are mutually exclusive. Setting a reference ID clears any value previously set via `accountId`.
To make it easier to find Inquiries in the Persona Dashboard, we recommend passing in your system's user ID for the Inquiry reference ID.
#### UIKit
```swift
let inquiry = Inquiry.from(templateId: "itmpl_EXAMPLE", delegate: delegate)
.referenceId("myUser_123")
.build()
```
#### SwiftUI
```swift
SomeView()
.personaInquiry(
isPresented: $showInquiry,
inquiryTemplate: "itmpl_EXAMPLE",
builder: { $0.referenceId("myUser_123") }
)
```
### Pre-writing Fields
If you want to add extra information to the Inquiry before the user even starts, you can pass them in as `fields`.
#### UIKit
```swift
let inquiry = Inquiry.from(templateId: "itmpl_EXAMPLE", delegate: delegate)
.fields([
"name_first": .string("Alexander"),
"name_last": .string("Example")
])
.build()
```
#### SwiftUI
```swift
SomeView()
.personaInquiry(
isPresented: $showInquiry,
inquiryTemplate: "itmpl_EXAMPLE",
builder: { builder in
builder.fields([
"name_first": .string("Alexander"),
"name_last": .string("Example")
])
}
)
```
### Resuming an Inquiry
When you create an Inquiry on the server, you can pass the Inquiry ID instead of the Template ID.
#### UIKit
```swift
let inquiry = Inquiry.from(inquiryId: "inq_EXAMPLE", delegate: delegate)
.build()
```
#### SwiftUI
```swift
SomeView()
.personaInquiry(
isPresented: $showInquiry,
inquiryId: "inq_EXAMPLE"
)
```
If the Inquiry has already started, you will need to also pass in the session token.
#### UIKit
```swift
let inquiry = Inquiry.from(inquiryId: "inq_EXAMPLE", delegate: delegate)
.sessionToken("ABD1234567890")
.build()
```
#### SwiftUI
```swift
SomeView()
.personaInquiry(
isPresented: $showInquiry,
inquiryId: "inq_EXAMPLE",
builder: { $0.sessionToken("ABD1234567890") }
)
```
### Overriding Device Locale
Our SDK will automatically use the language and region selected on a users device to determine localization. If your app has specific localization requirements independent of user's device settings, you can pass the localization directly to the inquiry as follows:
#### UIKit
```swift
let inquiry = Inquiry.from(templateId: "itmpl_EXAMPLE", delegate: delegate)
.locale("fr")
.build()
```
#### SwiftUI
```swift
SomeView()
.personaInquiry(
isPresented: $showInquiry,
inquiryTemplate: "itmpl_EXAMPLE",
builder: { $0.locale("fr") }
)
```
### Selecting an Environment
By default, an inquiry runs in the `.production` environment. To start an inquiry in your sandbox while you develop, pass `.sandbox` instead. If you maintain multiple sandbox environments, you can also identify a specific one by ID.
#### UIKit
```swift
let inquiry = Inquiry.from(templateId: "itmpl_EXAMPLE", delegate: delegate)
.environment(.sandbox)
// Optional: pin to a specific environment by ID
// .environmentId("env_EXAMPLE")
.build()
```
#### SwiftUI
```swift
SomeView()
.personaInquiry(
isPresented: $showInquiry,
inquiryTemplate: "itmpl_EXAMPLE",
builder: { $0.environment(.sandbox) }
)
```
### Redirect URI
If your template uses a step that hands off to a browser or another app (for example, an external verification provider), the SDK uses the redirect URI to return the user to your app. Provide a URL whose scheme is registered for your app.
#### UIKit
```swift
let inquiry = Inquiry.from(templateId: "itmpl_EXAMPLE", delegate: delegate)
.redirectUri(URL(string: "your-app://persona-redirect"))
.build()
```
#### SwiftUI
```swift
SomeView()
.personaInquiry(
isPresented: $showInquiry,
inquiryTemplate: "itmpl_EXAMPLE",
builder: { $0.redirectUri(URL(string: "your-app://persona-redirect")) }
)
```
### Sharing Data with a Share Token
If a user has already verified themselves with another organization on Persona and consents to reuse that data with you, you can [redeem a share token](https://docs.withpersona.com/redeeming-share-tokens) (prefixed with `cnst_`) when starting an inquiry. The server pulls the previously verified fields, and the inquiry only asks the user for whatever is still missing.
#### UIKit
```swift
let inquiry = Inquiry.from(templateId: "itmpl_EXAMPLE", delegate: delegate)
.shareToken("cnst_EXAMPLE")
.build()
```
#### SwiftUI
```swift
SomeView()
.personaInquiry(
isPresented: $showInquiry,
inquiryTemplate: "itmpl_EXAMPLE",
builder: { $0.shareToken("cnst_EXAMPLE") }
)
```
## Customization
You can configure the styles that are applied to the inquiry template in the Persona Dashboard. For more information on using the theme editor, see our [help article](https://help.withpersona.com/articles/6SIHupp847yaEuVMucKAff/tutorial-configure-a-theme-with-flow-editor/).
### Theme Set
If you have multiple themes configured on the inquiry template, you can pick which one the SDK should use by passing its theme set ID (prefixed with `thm_`). If you don't set a theme set ID, the template's default theme is used.
#### UIKit
```swift
let inquiry = Inquiry.from(templateId: "itmpl_EXAMPLE", delegate: delegate)
.themeSetId("thm_ABC123")
.build()
```
#### SwiftUI
```swift
SomeView()
.personaInquiry(
isPresented: $showInquiry,
inquiryTemplate: "itmpl_EXAMPLE",
builder: { $0.themeSetId("thm_ABC123") }
)
```
### Style Variant
By default, the SDK uses the light or dark variant of the active theme to match the device's system appearance. Pass a `StyleVariant` to force a specific variant.
#### UIKit
```swift
let inquiry = Inquiry.from(templateId: "itmpl_EXAMPLE", delegate: delegate)
.styleVariant(.dark)
.build()
```
#### SwiftUI
```swift
SomeView()
.personaInquiry(
isPresented: $showInquiry,
inquiryTemplate: "itmpl_EXAMPLE",
builder: { $0.styleVariant(.dark) }
)
```
### Custom Fonts
By default, the iOS SDK only has access to the device's system font. Non system fonts can either be downloaded at runtime when uploaded to your inquiry template, or bundled into your hosting application.
Custom fonts that are not available in Persona themes are only available to customers on Enterprise plans.
**Bundling a font**
For example, if you choose the font 'Rubik' in your template's Theme configuration, you will need to add a font file named `Rubik.ttf` (or any compatible format) to your project by following the instructions [here](https://developer.apple.com/documentation/uikit/text_display_and_fonts/adding_a_custom_font_to_your_app).
If you need to use different font weights for a given family, name each font file such that the weight is appended to the end of the family name with a `-`. For example, a bold version of the `Rubik` font would be named `Rubik-Bold.ttf`. Valid font weight suffixes are `Light`, `Regular`, `Medium`, `Bold`, and `ExtraBold`.
### Initial Loading Screen
The initial loading screen is shown after the inquiry is launched and before the first server response arrives. It is the only view in the SDK that is not configured by the server; every other screen is rendered from the theme set in the Persona Dashboard. You can replace the default loading animation with your own SwiftUI view to brand this moment.
Once the first server response arrives, subsequent loading screens use the theme configured in the Persona Dashboard and are not affected by this setting.
#### UIKit
```swift
let inquiry = Inquiry.from(templateId: "itmpl_EXAMPLE", delegate: delegate)
.initialLoadingView {
VStack(spacing: 16) {
ProgressView()
Text("Preparing your verification…")
}
}
.build()
```
#### SwiftUI
```swift
SomeView()
.personaInquiry(
isPresented: $showInquiry,
inquiryTemplate: "itmpl_EXAMPLE",
builder: { builder in
builder.initialLoadingView {
VStack(spacing: 16) {
ProgressView()
Text("Preparing your verification…")
}
}
}
)
```
## Government Id NFC Integration
In order to use a template that includes Government Id NFC reading capabilities on iOS, follow these steps:
* Include the [PersonaNfc project](https://github.com/persona-id/inquiry-ios-nfc) in your app via SPM. You can include this in the same way you would the main Persona SDK. Make sure that the version of PersonaNfc matches the version of the main Persona SDK that you are using.
* Link the [PersonaOpenSSL](https://github.com/persona-id/PersonaOpenSSL) library into your app using SPM.
* Add the NFC capability to your app (target → signing & capabilities → + Capability → Near Field Communication Tag Reading). You will also need to add the NFC capability to the Identifier for the app in the Apple Developer portal.
* Make sure that the entitlements file for your app includes both `TAG` and `PACE` for the Near Field Communication Tag Reader Session Formats:
**`text`**
```Text text
com.apple.developer.nfc.readersession.formats
TAG
PACE
```
* Add a `Privacy - NFC Scan Usage Description` to your info.plist file, along with a description.
* Add a `ISO7816 application identifiers for NFC Tag Reader Session` to your info.plist file with these values **in the following order**: `A0000002471001`, `A0000002472001`, and `00000000000000`.
* Pass in `PersonaNfcAdapter()` into the Inquiry builder for the `.nfcAdapter` function. You will need to import `PersonaNfc` to access this.
## Video Integration
In order to enable video recording over WebRTC on iOS follow these steps:
* Include the [PersonaWebRtc project](https://github.com/persona-id/inquiry-ios-webrtc) in your app via SPM. You can include this in the same way you would the main Persona SDK. Make sure that the version of PersonaWebRtc matches the version of the main Persona SDK that you are using.
* Link the [WebRTC version 111.0.0](https://github.com/stasel/WebRTC) library into your app.
* Pass in `PersonaWebRtcAdapter()` into the Inquiry builder for the `.webRtcAdapter` function. You will need to import `PersonaWebRtc` to access this.
* Add a `Privacy - Microphone Usage Description` to your Info.plist file.
In order to enable local video recording upload on iOS follow these steps:
* Add a `Privacy - Microphone Usage Description` to your Info.plist file.
## Phone Number Silent Network Authentication (SNA) Integration
In order to use a template that includes phone number silent network authentication on iOS, follow these steps:
* Include the [PersonaSna project](https://github.com/persona-id/inquiry-ios-sna) in your app via SPM. You can include this in the same way you would the main Persona SDK. Make sure that the version of PersonaSna matches the version of the main Persona SDK that you are using.
* Pass in `PersonaSnaAdapter()` into the Inquiry builder for the `.snaAdapter` function. You will need to import `PersonaSna` to access this.
## Licenses
The Persona iOS SDK is shipped with the licenses for the 3rd party software that it uses. Be sure to include these licenses in your app as well. See [here](/ios-licenses) for a list of the 3rd party software that we use and their associated licenses.
> Integrate Persona identity verification into an iOS application with UIKit or SwiftUI.