Quickstart: Pre-create inquiries for Embedded Flow

An Embedded Flow embeds Persona’s verification UI directly into your website as an iframe.

There are two ways to use Embedded Flow:

  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 inquiries with prefilled user data in your backend server
  • Check for existing inquiries to avoid duplicates
  • Enable the user to resume partially-completed inquiries
  • Pass an inquiry ID to your frontend
  • Display the Embedded Flow with an 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 Embedded Flow, see Quickstart: Embedded Flow with Inquiry Template.

Prerequisites

You’ll need:

  • A Persona account
  • A Persona API key - use the Sandbox API key
  • Python installed locally
    • This guide provides sample code in Python, but you can adapt it to any language.

Before you start, you should:

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, 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 that include preconfigured inquiry templates. In this tutorial, use the “KYC” solution to verify your dog walkers.

Follow these instructions 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 web page

Create the web page that will display the Embedded Flow. This page shows the user onboarding flow that Alexander Sample is completing in your dog walking app.

  1. Create a new directory called embedded-flow-precreate-demo/. Then create a subdirectory called frontend/.

  2. Inside frontend/, create a file called onboarding.html with the following code:

embedded-flow-precreate-demo/frontend/onboarding.html
1<!DOCTYPE html>
2<html>
3<head>
4 <title>Complete Your Profile - Dog Walker</title>
5 <style>
6 body {
7 font-family: Arial, sans-serif;
8 max-width: 600px;
9 margin: 50px auto;
10 padding: 20px;
11 text-align: center;
12 }
13 .container {
14 border: 1px solid #ddd;
15 border-radius: 8px;
16 padding: 40px;
17 background-color: #f9f9f9;
18 }
19 h1 {
20 color: #333;
21 margin-bottom: 10px;
22 }
23 p {
24 color: #666;
25 line-height: 1.6;
26 margin-bottom: 30px;
27 }
28 button {
29 background-color: #0066cc;
30 color: white;
31 border: none;
32 padding: 15px 40px;
33 font-size: 16px;
34 border-radius: 5px;
35 cursor: pointer;
36 }
37 button:hover {
38 background-color: #0052a3;
39 }
40 button:disabled {
41 background-color: #ccc;
42 cursor: not-allowed;
43 }
44 .user-info {
45 background-color: #fff;
46 border: 1px solid #ddd;
47 border-radius: 4px;
48 padding: 15px;
49 margin-bottom: 30px;
50 text-align: left;
51 }
52 .user-info h3 {
53 margin-top: 0;
54 color: #333;
55 }
56 .user-info p {
57 margin: 8px 0;
58 }
59 </style>
60 <script src="https://cdn.withpersona.com/dist/persona-vX.Y.Z.js" crossorigin="anonymous"></script>
61 <meta charset="UTF-8" />
62 <meta name="viewport" content="width=device-width, initial-scale=1" />
63</head>
64<body>
65 <div class="container">
66 <h1>Welcome, Alexander 🐕</h1>
67 <p>
68 Before you can start walking dogs, we need to verify your identity.
69 This helps ensure the safety of pet owners and walkers like you.
70 </p>
71
72 <div class="user-info">
73 <h3>Your Information</h3>
74 <p><strong>Name:</strong> Alexander Sample</p>
75 <p><strong>Birthdate:</strong> August 31, 1977</p>
76 </div>
77
78 <button id="verify-button">Start Verifying</button>
79 </div>
80 <div>
81 <p>Debug info:</p>
82 <p id="debug-info"></p>
83 </div>
84
85 <script>
86 document.getElementById('verify-button').addEventListener('click', async () => {
87 try {
88 // Call your backend to get or create an inquiry
89 const response = await fetch('http://localhost:8000/api/inquiries/get-or-create', {
90 method: 'POST',
91 headers: {
92 'Content-Type': 'application/json',
93 }
94 });
95
96 if (!response.ok) {
97 throw new Error('Failed to create inquiry');
98 }
99
100 const data = await response.json();
101 const inquiryId = data.inquiry_id;
102 const sessionToken = data.session_token;
103
104 // Build client config
105 const clientConfig = {
106 inquiryId: inquiryId,
107 environmentId: `env_XXXXXXXXXXXXX`,
108 onReady: () => client.open(),
109 onComplete: ({ inquiryId, status }) => {
110 // Inquiry completed. For demonstration purposes, we will show a debug message in the UI.
111 document.getElementById('debug-info').innerText = `Completed inquiry with ID: ${inquiryId} \n\nWrite down this ID for Step 8.`;
112
113 // Here, you could also send a request to your backend to log the completion.
114
115 // Clean up the client to avoid memory leaks.
116 client.destroy();
117 },
118 onCancel: () => {
119 console.log('User cancelled verification');
120 },
121 onError: (error) => {
122 console.error('Verification error:', error);
123 }
124 };
125
126 // Add session token if resuming a pending inquiry
127 if (sessionToken) {
128 clientConfig.sessionToken = sessionToken;
129 }
130
131 // Open Persona flow
132 const client = new Persona.Client(clientConfig);
133
134 } catch (error) {
135 console.error('Error:', error);
136 }
137 });
138 </script>
139</body>
140</html>
  1. In the code, replace:
  1. Serve the frontend HTML file from a local python server:
