> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.withverifa.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.withverifa.com/_mcp/server.

# Tutorial: Creating Your First Verification Session

> Walk through creating a session via API, sending a user through the capture flow, choosing a sandbox outcome, and retrieving the verification result.

This tutorial walks you through the core Verifa integration — creating a
verification session, guiding a user through identity capture, and getting the
result back. By the end, you'll understand the full session lifecycle and have
a working end-to-end flow in sandbox.

## What you'll learn

* Creating a verification session via the API
* Redirecting a user to the capture flow
* How sandbox mode works (choosing verification outcomes)
* Retrieving the full verification result
* Connecting session results to your webhook handler

## Prerequisites

* A Verifa account ([sign up](https://app.withverifa.com))
* A **sandbox** API key (starts with `vk_sandbox_`). Find it under
  **Developers > API > API Keys** in the dashboard.

![API Keys page showing a sandbox key under Developers > API > API Keys](/_fern-img/9980d0c59fb1a97c2011fc0a8a69e19d7e1253c9edc961336ee4cc2cd9d98c88.webp)

## The session lifecycle

Before we start, here's the path a session takes:

```
pending → capturing → sandbox_pending → completed
```

| Status            | What's happening                                              |
| ----------------- | ------------------------------------------------------------- |
| `pending`         | Session created, waiting for the user to open the capture URL |
| `capturing`       | User is actively going through the capture flow               |
| `sandbox_pending` | Capture complete, waiting for you to choose a sandbox outcome |
| `completed`       | Verification finished — result available                      |

In live mode, `sandbox_pending` is replaced by `processing` (async
verification), but the rest is the same.

## Step 1: Create a session

Call the API to create a new verification session. The only required field is
your API key — everything else is optional.

#### cURL

```bash
curl -X POST https://api.withverifa.com/api/v1/sessions \
  -H "X-API-Key: vk_sandbox_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "external_ref": "tutorial_user_1"
  }'
```

#### Python

```python
import requests

response = requests.post(
    "https://api.withverifa.com/api/v1/sessions",
    headers={"X-API-Key": "vk_sandbox_your_key_here"},
    json={"external_ref": "tutorial_user_1"},
)
session = response.json()
print(f"Session ID: {session['id']}")
print(f"Capture URL: {session['capture_url']}")
```

#### Node.js

```javascript
const response = await fetch("https://api.withverifa.com/api/v1/sessions", {
  method: "POST",
  headers: {
    "X-API-Key": "vk_sandbox_your_key_here",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ external_ref: "tutorial_user_1" }),
});
const session = await response.json();
console.log(`Session ID: ${session.id}`);
console.log(`Capture URL: ${session.capture_url}`);
```

#### Go

```go
body, _ := json.Marshal(map[string]string{
    "external_ref": "tutorial_user_1",
})
req, _ := http.NewRequest("POST",
    "https://api.withverifa.com/api/v1/sessions",
    bytes.NewBuffer(body))
req.Header.Set("X-API-Key", "vk_sandbox_your_key_here")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()

var session map[string]interface{}
json.NewDecoder(resp.Body).Decode(&session)
fmt.Println("Capture URL:", session["capture_url"])
```

The response includes the `capture_url` — the link you'll send your user to:

```json
{
  "id": "session_abc123",
  "status": "pending",
  "external_ref": "tutorial_user_1",
  "capture_url": "https://app.withverifa.com/verify/tok_...",
  "qr_code_data_url": "data:image/png;base64,...",
  "is_sandbox": true,
  "created_at": "2026-04-01T12:00:00Z",
  "expires_at": "2026-04-01T12:20:00Z"
}
```

| Field              | Description                                                                 |
| ------------------ | --------------------------------------------------------------------------- |
| `id`               | Session ID — use this to retrieve results later                             |
| `capture_url`      | URL to redirect the user to for identity capture                            |
| `qr_code_data_url` | QR code image (base64 PNG) that encodes the capture URL — useful for mobile |
| `is_sandbox`       | `true` because we used a sandbox API key                                    |
| `expires_at`       | When the capture token expires (user must start before this)                |

> **Note**
>
> The `external_ref` field is optional but strongly recommended. It's your
> internal user ID — you'll use it to correlate webhook events and results back
> to users in your system.

### Optional: Create a session from the dashboard

You can also create sessions directly from the dashboard. Navigate to
**Verifications** in the sidebar and click **+ New Verification** in the top
right corner.

![Verifications page with arrows pointing to the sidebar and the + New Verification button](/_fern-img/c3cd2fcc756c57e286ebd19600f7c0f76458959b80a83c9a2f7eeb1d7e4e09af.webp)

The **New Verification** modal lets you fill in:

* **External Reference** — your internal user/case ID (you can click **Generate**
  to auto-fill one)
* **Applicant Name** — optional name for the verification
* **Applicant Email** — optional email (used if you enable email invites)
* **Email invite to applicant** — toggle to send the verification link directly
  to the applicant's email
* **Workflow** — select which workflow to use (defaults to your org's default
  workflow)

Click **Create Verification** when you're ready.

![New Verification modal showing External Reference, Applicant Name, Email, and Workflow fields](/_fern-img/92999768101e62784e31906ce88edf94925216b013641dea936b52509e4d86e7.webp)

After creation, a **Verification Created** modal appears with the capture URL
and a QR code. Click **Copy** to grab the capture URL, or scan the QR code on
a mobile device. You can also click **View Session** to go to the session
detail page, or **Create Another** to start a new one.

![Verification Created modal showing the capture URL, QR code, and session ID](/_fern-img/1901864ccc2639d0eb3c0d0de875e65fc96be6c1a70273af351f7027a71c3b7a.webp)

### What you can configure

The create session endpoint accepts several optional fields:

| Field            | Type    | Description                                                               |
| ---------------- | ------- | ------------------------------------------------------------------------- |
| `external_ref`   | string  | Your internal user ID for correlation                                     |
| `metadata`       | object  | Arbitrary key-value data stored with the session                          |
| `country`        | string  | ISO 3166-1 alpha-2 country code (e.g., `"US"`)                            |
| `workflow_id`    | string  | Bind the session to a specific workflow                                   |
| `redirect_url`   | string  | Where to redirect the user after capture completes                        |
| `send_email`     | boolean | Send a verification invite email (requires `applicant_email` in metadata) |
| `applicant_name` | string  | Name for the invite email                                                 |

For this tutorial, we only need `external_ref`. You can explore the other
options once you have the basic flow working.

## Step 2: Walk through the capture flow

In a real integration, you'd redirect the user to the `capture_url` or display
it in an iframe/popup via [verifa.js](/integration-guides/java-script-sdk). For this
tutorial, just open the capture URL in your browser.

The capture flow walks the user through several steps. Here's what each one
looks like:

### 2a. Consent

The first screen explains what data Verifa will collect and asks for consent.
The user checks the consent box and clicks **Continue**.

![Capture consent screen — "Identity Verification", consent checkbox, and Continue button](/_fern-img/87d215f1f806d54782f5bb35c5af39a5a0962955c80e6dcd58e34836c57ef496.webp)

### 2b. Document type

The user selects what type of government-issued ID they'll provide:

* **Driver's License** — front and back required
* **Passport** — photo page only
* **State ID / National ID** — front only

![Document type selection — Driver's License, Passport, or State ID / National ID](/_fern-img/e5041e65d2d9d16f3f63151d909503228e86f3ac1ab748051ce7f2c22f7d738a.webp)

### 2c. Document country and state

Next, the user selects where their ID was issued. For US documents, they also
select the issuing state.

![Document details — Country and State dropdowns with Continue button](/_fern-img/a26c3c817184f243fd90a6fd77ea5439414e4bf724484e08b69eb79f9f98b465.webp)

### 2d. Scan the front of the ID

The user is prompted to scan the front of their document. Tips are shown for
best results: use good lighting, avoid glare, and place the ID on a dark flat
surface. Click **I'm ready** to open the camera.

![Scan front of ID prompt with tips and I'm ready button](/_fern-img/e637aea2ec18951e45c6b52a930a49e4e2de6e22d2b77ed74075e8234e1e3e3f.webp)

The camera opens with a document frame. The user can switch between **Auto**
(captures automatically when the document is detected) and **Manual** mode.
There's also an upload button in the bottom-right corner to upload an existing
photo instead.

![Camera viewfinder for scanning the front of the ID](/_fern-img/b8f8fecbc2dd0ab12d904f913f559802c01006ffcd2d3e11532e47be870f0fca.webp)

### 2e. Review the front

After capture, the user reviews the photo. They can **Submit photo** to
continue or **Retake photo** to try again. They can also change the ID type
if they picked the wrong one.

![Review front of ID — Submit photo and Retake photo buttons](/_fern-img/40efcdb7a363607177cb5dd773066dad868776de3e022eb5e7d6a4f043386fba.webp)

> **Tip**
>
> In sandbox mode, you can upload any image — Verifa won't run real OCR. The
> "Photo quality is low" warning is normal for test images. Just click
> **Submit photo** to continue.

### 2f. Scan the back of the ID

For document types that require a back scan (like driver's licenses), the user
is prompted to flip their ID over. The tips remind them to keep the barcode
fully visible.

![Scan back of ID prompt — flip your ID over, make sure barcode is visible](/_fern-img/60fd919a4c63e85ebac7b000304ad234b5f7841b44fd927782c0e2e1e7d0ad7f.webp)

After scanning, the same review screen appears for the back:

![Review back of ID — Submit photo and Retake photo buttons](/_fern-img/6cb5e74545b52dc4f4d9ab3a494a8cf5a332520e4d4c71715e92c692f8a1515c.webp)

### 2g. Selfie capture

If the workflow includes facial recognition (most do), the user takes a live
selfie. This is compared against the photo on the ID document to verify
the person presenting the ID is the same person pictured on it.

### 2h. User information (if configured)

If the workflow's capture requirements include user information fields (name,
email, phone, etc.), the user fills those in before submitting. See the
[Creating a Workflow](/tutorials/creating-a-workflow) tutorial for how to
configure which fields are collected.

## Step 3: Choose a sandbox outcome

After completing the capture flow in sandbox mode, the session moves to
`sandbox_pending` status. The capture UI presents a **Choose Verification
Outcome** screen with preset options:

![Sandbox outcome selector — Approved, Rejected (Face Mismatch, Underage, Expired ID, Unreadable), and Needs Review](/_fern-img/530049b042b3350762722f51dd27ca72b573f19d5c654d3892601475fffbf92f.webp)

| Preset                       | Result         | What it simulates                                            |
| ---------------------------- | -------------- | ------------------------------------------------------------ |
| **Approved**                 | `approved`     | Everything passes — face match, age check, readable document |
| **Rejected — Face Mismatch** | `rejected`     | Selfie doesn't match the ID photo                            |
| **Rejected — Underage**      | `rejected`     | Person is under the minimum age requirement                  |
| **Rejected — Expired ID**    | `rejected`     | The ID document appears to be expired                        |
| **Rejected — Unreadable**    | `rejected`     | Document text is not legible enough to extract data          |
| **Needs Review**             | `needs_review` | Borderline face match — queued for manual review             |

Select **Approved** for this tutorial. The session completes immediately and
Verifa dispatches a `session.approved` webhook (if you have an endpoint
configured).

After selecting an outcome, you'll see a confirmation screen:

![Verification complete — "We're reviewing your details", sandbox verification completed](/_fern-img/0719e6d70bc1068334214d27b5cfd668e3b09daa96d963622da005c776b242e7.webp)

The user can now close this page. In a real integration, they'd be redirected
to your `redirect_url` if you set one.

All sandbox presets return mock PII data:

```json
{
  "first_name": "Jane",
  "last_name": "Doe",
  "date_of_birth": "1997-06-15",
  "address_line1": "123 Test Street",
  "city": "San Francisco",
  "state": "CA",
  "zip_code": "94102",
  "document_number": "D1234567"
}
```

## Step 4: Check the session status

After choosing an outcome, poll the session to confirm it completed:

#### cURL

```bash
curl https://api.withverifa.com/api/v1/sessions/session_abc123 \
  -H "X-API-Key: vk_sandbox_your_key_here"
```

#### Python

```python
session = requests.get(
    "https://api.withverifa.com/api/v1/sessions/session_abc123",
    headers={"X-API-Key": "vk_sandbox_your_key_here"},
).json()
print(f"Status: {session['status']}")
```

#### Node.js

```javascript
const session = await fetch(
  "https://api.withverifa.com/api/v1/sessions/session_abc123",
  { headers: { "X-API-Key": "vk_sandbox_your_key_here" } }
).then((r) => r.json());
console.log(`Status: ${session.status}`);
```

#### Go

```go
req, _ := http.NewRequest("GET",
    "https://api.withverifa.com/api/v1/sessions/session_abc123", nil)
req.Header.Set("X-API-Key", "vk_sandbox_your_key_here")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()

var session map[string]interface{}
json.NewDecoder(resp.Body).Decode(&session)
fmt.Println("Status:", session["status"])
```

```json
{
  "id": "session_abc123",
  "status": "completed",
  "external_ref": "tutorial_user_1",
  "is_sandbox": true,
  "metadata": {},
  "country": null,
  "document_type": "drivers_license",
  "retention_status": "active",
  "tags": [],
  "created_at": "2026-04-01T12:00:00Z",
  "updated_at": "2026-04-01T12:02:00Z"
}
```

The `status` is now `completed`. Note that the session response itself doesn't
include the verification results — for that, you need the result endpoint.

## Step 5: Retrieve the verification result

Fetch the full result with extracted data, check outcomes, and scores:

#### cURL

```bash
curl https://api.withverifa.com/api/v1/sessions/session_abc123/result \
  -H "X-API-Key: vk_sandbox_your_key_here"
```

#### Python

```python
result = requests.get(
    "https://api.withverifa.com/api/v1/sessions/session_abc123/result",
    headers={"X-API-Key": "vk_sandbox_your_key_here"},
).json()

print(f"Status: {result['status']}")
print(f"Face match: {result['face_match_passed']}")
print(f"Age check: {result['age_check_passed']}")
print(f"Name: {result['extracted_data']['first_name']} {result['extracted_data']['last_name']}")
```

#### Node.js

```javascript
const result = await fetch(
  "https://api.withverifa.com/api/v1/sessions/session_abc123/result",
  { headers: { "X-API-Key": "vk_sandbox_your_key_here" } }
).then((r) => r.json());

console.log(`Status: ${result.status}`);
console.log(`Face match: ${result.face_match_passed}`);
console.log(`Age check: ${result.age_check_passed}`);
console.log(`Name: ${result.extracted_data.first_name} ${result.extracted_data.last_name}`);
```

#### Go

```go
req, _ := http.NewRequest("GET",
    "https://api.withverifa.com/api/v1/sessions/session_abc123/result", nil)
req.Header.Set("X-API-Key", "vk_sandbox_your_key_here")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()

var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
fmt.Println("Status:", result["status"])
fmt.Println("Face match:", result["face_match_passed"])
```

```json
{
  "session_id": "session_abc123",
  "status": "approved",
  "is_sandbox": true,
  "face_match_passed": true,
  "face_match_score": 0.95,
  "age_check_passed": true,
  "extracted_age": 28,
  "extracted_data": {
    "first_name": "Jane",
    "last_name": "Doe",
    "date_of_birth": "1997-06-15",
    "address_line1": "123 Test Street",
    "city": "San Francisco",
    "state": "CA",
    "zip_code": "94102",
    "document_number": "D1234567",
    "document_type": "drivers_license",
    "document_issuing_state": "CA",
    "document_issuing_country": "US"
  },
  "check_results": {
    "face_match": {
      "status": "passed",
      "score": 0.95
    },
    "age_check": {
      "status": "passed",
      "age": 28
    }
  },
  "rejection_reason": null,
  "processing_mode": "simulated",
  "country_used": "US",
  "created_at": "2026-04-01T12:02:00Z"
}
```

### Understanding the result

| Field               | Description                                                             |
| ------------------- | ----------------------------------------------------------------------- |
| `status`            | `"approved"`, `"rejected"`, or `"needs_review"`                         |
| `face_match_passed` | Whether the selfie matched the document photo                           |
| `face_match_score`  | Confidence score (0.0 to 1.0)                                           |
| `age_check_passed`  | Whether the applicant meets the minimum age                             |
| `extracted_age`     | Age calculated from the document's date of birth                        |
| `extracted_data`    | PII extracted from the document (decrypted)                             |
| `check_results`     | Per-check breakdown with status and details                             |
| `rejection_reason`  | Human-readable reason (only present for rejections)                     |
| `processing_mode`   | `"simulated"` (sandbox), `"automatic"` (live), or `"manual"` (reviewed) |

> **Note**
>
> In live mode, which PII fields are returned via the API depends on your
> organization's `api_pii_fields` allowlist. SSN, for example, is only included
> if explicitly enabled.

## Step 6: View the session in the dashboard

You can also view completed sessions in the dashboard. Navigate to
**Verifications** in the sidebar to see all your sessions with their status,
result, risk level, and other details.

![Verifications list showing the completed session with approved result](/_fern-img/00fffa29d81ff65dec6883a614eec06c5f65d16ba44797b351833495ef3e98f6.webp)

Click on a session to open the detail page. Here you can see:

* **Session Details** — session ID, status, environment, country, IP address,
  timestamps, and retention status
* **Result** — the overall result (approved/rejected) and face match score
* **Checks** — individual check outcomes (Document Scan, Facial Recognition,
  Age Verification) organized by category
* **Documents** — uploaded ID images
* **Risk** — risk assessment details
* **Workflow** — which workflow version processed the session
* **Webhooks** — delivery history for this session's events

![Session detail page showing checks — Document Scan, Facial Recognition, and Age Verification all passed](/_fern-img/524bddccdb3b4e14246cb08ebbe972d25ba0eb773e2c5f0d2944cdb21b42f1a1.webp)

## Step 7: Connect it to webhooks

Polling the API works, but webhooks are the recommended way to get notified
when a session completes. If you followed the
[Receiving Webhooks](/tutorials/receiving-webhooks-validating-signatures) tutorial, your
endpoint will receive a `session.approved` event like this:

```json
{
  "event": "session.approved",
  "session_id": "session_abc123",
  "external_ref": "tutorial_user_1",
  "status": "approved",
  "is_sandbox": true,
  "identity_id": "identity_def789",
  "result": {
    "face_match_passed": true,
    "age_check_passed": true,
    "extracted_data": {
      "first_name": "Jane",
      "last_name": "Doe",
      "date_of_birth": "1997-06-15"
    }
  }
}
```

See the [Handling Webhook Events](/tutorials/handling-webhook-events)
tutorial for how to build handlers for each event type.

## Putting it all together

Here's the complete integration in a single backend endpoint:

#### Python (FastAPI)

```python
import os
import requests
from fastapi import FastAPI, Request
from fastapi.responses import RedirectResponse

app = FastAPI()
API_KEY = os.environ["VERIFA_API_KEY"]
API_BASE = "https://api.withverifa.com/api/v1"

@app.post("/verify")
async def start_verification(request: Request):
    """Create a session and redirect to the capture flow."""
    body = await request.json()

    session = requests.post(
        f"{API_BASE}/sessions",
        headers={"X-API-Key": API_KEY},
        json={
            "external_ref": body["user_id"],
            "redirect_url": "https://your-app.com/verification-complete",
        },
    ).json()

    # Store session.id linked to your user
    # await db.users.update(body["user_id"], verifa_session_id=session["id"])

    return {"capture_url": session["capture_url"], "session_id": session["id"]}

@app.get("/verification-complete")
async def verification_complete():
    """User lands here after capture. Show a waiting page."""
    return {"message": "Verification submitted. We'll notify you when it's complete."}

@app.post("/webhooks/verifa")
async def handle_webhook(request: Request):
    """Receive verification results via webhook."""
    # (Add signature verification — see the webhook tutorial)
    event = await request.json()

    if event["event"] == "session.approved":
        # Activate the user
        user_id = event["external_ref"]
        # await db.users.update(user_id, verified=True)
        print(f"User {user_id} verified!")

    return {"status": "ok"}
```

#### Node.js (Express)

```javascript
const express = require("express");
const app = express();
app.use(express.json());

const API_KEY = process.env.VERIFA_API_KEY;
const API_BASE = "https://api.withverifa.com/api/v1";

app.post("/verify", async (req, res) => {
  // Create a session and return the capture URL
  const response = await fetch(`${API_BASE}/sessions`, {
    method: "POST",
    headers: {
      "X-API-Key": API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      external_ref: req.body.user_id,
      redirect_url: "https://your-app.com/verification-complete",
    }),
  });
  const session = await response.json();

  // Store session.id linked to your user
  // await db.users.update(req.body.user_id, { verifaSessionId: session.id });

  res.json({ capture_url: session.capture_url, session_id: session.id });
});

app.get("/verification-complete", (req, res) => {
  res.json({ message: "Verification submitted. We'll notify you when it's complete." });
});

app.post("/webhooks/verifa", express.raw({ type: "application/json" }), (req, res) => {
  // (Add signature verification — see the webhook tutorial)
  const event = JSON.parse(req.body);

  if (event.event === "session.approved") {
    const userId = event.external_ref;
    // await db.users.update(userId, { verified: true });
    console.log(`User ${userId} verified!`);
  }

  res.status(200).send("OK");
});

app.listen(3000);
```

#### Go

```go
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"
    "os"
)

var apiKey = os.Getenv("VERIFA_API_KEY")
const apiBase = "https://api.withverifa.com/api/v1"

func startVerification(w http.ResponseWriter, r *http.Request) {
    var body struct{ UserID string `json:"user_id"` }
    json.NewDecoder(r.Body).Decode(&body)

    payload, _ := json.Marshal(map[string]string{
        "external_ref": body.UserID,
        "redirect_url": "https://your-app.com/verification-complete",
    })
    req, _ := http.NewRequest("POST", apiBase+"/sessions", bytes.NewBuffer(payload))
    req.Header.Set("X-API-Key", apiKey)
    req.Header.Set("Content-Type", "application/json")

    resp, _ := http.DefaultClient.Do(req)
    defer resp.Body.Close()
    respBody, _ := io.ReadAll(resp.Body)

    w.Header().Set("Content-Type", "application/json")
    w.Write(respBody)
}

func handleWebhook(w http.ResponseWriter, r *http.Request) {
    // (Add signature verification — see the webhook tutorial)
    body, _ := io.ReadAll(r.Body)
    var event map[string]interface{}
    json.Unmarshal(body, &event)

    if event["event"] == "session.approved" {
        userID, _ := event["external_ref"].(string)
        fmt.Printf("User %s verified!\n", userID)
    }

    w.WriteHeader(http.StatusOK)
}

func main() {
    http.HandleFunc("/verify", startVerification)
    http.HandleFunc("/webhooks/verifa", handleWebhook)
    log.Fatal(http.ListenAndServe(":3000", nil))
}
```

## Try different sandbox presets

Now that you have the flow working, try the other sandbox presets to see how
your integration handles rejections and edge cases:

| Try this                                       | What happens                                                    |
| ---------------------------------------------- | --------------------------------------------------------------- |
| Select **Rejected — Face Mismatch**            | `session.declined` webhook fires with `rejection_reason`        |
| Select **Needs Review**                        | `session.requires-review` webhook fires, session shows in Cases |
| Let the session expire (wait for `expires_at`) | `session.expired` webhook fires                                 |

This lets you test all the code paths in your webhook handler before going live.

## Next steps

* [Receiving Webhooks & Validating Signatures](/tutorials/receiving-webhooks-validating-signatures) — Set up webhook signature verification
* [Handling Webhook Events](/tutorials/handling-webhook-events) — Build handlers for each event type
* [Creating a Workflow](/tutorials/creating-a-workflow) — Customize the verification pipeline
* [JavaScript SDK](/integration-guides/java-script-sdk) — Embed capture in your frontend with verifa.js
* [Testing](/best-practices/testing) — Sandbox testing strategies
* [Sessions](/core-concepts/sessions) — Full session lifecycle reference