> 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/tutorial-ios-sdk-precreate/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.withpersona.com/_mcp/server. # Tutorial: Pre-create inquiries for iOS > Pre-create an Inquiry through the API before launching the Persona iOS SDK. The Persona iOS SDK lets you integrate identity verification directly into your native iOS app. There are [two ways to use the iOS SDK](/choosing-an-integration-method#mobile-sdk): 1. **Generate inquiries from an inquiry template** (Minimal code required) 2. **Pre-create inquiries via API** (More code required) This guide walks you through the second method: pre-creating inquiries via API. This is the method we recommend you use in production. You will: * Create a backend server with an endpoint that: * Creates inquiries with prefilled user data * Checks for existing inquiries to avoid duplicates * Enables the user to resume partially-completed inquiries by providing a session token * Create an iOS app that: * Retrieves an inquiry ID from the backend for the current user * Displays the Persona verification flow using the inquiry ID > **Production note** > > The sample code in this guide illustrates an approach that we recommend in production. > > However, for demonstration purposes, the code itself is simplified and not production-ready. For example, it does not include: > > * Authentication > * Fetching real user information from a database > * Error handling and retry logic > * Monitoring > **Alternative: Generate inquiries from template** > > Pre-creating inquiries (the method shown in this guide) is recommended for production use. However, if you're looking for the fastest way to test the iOS SDK, see the [inquiry template approach](/ios-sdk-v2-integration-guide#build-and-launch-the-inquiry). ## Prerequisites You'll need: * A Persona account with a [Sandbox API key](/api-keys) * macOS with Xcode installed * Python 3.7+ installed locally (for the backend server) * This guide provides sample backend code in Python, but you can adapt it to any language. * Basic SwiftUI knowledge * This guide provides sample iOS code using SwiftUI, but you can adapt it to UIKit. Before you start, you should: * Understand what an [inquiry](/inquiries) is * Sign into the [Persona dashboard](https://help.withpersona.com/articles/3QGnmQLLnykxUkPl1wIdLT/) and switch into your [Sandbox environment](/environments) ## Scenario A user named Alexander Sample just joined your dog walking app as a dog walker. You want to verify his identity to ensure the safety of users on your service. Alexander's user ID in *your* app is "usr\_ABC123". During account signup in your app, he stated his birthdate is August 31, 1977. ## Step 1: Create an inquiry template Every inquiry is created from an [inquiry template](/inquiry-templates), which defines details like the specific verification logic and UI text and branding of that inquiry. You can think of inquiry templates as a mold that lets you create many inquiries. Persona offers a suite of [solutions](https://help.withpersona.com/solutions/all-solutions/) that include preconfigured inquiry templates. In this tutorial, use the "KYC" solution to verify your dog walkers. **Follow [these instructions](https://help.withpersona.com/articles/67J7FurQtIgwxkWWvUropu/)** to add the "KYC" solution to your Sandbox environment. ## Step 2: Locate the inquiry template ID Find the ID of the newly-created inquiry template. In the Persona dashboard, navigate to **Inquiries** > **Templates**. Find the "KYC" template in the list of inquiry templates, and note the value in the `ID` field. The value should begin with `itmpl_`. ## Step 3: Create the backend API Create a backend server with one endpoint that returns an inquiry for the current logged-in user. If the user has an in-progress inquiry, the backend will return that inquiry's ID. Otherwise, the backend will create a new inquiry and return its ID. This is the same backend from the [Embedded Flow Pre-create guide](/quickstart-embedded-flow-precreate). 1. Create a new directory for the backend: ```bash mkdir ios-demo-backend cd ios-demo-backend ``` 2. Create `server.py` with the following code: **`ios-demo-backend/server.py`** ```python ios-demo-backend/server.py from flask import Flask, request, jsonify from flask_cors import CORS import requests import os app = Flask(__name__) CORS(app) PERSONA_API_KEY = os.environ.get('PERSONA_API_KEY') PERSONA_API_URL = 'https://withpersona.com/api/v1' PERSONA_VERSION = '2025-10-27' INQUIRY_TEMPLATE_ID = 'itmpl_XXXXXXXXXXXXX' # Replace with your template ID def _get_persona_req_headers(): return { 'Authorization': f'Bearer {PERSONA_API_KEY}', 'Content-Type': 'application/json', 'Persona-Version': PERSONA_VERSION } @app.route('/api/inquiries/get-or-create', methods=['POST']) def get_or_create_inquiry(): # In production, get user ID from your auth system user_id = 'usr_ABC123' # In production, fetch user data from your database user_data = { 'name_first': 'Alexander', 'name_last': 'Sample', 'birthdate': '1977-08-31' } # Check for existing incomplete inquiry existing = find_incomplete_inquiry(user_id) if existing: inquiry_id = existing['id'] status = existing['attributes']['status'] print(f"Found existing inquiry {inquiry_id} with status: {status}") if status == 'pending': print("Inquiry is pending, generating session token to resume") session_token = create_session_token(inquiry_id) return jsonify({ 'inquiry_id': inquiry_id, 'session_token': session_token }) else: print("Inquiry is created, resuming without session token") return jsonify({ 'inquiry_id': inquiry_id, 'session_token': None }) else: print("No incomplete inquiry found, creating new one") inquiry_id = create_inquiry(user_id, user_data) return jsonify({ 'inquiry_id': inquiry_id, 'session_token': None }) def find_incomplete_inquiry(user_id): """Find existing inquiry with status 'created' or 'pending'""" try: response = requests.get( f"{PERSONA_API_URL}/inquiries", params={ 'filter[reference-id]': user_id, 'filter[status]': 'created,pending', 'page[size]': 1 }, headers=_get_persona_req_headers() ) response.raise_for_status() inquiries = response.json().get('data', []) return inquiries[0] if inquiries else None except Exception as e: print(f"Error finding inquiry: {e}") return None def create_inquiry(user_id, user_data): """Create new inquiry via Persona API""" payload = { 'data': { 'attributes': { 'inquiry-template-id': INQUIRY_TEMPLATE_ID, 'reference-id': user_id, 'fields': user_data } } } response = requests.post( f"{PERSONA_API_URL}/inquiries", json=payload, headers=_get_persona_req_headers() ) response.raise_for_status() return response.json()['data']['id'] def create_session_token(inquiry_id): """Generate session token for resuming pending inquiry""" try: response = requests.post( f"{PERSONA_API_URL}/inquiries/{inquiry_id}/resume", json={'meta': {}}, headers=_get_persona_req_headers() ) response.raise_for_status() meta = response.json().get('meta', {}) return meta.get('session-token') except Exception as e: print(f"Error creating session token: {e}") return None if __name__ == '__main__': app.run(port=8000, debug=True) ``` In this code, replace `itmpl_XXXXXXXXXXXXX` with your inquiry template ID from Step 2. 3. Create the `requirements.txt` file: **`ios-demo-backend/requirements.txt`** ```txt ios-demo-backend/requirements.txt flask flask-cors requests ``` 4. Install required dependencies: ```bash # Create a virtual environment cd ios-demo-backend python -m venv venv # Activate the virtual environment source venv/bin/activate # Install dependencies pip install -r requirements.txt ``` 5. Set your Persona Sandbox API key as an environment variable: ```bash export PERSONA_API_KEY="your_sandbox_api_key_here" ``` 6. Start the server: ```bash cd ios-demo-backend python server.py ``` You should see: ``` * Running on http://127.0.0.1:8000 * Debug mode: on ``` Keep this terminal window open. ### About the backend code The backend is identical to the Embedded Flow Pre-create guide. See [that guide's explanation](/quickstart-embedded-flow-precreate#about-the-backend-code) for details on: * Finding incomplete inquiries * Resuming inquiries with session tokens * Securely prefilling the inquiry with user data and providing a reference ID ## iOS app architecture In the next steps, you will construct the iOS app. Here's how the pieces fit together: **View Model:** * **OnboardingViewModel**: Communicates with the backend API and manages state used by the views **Views:** * **OnboardingView**: The step in Alexander's user onboarding flow that shows a "Start Verifying" button * **PersonaFlowPresenter**: View that wraps the Persona iOS SDK for use in SwiftUI. Is presented by OnboardingView. * **FinishedView**: The screen shown to Alexander after he completes verification * **ContentView**: View that provides simple logic to show OnboardingView versus FinishedView. Creates and owns the view model. **UX flow:** 1. Alexander taps "Start Verifying" in OnboardingView 2. OnboardingViewModel calls backend API 3. Backend returns inquiry ID (and session token if resuming pending inquiry) 4. PersonaFlowPresenter presents Persona verification 5. Alexander completes verification and sees FinishedView Next, you'll create the app and build the view model first, then the views. ## Step 4: Create iOS app with Persona SDK Create a new iOS app that will display the Persona verification flow. 1. Open Xcode and create a new iOS App project: * Product Name: `Pawsona` * Interface: SwiftUI * Language: Swift 2. Install the Persona iOS SDK using Swift Package Manager: * In Xcode, go to **File** > **Add Package Dependencies** * Enter the repository URL: `https://github.com/persona-id/inquiry-ios-2` * Select "Up to Next Major Version" with the current version of the SDK * Click **Add Package** ## Step 5: Create the view model Create a new Swift file called `OnboardingViewModel.swift`: **`OnboardingViewModel.swift`** ```swift OnboardingViewModel.swift import Foundation import Combine import Persona2 @MainActor class OnboardingViewModel: ObservableObject { @Published var isLoading = false @Published var errorMessage: String? @Published var showPersonaFlow = false @Published var showSuccess = false @Published var inquiryId: String? @Published var sessionToken: String? @Published var completedInquiryId: String? @Published var completedInquiryStatus: String? // Replace with your backend URL // For iOS Simulator: use http://localhost:8000 // For physical device: use your computer's IP address (e.g., http://192.168.1.100:8000) private let backendURL = "http://localhost:8000/api/inquiries/get-or-create" func startVerification() async { isLoading = true errorMessage = nil guard let url = URL(string: backendURL) else { errorMessage = "Invalid server URL" isLoading = false return } var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") do { let (data, _) = try await URLSession.shared.data(for: request) let response = try JSONDecoder().decode(InquiryResponse.self, from: data) inquiryId = response.inquiryId sessionToken = response.sessionToken showPersonaFlow = true } catch { errorMessage = "Failed to verify: \(error.localizedDescription)" } isLoading = false } func handleCompletion(inquiryId: String, status: String) { print("Verification completed. Inquiry ID: \(inquiryId), Status: \(status)") completedInquiryId = inquiryId completedInquiryStatus = status showPersonaFlow = false showSuccess = true } func handleCancellation() { print("User cancelled verification") showPersonaFlow = false } func handleEvent(_ event: InquiryEvent) { switch event { case .start(let startEvent): print("Inquiry started: \(startEvent.inquiryId) with session token: \(startEvent.sessionToken)") case .pageChange(let pageChangeEvent): print("Page changed to: \(pageChangeEvent.name)") default: print("Inquiry event: \(event)") } } func handleError(_ error: PersonaError) { print("Verification error: \(error.localizedDescription)") showPersonaFlow = false errorMessage = "Verification failed. Please try again." } } struct InquiryResponse: Codable { let inquiryId: String let sessionToken: String? enum CodingKeys: String, CodingKey { case inquiryId = "inquiry_id" case sessionToken = "session_token" } } ``` ### About the view model This view model handles the following tasks: **Fetching inquiry from backend:** Your iOS app fetches an inquiry from your backend. This is the key part of the "Pre-create" approach: **`OnboardingViewModel.swift`** ```swift OnboardingViewModel.swift func startVerification() { … // Call backend API var request = URLRequest(url: url) request.httpMethod = "POST" … // Parse response if let inquiryId = json["inquiry_id"] as? String { self.inquiryId = inquiryId self.sessionToken = json["session_token"] as? String self.showPersonaFlow = true } } ``` **Handling callbacks:** The view model handles callbacks from the Persona SDK: * `handleCompletion`: Called when verification finishes successfully * `handleCancellation`: Called when user cancels the flow * `handleEvent`: Called when the flow starts or progresses to a new page * `handleError`: Called when an error occurs These callbacks let you coordinate your app's UI with changes in the Persona UI. Do not rely on them for up-to-date data about the state of the inquiry. Use webhooks (Step 11) for logic that depends on inquiry state. **Storing and updating state:** The view model updates its state based on the Persona callbacks. This state is consumed by the views in our app: **`OnboardingViewModel.swift`** ```swift OnboardingViewModel.swift @Published var isLoading = false @Published var errorMessage: String? @Published var showPersonaFlow = false @Published var showSuccess = false @Published var inquiryId: String? @Published var sessionToken: String? @Published var completedInquiryId: String? @Published var completedInquiryStatus: String? ``` ## Step 6: Create the onboarding screen Now, create the onboarding screen that Alexander sees before he starts verification: ![pawsona onboarding page](https://assets.withpersona.com/f_auto,q_auto/developer-docs/images/inquiry-tutorials/ios-sdk-precreate/pawsona-ios-start.png) When you created the iOS app, a `ContentView` was generated. Replace the contents of `ContentView.swift` with the following code: **`ContentView.swift`** ```swift ContentView.swift import SwiftUI import Persona2 struct ContentView: View { @StateObject private var viewModel = OnboardingViewModel() var body: some View { if viewModel.showSuccess { FinishedView( inquiryId: viewModel.completedInquiryId ?? "", status: viewModel.completedInquiryStatus ?? "" ) } else { OnboardingView(viewModel: viewModel) } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } ``` Then create `OnboardingView.swift`: **`OnboardingView.swift`** ```swift OnboardingView.swift import SwiftUI struct OnboardingView: View { @ObservedObject var viewModel: OnboardingViewModel var body: some View { VStack(spacing: 0) { VStack(spacing: 20) { Text("Pawsona") .font(Font.title2) Spacer() Text("Welcome, Alexander! 🐕") .font(.title) .fontWeight(.bold) .foregroundColor(.primary) Text("Before you can start walking dogs, we need to verify your identity. This helps ensure the safety of pet owners and walkers like you.") .font(.body) .foregroundColor(.secondary) .multilineTextAlignment(.center) .fixedSize(horizontal: false, vertical: true) VStack(alignment: .leading, spacing: 12) { Text("Your Information") .font(.headline) .foregroundColor(.primary) InfoRow(label: "Name:", value: "Alexander Sample") InfoRow(label: "Birthdate:", value: "August 31, 1977") } .frame(maxWidth: .infinity, alignment: .leading) .padding() .background(Color(UIColor.systemBackground)) .cornerRadius(8) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(Color(UIColor.separator), lineWidth: 1) ) if let errorMessage = viewModel.errorMessage { Text(errorMessage) .font(.subheadline) .foregroundColor(.white) .padding() .frame(maxWidth: .infinity) .background(Color.red.opacity(0.8)) .cornerRadius(8) } Spacer() Button(action: { Task { await viewModel.startVerification() } }) { if viewModel.isLoading { ProgressView() .progressViewStyle(CircularProgressViewStyle(tint: .white)) .frame(maxWidth: .infinity) } else { Text("Start Verifying") .fontWeight(.semibold) .frame(maxWidth: .infinity) } } .frame(height: 50) .background(viewModel.isLoading ? Color.gray : Color.blue) .foregroundColor(.white) .cornerRadius(8) .disabled(viewModel.isLoading) } .padding(20) .frame(maxWidth: .infinity, maxHeight: .infinity) } .background( PersonaFlowPresenter( isPresented: $viewModel.showPersonaFlow, inquiryId: viewModel.inquiryId ?? "", sessionToken: viewModel.sessionToken, onComplete: { inquiryId, status in viewModel.handleCompletion(inquiryId: inquiryId, status: status) }, onCancel: { viewModel.handleCancellation() }, onEvent: { event in viewModel.handleEvent(event) }, onError: { error in viewModel.handleError(error) } ) ) } } struct InfoRow: View { let label: String let value: String var body: some View { HStack { Text(label) .fontWeight(.semibold) .foregroundColor(.primary) Text(value) .foregroundColor(.secondary) } } } ``` ### About OnboardingView Note the following features of OnboardingView: **"Start Verifying" button:** OnboardingView contains a "Start Verifying" button. When tapped, it triggers the view model to initiate the Persona verification flow: **`OnboardingView.swift`** ```swift OnboardingView.swift Button(action: { viewModel.startVerification() }) { if viewModel.isLoading { ProgressView() } else { Text("Start Verifying") } } ``` **Persona flow presentation:** After the view model gets an inquiry from the backend, it sets `showPersonaFlow` to `true`. The Persona flow is then presented: **`OnboardingView.swift`** ```swift OnboardingView.swift .background( PersonaFlowPresenter( isPresented: $viewModel.showPersonaFlow, ... ``` ![persona verification flow start](https://assets.withpersona.com/f_auto,q_auto/developer-docs/images/inquiry-tutorials/ios-sdk-precreate/persona-ios-modal.png) ## Step 7: Create the Persona flow wrapper The Persona SDK provides the `Inquiry` class, which has a `start` method that starts the verification flow as a modal: ```swift final public func start(from viewController: UIViewController, animated: Bool = true) ``` As you can see from the method above, the Inquiry needs to be presented from a UIKit UIViewController. In SwiftUI, we must wrap a UIViewController within a `UIViewControllerRepresentable`. You'll create this now. Create `PersonaFlowPresenter.swift` with the following code: **`PersonaFlowPresenter.swift`** ```swift PersonaFlowPresenter.swift // UIKit bridge for the Persona iOS SDK import SwiftUI import Persona2 struct PersonaFlowPresenter: UIViewControllerRepresentable { @Binding var isPresented: Bool let inquiryId: String let sessionToken: String? let onComplete: (String, String) -> Void let onCancel: () -> Void let onEvent: (InquiryEvent) -> Void let onError: (PersonaError) -> Void func makeCoordinator() -> Coordinator { Coordinator( onComplete: onComplete, onCancel: onCancel, onEvent: onEvent, onError: onError ) } func makeUIViewController(context: Context) -> UIViewController { let viewController = UIViewController() viewController.view.backgroundColor = .clear return viewController } func updateUIViewController(_ uiViewController: UIViewController, context: Context) { // Only present once - prevent re-presenting if SwiftUI re-renders guard isPresented && uiViewController.presentedViewController == nil else { return } var builder = Inquiry.from(inquiryId: inquiryId, delegate: context.coordinator) if let sessionToken { builder = builder.sessionToken(sessionToken) } let inquiry = builder.build() DispatchQueue.main.async { inquiry.start(from: uiViewController) } } // MARK: - Coordinator (InquiryDelegate) @MainActor class Coordinator: NSObject, InquiryDelegate { let onComplete: (String, String) -> Void let onCancel: () -> Void let onEvent: (InquiryEvent) -> Void let onError: (PersonaError) -> Void init(onComplete: @escaping (String, String) -> Void, onCancel: @escaping () -> Void, onEvent: @escaping (InquiryEvent) -> Void, onError: @escaping (PersonaError) -> Void) { self.onComplete = onComplete self.onCancel = onCancel self.onEvent = onEvent self.onError = onError } func inquiryComplete(inquiryId: String, status: String, fields: [String : InquiryField]) { onComplete(inquiryId, status) } func inquiryCanceled(inquiryId: String?, sessionToken: String?) { onCancel() } func inquiryEventOccurred(event: InquiryEvent) { onEvent(event) } func inquiryError(_ error: PersonaError) { onError(error) } } } ``` ### About the Persona flow wrapper This wrapper integrates the Persona iOS SDK with SwiftUI. Note the following features: **Uses a pre-created inquiry:** The SDK is initialized with `inquiryId` instead of a template ID. The session token is non-nil only when resuming pending inquiries: **`PersonaFlowPresenter.swift`** ```swift PersonaFlowPresenter.swift var builder = Inquiry.from(inquiryId: inquiryId, delegate: context.coordinator) if let sessionToken { builder = builder.sessionToken(sessionToken) } let inquiry = builder.build() DispatchQueue.main.async { inquiry.start(from: uiViewController) } ``` Note that the code here doesn't prefill user data or set a reference ID on the inquiry, because the backend already did that. **Implements an InquiryDelegate:** The Persona SDK requires a delegate object that conforms to `InquiryDelegate` to receive completion, cancellation, event, and error callbacks. In PersonaFlowPresenter, the internal `Coordinator` class is this delegate: **`PersonaFlowPresenter.swift`** ```swift PersonaFlowPresenter.swift class Coordinator: NSObject, InquiryDelegate { … func inquiryComplete(inquiryId: String, status: String, fields: [String : InquiryField]) { onComplete(inquiryId, status) } func inquiryCanceled(inquiryId: String?, sessionToken: String?) { onCancel() } func inquiryEventOccurred(event: InquiryEvent) { onEvent(event) } func inquiryError(_ error: PersonaError) { onError(error) } } ``` ## Step 8: Create the finish screen The final piece of iOS app code is the simple screen that's shown after Alexander finishes the Persona verification: ![pawsona finished page](https://assets.withpersona.com/f_auto,q_auto/developer-docs/images/inquiry-tutorials/ios-sdk-precreate/pawsona-ios-finished.png) Create a new Swift file called `FinishedView.swift`: **`FinishedView.swift`** ```swift FinishedView.swift import SwiftUI struct FinishedView: View { let inquiryId: String let status: String var body: some View { VStack(spacing: 20) { Text("Pawsona") .font(Font.title2) Spacer() Text("✔️") .font(.system(size: 60)) Text("Verification Submitted") .font(.title) .fontWeight(.bold) .foregroundColor(.primary) .multilineTextAlignment(.center) Text("Your verification is being processed.") .font(.body) .foregroundColor(.secondary) .multilineTextAlignment(.center) Spacer() // Debug info VStack(alignment: .leading, spacing: 8) { HStack { Text("Status:") .fontWeight(.semibold) Text(status) .foregroundColor(.secondary) } HStack(alignment: .top) { Text("Inquiry ID:") .fontWeight(.semibold) Text(inquiryId) .foregroundColor(.secondary) .lineLimit(nil) .fixedSize(horizontal: false, vertical: true) } } .frame(maxWidth: .infinity, alignment: .leading) .padding() .background(Color(UIColor.secondarySystemBackground)) .cornerRadius(8) Button(action: { // In production: navigate to home print("Navigate to home") }) { Text("Return to Home") .fontWeight(.semibold) .frame(maxWidth: .infinity) } .frame(height: 50) .background(Color.blue) .foregroundColor(.white) .cornerRadius(8) } .padding(20) .frame(maxWidth: .infinity, maxHeight: .infinity) } } struct FinishedView_Previews: PreviewProvider { static var previews: some View { FinishedView(inquiryId: "inq_12345", status: "completed") } } ``` ## Step 9: Add required iOS permissions The Persona SDK requires a few [permissions](/ios-sdk-v2-integration-guide#permissions) you must prompt the user for. 1. In your app settings, go to **Targets** > **Pawsona** > **Info**. 2. Edit "Custom iOS Target Properties" to include the following settings: * Camera permissions * Key: "Privacy - Camera Usage Description" * Value: "This app needs access to your camera to enable you to verify your identity." * Location permissions * Key: "Privacy - Location When In Use Usage Description" * Value: "This app may need access to your location to enable you to verify your identity." ## Step 10: Configure the iOS app for your computer There are a few settings needed in this demo app. These settings are NOT needed for a production app. 1. Allow your iOS app to talk to your local backend. In the "Custom iOS Target Properties" (which you edited in the previous step), add the following setting: * Key: "App Transport Security Settings" (Dictionary) * Add Child: "Allows Local Networking - Local Networking Exception Usage" 2. If you're testing on a physical device (e.g. a real iPhone), you need to update the backend URL: 1. Find your computer's IP address: * On macOS: **System Settings** > **Network** > **Wi-Fi** > **Details** > **TCP/IP** * Look for "IP Address" (e.g., `192.168.1.100`) 2. In `OnboardingViewModel.swift`, update the `backendURL`: ```swift // For iOS Simulator private let backendURL = "http://localhost:8000/api/inquiries/get-or-create" // For physical device (replace with your computer's IP) private let backendURL = "http://192.168.1.100:8000/api/inquiries/get-or-create" ``` Make sure your iPhone and computer are on the same Wi-Fi network. ## Step 11: Set up a webhook (optional) You can receive notifications when any inquiry's state changes. For example, you can be alerted when any inquiry is started by a user, or when any inquiry is completed. See the [full list of inquiry events](/model-lifecycle#events) you can be alerted about. To receive automatic notifications: 1. Create a webhook endpoint (for a sample server, see [Webhook quickstart](/quickstart-webhooks)) 2. In the dashboard, navigate to **Webhooks** > **Webhooks**. 3. Add your endpoint URL 4. Select the following "Enabled events": `inquiry.started`, `inquiry.completed`, `inquiry.approved`, `inquiry.declined`, and `inquiry.failed` For this tutorial, you can skip webhooks and view results in the dashboard. ## Step 12: Test the complete flow Build and run the app in Xcode. Then test the inquiry creation and resumption logic with different scenarios. When doing these manual tests: * Do not enter real personal information, since this is Sandbox. * Keep the "Pass verification" setting selected (set via the pink config menu on screen) to simulate passing all the checks. **Scenario 1: Create new inquiry** 1. Click "Start Verifying" 2. Wait for the Persona modal to open 3. Check backend logs for: `No incomplete inquiry found, creating new one` 4. Close the modal without entering any information **Scenario 2: Resume created inquiry** 1. Restart the app 2. Click "Start Verifying" 3. Check backend logs for: * `Found existing inquiry inq_XXXXXXXXXXXXX with status: created` * `Inquiry is created, resuming without session token` 4. Complete the government ID verification step 5. Stop before completing the selfie verification 6. Close the modal **Scenario 3: Resume pending inquiry** 1. Restart the app 2. Click "Start Verifying" 3. Check backend logs for: * `Found existing inquiry inq_XXXXXXXXXXXXX with status: pending` * `Inquiry is pending, generating session token to resume` 4. Check that the Persona modal starts at the selfie verification step (government ID already completed) 5. Complete the selfie verification **Scenario 4: Create new inquiry after completion** 1. Restart the app 2. Click "Start Verifying" 3. Check backend logs for: `No incomplete inquiry found, creating new one` 4. Check that the Persona modal shows a new inquiry 5. Do not complete this inquiry ## Step 13: (optional) Inspect webhook events If you set up the webhook in Step 11, check your server logs. You should see events from `inquiry.started`, `inquiry.completed`, and `inquiry.approved`. Note: If you want to receive the `inquiry.failed` event, open the verification flow again and select "Fail verification" in the debug menu (the pink config menu on screen). Then click through the verification flow. ## Step 14: View inquiry results In the Persona dashboard: 1. Navigate to **Inquiries** > **All Inquiries** 2. Find Alexander's inquiries (search by reference ID `usr_ABC123`) You should see two inquiries if you tested all four scenarios above. Click on each to see their status and details. Note that because this inquiry was created in Sandbox, some of the data shown will be demo data. You can also retrieve inquiry details via API. You'll need the inquiry ID you see printed in the "Debug info" section of the UI. See [Retrieve an Inquiry](/api-reference/inquiries/retrieve-an-inquiry). ## Summary You have built a backend API that: * Pre-creates inquiries securely on the backend * Prevents duplicate inquiries by checking for incomplete ones * Enables inquiries to be resumed with session tokens * Prefills user data securely on the backend You also: * Built an iOS app that receives inquiry IDs from your backend and presents the Persona flow * Tested a whole inquiry lifecycle (create → resume → complete) This is a complete example of how you can pre-create inquiries for a verification flow created with the Persona iOS SDK. ## Next steps Enhance this integration: * [Full iOS SDK guide](/ios-sdk-v2-integration-guide) * [Set up webhooks](/webhooks-best-practices) with best practices for production * [Handle inquiry expiration](/inquiry-expiration) for inquiries not completed in time Explore further: * [How the KYC solution works](https://help.withpersona.com/articles/2OOWdhAoEeVrMRKRFENneW/) * [Other integration methods](/choosing-an-integration-method) > Pre-create an Inquiry through the API before launching the Persona iOS SDK.