$cd embedded-flow-precreate-demo/frontend/
>python -m http.server 8001

Keep this terminal window open.

  1. Open http://localhost:8001/onboarding.html in your browser to view the page.

You should see the onboarding flow for our newly-registered dog walker:

dog walker onboarding page

Right now, the “Start Verifying” button doesn’t work. Notably, we haven’t yet built the backend API that it hits.

About the frontend code

The UI and styling here are identical to the code in the Embedded Flow with Inquiry Template quickstart.

The key difference is in the JavaScript:

  1. We fetch the inquiry ID from our backend:
1const response = await fetch('http://localhost:8000/api/inquiries/get-or-create', {
2 method: 'POST',
3 headers: {
4 'Content-Type': 'application/json',
5 }
6});
7const data = await response.json();
8const inquiryId = data.inquiry_id;
9const sessionToken = data.session_token;

Our backend determines if an inquiry exists or needs to be created. If an inquiry exists and it needs to be resumed, the backend also returns a session token that lets the client resume that inquiry.

  1. We initialize the Persona client with inquiryId instead of templateId:
1const clientConfig = {
2 inquiryId: inquiryId, // Pre-created inquiry from backend
3 environmentId: environmentId,
4 // No templateId needed - we use inquiry ID instead
5 // No fields needed - already set on the inquiry by the backend
6 // No referenceId needed - already set on the inquiry by the backend
7 ...
8};
9const client = new Persona.Client(clientConfig);

This approach is more secure because users can’t change the reference ID or prefilled fields.

Step 4: Create the backend API

Now, 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.

  1. Create the embedded-flow-precreate-demo/backend/ subdirectory.

  2. Create the file backend/server.py with the following code:

