> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.withpersona.com/2021-05-14/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.