> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kayle.id/llms.txt
> Use this file to discover all available pages before exploring further.

# Verification sessions

> Lifecycle, attempts, expiry, and cancellation.

A verification session represents one Kayle check you want to run. You create a session via the API, redirect the user to the session's `verification_url`, and receive the Kayle result over a webhook when the user finishes, or a `verification.session.expired` event if they don't.

## The session object

The shape returned by the API:

```json theme={null}
{
  "id": "vs_mza7vec...",
  "status": "created",
  "contract_version": 1,
  "share_fields": {
    "date_of_birth":     { "required": true,  "reason": "Verify age",        "source": "rc" },
    "kayle_document_id": { "required": true,  "reason": "Sharing \"Kayle Document ID\"", "source": "default" }
  },
  "redirect_url": "https://yourapp.com/verification/done",
  "verification_url": "https://verify.kayle.id/vs_...?cancel_token=...",
  "cancel_token": "...",
  "expires_at": "2026-05-05T12:00:00Z",
  "completed_at": null,
  "created_at": "2026-05-05T11:00:00Z",
  "updated_at": "2026-05-05T11:00:00Z"
}
```

`cancel_token` is returned **only** at creation. The verify URL embeds it as a query parameter so the verify browser and native apps can pass it back when cancelling. If you want to surface a cancel link from somewhere else (a dashboard, an email), store it server-side at creation time.

## Lifecycle

A session moves through these statuses:

| Status        | Meaning                                                                                                    |
| ------------- | ---------------------------------------------------------------------------------------------------------- |
| `created`     | The session exists. The mobile client has not authenticated yet.                                           |
| `in_progress` | A mobile client is actively performing the Kayle check.                                                    |
| `succeeded`   | All Kayle checks confirmed and the user consented to share at least all required fields.                   |
| `failed`      | A check exhausted its retry budget, or a hard-kill code (anti-cloning attestation) terminated the session. |
| `expired`     | 60 minutes elapsed since creation without a terminal outcome.                                              |
| `cancelled`   | The session was cancelled by you or by the user.                                                           |

Sessions expire 60 minutes after `created_at`. A scheduled job sweeps expired sessions and emits `verification.session.expired` for any that didn't reach a terminal status first.

## Retries

A session has per-check retry budgets — not a single whole-session budget. When a user fails NFC, they don't have to re-scan MRZ; when they fail liveness, they don't have to re-tap the chip.

| Check            | Budget                                                                      |
| ---------------- | --------------------------------------------------------------------------- |
| MRZ scan         | Unlimited (purely client-side OCR; the server never sees a failed MRZ scan) |
| NFC chip read    | 3 retries                                                                   |
| Liveness capture | 3 retries                                                                   |

Retries happen entirely inside the mobile app — the user does not re-scan the handoff QR code, and your server is not notified per retry. Only the terminal outcome fires a webhook. The `verification.session.failed` payload's `data.nfc_tries_used` and `data.liveness_tries_used` tell you which check exhausted.

`document_anti_cloning_attestation_failed` is a hard-kill: the session terminates immediately, regardless of retry counters.

The session response exposes the live counters on `nfc_tries_used` and `liveness_tries_used` so you can render an "attempts remaining" UI without subscribing to webhooks.

## Creating a session

```bash theme={null}
curl -X POST https://api.kayle.id/v1/sessions \
  -H "Authorization: Bearer kk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "redirect_url": "https://yourapp.com/verification/done",
    "share_fields": {
      "date_of_birth": { "required": true, "reason": "Check age eligibility" },
      "family_name":   { "required": true, "reason": "Match account name" }
    }
  }'
```

`share_fields` is optional. If you omit it, the session uses Kayle's default share contract (a minimal set including `kayle_document_id`). If you include it, every entry needs `required` and a non-empty `reason` (200 characters max). See [Share fields](/verifications/share-fields) for the catalogue.

`redirect_url` is also optional, but it must begin with `https://`.

Both fields are reflected back to the user during the consent step, so write `reason` strings the user will see and trust.

## Cancelling

Two cancel paths exist:

* **Authenticated cancel** — `POST /v1/sessions/:id/cancel` with your API key. Idempotent. Use this when you decide server-side that you no longer want the Kayle check to proceed.
* **Public cancel** — `POST /v1/verify/session/:id/cancel` with the one-shot `cancel_token` in the body. The verify app and native apps use this to abort from the user's side. The token is consumed on first successful use; subsequent calls return `204` if the session is already terminal, otherwise `CANCEL_TOKEN_USED`.

A cancelled session emits `verification.session.cancelled` to subscribed webhooks. The payload carries a `reason` (`cancelled`, `cancelled_after_failed_check`, or one of the `privacy_cancelled_after_terminal_*` variants when the event replaces a scrubbed `succeeded` / `failed` payload) plus the per-check retry counters. See the [reference](/api-reference/webhooks/verification-session-cancelled) for the full field list.

<Note>
  The `verification_url` in the create response already contains `cancel_token` as a query parameter, so you don't need to surface it yourself unless you're building a separate cancel UI.
</Note>

## Listing sessions

`GET /v1/sessions` supports filtering by `status`, `created_from`, and `created_to`, and follows the standard cursor pagination (`limit`, `starting_after`, `next_cursor`). Pass `include_attempts=true` to attach the attempt array to each session — handy for reporting, but increases payload size.

```bash theme={null}
curl "https://api.kayle.id/v1/sessions?status=completed&limit=50" \
  -H "Authorization: Bearer kk_..."
```

## What you don't see

The session object never includes raw document data, MRZ fields, the on-chip portrait, selfies, or the face match score. Those are computed and discarded inside the verification pipeline. The consented claims surface only in the `verification.session.succeeded` webhook payload.