embedded-flow-precreate-demo/backend/server.py
1from flask import Flask, request, jsonify
2from flask_cors import CORS
3import requests
4import os
5
6app = Flask(__name__)
7CORS(app)
8
9PERSONA_API_KEY = os.environ.get('PERSONA_API_KEY') # Configure your Sandbox API key
10PERSONA_API_URL = 'https://api.withpersona.com/api/v1'
11PERSONA_VERSION = '2025-10-27'
12INQUIRY_TEMPLATE_ID = 'itmpl_XXXXXXXXXXXXX' # Replace with your template ID
13
14def _get_persona_req_headers():
15 return {
16 'Authorization': f'Bearer {PERSONA_API_KEY}',
17 'Content-Type': 'application/json',
18 'Persona-Version': PERSONA_VERSION
19 }
20
21@app.route('/api/inquiries/get-or-create', methods=['POST'])
22def get_or_create_inquiry():
23 # In production, get the user ID from your session/auth system
24 user_id = 'usr_ABC123'
25
26 # In production, fetch user data from your database
27 user_data = {
28 'name_first': 'Alexander',
29 'name_last': 'Sample',
30 'birthdate': '1977-08-31'
31 }
32
33 # Check for existing incomplete inquiry
34 existing = find_incomplete_inquiry(user_id)
35
36 if existing:
37 inquiry_id = existing['id']
38 status = existing['attributes']['status']
39
40 print(f"Found existing inquiry {inquiry_id} with status: {status}")
41
42 # If inquiry is pending (has submitted verifications), generate session token
43 if status == 'pending':
44 print("Inquiry is pending, generating session token to resume")
45 session_token = create_session_token(inquiry_id)
46 return jsonify({
47 'inquiry_id': inquiry_id,
48 'session_token': session_token
49 })
50
51 # If inquiry is created (not yet started), no session token needed
52 else: # status == 'created'
53 print("Inquiry is created, continuing without session token")
54 return jsonify({
55 'inquiry_id': inquiry_id,
56 'session_token': None
57 })
58 else:
59 # Create new inquiry
60 print("No incomplete inquiry found, creating new one")
61 inquiry_id = create_inquiry(user_id, user_data)
62 return jsonify({
63 'inquiry_id': inquiry_id,
64 'session_token': None
65 })
66
67def find_incomplete_inquiry(user_id):
68 """Find existing inquiry with status 'created' or 'pending'"""
69 try:
70 response = requests.get(
71 f"{PERSONA_API_URL}/inquiries",
72 params={
73 'filter[reference-id]': user_id,
74 'filter[status]': 'created,pending',
75 'page[size]': 1
76 },
77 headers=_get_persona_req_headers()
78 )
79 response.raise_for_status()
80 inquiries = response.json().get('data', [])
81 return inquiries[0] if inquiries else None
82 except Exception as e:
83 print(f"Error finding inquiry: {e}")
84 return None
85
86def create_inquiry(user_id, user_data):
87 """Create new inquiry via Persona API"""
88 payload = {
89 'data': {
90 'attributes': {
91 'inquiry-template-id': INQUIRY_TEMPLATE_ID,
92 'reference-id': user_id,
93 'fields': user_data
94 }
95 }
96 }
97
98 response = requests.post(
99 f"{PERSONA_API_URL}/inquiries",
100 json=payload,
101 headers=_get_persona_req_headers()
102 )
103 response.raise_for_status()
104 return response.json()['data']['id']
105
106def create_session_token(inquiry_id):
107 """Generate session token for resuming pending inquiry"""
108 try:
109 response = requests.post(
110 f"{PERSONA_API_URL}/inquiries/{inquiry_id}/resume",
111 json={'meta': {}},
112 headers=_get_persona_req_headers()
113 )
114 response.raise_for_status()
115
116 # Session token is in the meta object
117 meta = response.json().get('meta', {})
118 session_token = meta.get('session-token')
119
120 return session_token
121 except Exception as e:
122 print(f"Error creating session token: {e}")
123 return None
124
125if __name__ == '__main__':
126 app.run(port=8000, debug=True)

In this code, replace:

  • itmpl_XXXXXXXXXXXXX with your inquiry template ID from Step 2
  1. Create the backend/requirements.txt file:
embedded-flow-precreate-demo/backend/requirements.txt
flask
flask-cors
requests
  1. Install required dependencies:
$# Create a virtual environment
>cd embedded-flow-precreate-demo/backend/
>python -m venv venv
>
># Activate the virtual environment
>source venv/bin/activate
>
># Install dependencies
>pip install -r requirements.txt
  1. Set your Persona Sandbox API key as an environment variable:
$export PERSONA_API_KEY="your_sandbox_api_key_here"
  1. Start the server:
$cd embedded-flow-precreate-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

This backend implements three key features:

  1. Find existing incomplete inquiries: The backend searches for inquiries that the current user has created but not finished. (Note that you must consistently provide a reference ID for each inquiry you create, if you want to be able to use the reference ID to look up all inquiries created for a given user.)
1def find_incomplete_inquiry(user_id):
2 """Find existing inquiry with status 'created' or 'pending'"""
3 try:
4 response = requests.get(
5 f"{PERSONA_API_URL}/inquiries",
6 params={
7 'filter[reference-id]': user_id,
8 'filter[status]': 'created,pending',
9 'page[size]': 1
10 },
11 headers=_get_persona_req_headers()
12 )
  1. Resume incomplete inquiries: If the user has a “pending” inquiry (an inquiry that they have submitted information to but not completed), we enable them to resume the inquiry by creating a new session token.
1if status == 'pending':
2 # User has submitted verifications - needs session token
3 session_token = create_session_token(inquiry_id)
4else: # status == 'created'
5 # User hasn't started - no session token needed
6 session_token = None
Handling other inquiry statuses

In a production app, you may want to handle additional inquiry statuses beyond “created” and “pending”. For example, if a user has any finished inquiry (“completed” or “approved”), you may not want that user to create a new inquiry.

See this guide for a list of other statuses you might want to handle.

3. Securely prefill data in new inquiries: All new inquiries are created on the backend, so user data is set where users can’t modify it.

1def create_inquiry(user_id, user_data):
2 """Create new inquiry via Persona API"""
3 payload = {
4 'data': {
5 'attributes': {
6 'inquiry-template-id': INQUIRY_TEMPLATE_ID,
7 'reference-id': user_id,
8 'fields': user_data
9 }
10 }
11 }

Step 5: 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 you can be alerted about.

To receive automatic notifications:

  1. Create a webhook endpoint (for a sample server, see Webhook quickstart)
  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 6: Test the complete flow

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 verifications” toggle enabled (visible at the bottom of the flow) 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. Refresh the page
  2. Click “Start Verifying”
  3. Check backend logs for:
    • Found existing inquiry inq_XXXXXXXXXXXXX with status: created
    • Inquiry is created, continuing 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. Refresh the page
  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. Refresh the page
  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 7: (optional) Inspect webhook events

If you set up the webhook in Step 5, 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, you can reload the page and click “Start Verifying” again. Then click through the verification flow, this time with the “Pass verifications” toggle disabled.

Step 8: 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.

Step 9: Disable client-side inquiry creation

Now that you’re creating inquiries server side, you can block client-side inquiry creation completely. This ensures users can’t create inquiries using the template ID approach.

To disable client-side inquiry creation for your “KYC” inquiry template:

  1. In the Persona dashboard, navigate to Inquiries > Templates.
  2. Select the “KYC” template.
  3. Click the gear icon to open Settings.
  4. Navigate to Security.
  5. Enable Block client-side Inquiry creation.

Summary

In this tutorial, you built an Embedded Flow integration 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 server side

You also:

  • Built a frontend that receives inquiry IDs from your backend
  • Tested a whole inquiry lifecycle (create → resume → complete)

This is a complete example of how you can pre-create inquiries for Embedded Flow.

Next steps

Enhance this integration:

  • Subscribe to additional events: Understand the different inquiry events you can be alerted about, and the difference between the “Done” and “Post-inquiry” phases.
  • Learn webhook best practices: In production, you’ll need to handle duplicate events and other issues.

Explore further:

  • Explore the KYC solution: The KYC solution includes two Workflows and a Case template. In this tutorial, the Workflows seamlessly ran in the background and changed the final status of your inquiry from completed to approved.
  • Explore other integration methods: Try Hosted Flow if you would like to distribute verification links, or Mobile SDK for native apps. See Choosing an integration method.