Verification Sessions
Create, retrieve, cancel, and list verification sessions — the core resource of every Stile integration.
A verification session represents a single attempt to verify a user's age or identity. Your server creates one, hands the client_secret to the widget, and listens for the webhook that reports the outcome. Every session runs inside a published workflow, which carries the use case, target jurisdictions, and verification methods.
When do I need this?
This is the endpoint behind the widget's session-url mode — your backend creates the session and
returns the client_secret to the page. You'll also use it to retrieve, cancel, or list sessions
programmatically.
Examples show cURL, Python, Go, and Node.js. You can also use the Node.js SDK as a typed convenience wrapper, or call the API from any language — see HTTP API Overview.
The verification session object
Every endpoint on this page returns (or lists) this object. Timestamps are Unix seconds.
| Parameter | Type | Description |
|---|---|---|
id | string | Unique identifier for the session, prefixed vks_. |
object | string | Always "verification_session". |
status | string | Lifecycle state of the session. See the statuses table below. |
type | "identity" | "age" | "student" | The type of verification this session performs. |
methods | string[] | Verification methods compiled from the published workflow, lowercase (e.g. ["mdl", "document_capture"]). Configured in the workflow editor, never per request. |
current_method | string | null | The method the user is currently working through. null when no method is active. |
client_reference_id | string | null | Your internal ID for this user or transaction, echoed back in webhook events. |
return_url | string | null | Where the user is redirected after successful verification. |
cancel_url | string | null | Where the user is redirected if they cancel. |
client_secret | string | null | Secret the frontend widget uses to drive this session. Present only on the create response — null on retrieve and list. Treat it like a password; never log it. |
metadata | object | The key-value pairs you attached at creation. Values are strings. |
expires_at | number | When the session expires — 24 hours after creation. Unix seconds. |
completed_at | number | null | When the session completed. null until then. |
cancelled_at | number | null | When the session was cancelled, if it was. |
created | number | When the session was created. Unix seconds. |
updated | number | When the session was last updated. Unix seconds. |
verification_path | string | null | How the session was verified. null for the standard in-widget flow; set when an alternate path satisfied the session, such as returning-user or credential reuse. |
active_device | string | null | "mobile" while a phone holds the desktop-to-mobile handoff lock; null otherwise. Lets your desktop UI show a waiting state. |
device_locked_at | number | null | When the handoff lock was taken. null when no device holds the lock. |
age_tier | string | null | Resolved age tier, e.g. min_age_21. Tiers are ordered min_age_13 < min_age_16 < min_age_18 < min_age_21 — a higher tier satisfies a lower one, never the reverse. |
jurisdiction | string | null | Resolved jurisdiction code — ISO 3166-1 alpha-2, optionally with a region (e.g. "US-CA"). |
jurisdiction_audit_declared | string | null | Audit trail: the jurisdiction your request declared. |
jurisdiction_audit_ip_derived | string | null | Audit trail: the jurisdiction derived from the user's IP. |
jurisdiction_audit_resolved | string | null | Audit trail: the final jurisdiction the session resolved to. |
jurisdiction_audit_source | string | null | Audit trail: which input the resolved jurisdiction came from. |
jurisdiction_audit_mismatch | boolean | null | Audit trail: whether the declared and IP-derived jurisdictions disagreed. |
physical_check_required | boolean | Whether the resolved compliance rule requires a supplementary physical ID check. |
user_message | string | null | User-facing message shown in the widget. Reflects custom_user_message when set. |
compliance | object | null | Resolved compliance block — populated once the session has a resolved age tier. Contains jurisdiction, age_tier, mdl_acceptance_status (full | supplementary | not_recognized | unknown), mdl_legally_recognized, physical_check_required, user_message, governing_regulator, and rule_version. |
requires_email_otp | boolean | true when the workflow requires a returning user to prove email ownership via OTP before reuse. Recomputed on every retrieve. See Returning Users. |
results | array | The session's verification results. Included only when you pass expand[]=results. |
ip_jurisdiction | string | Create response only, when delivery_jurisdiction was passed: the jurisdiction derived from the request IP. |
jurisdiction_mismatch | boolean | Create response only, when delivery_jurisdiction was passed: true when the delivery jurisdiction and IP-derived jurisdiction disagree — a VPN/geolocation red flag. |
Session statuses
| Status | Description |
|---|---|
created | Session created, waiting for the user to start verification. |
pending | User has opened the verification UI. |
processing | Verification is actively being processed by a method. |
requires_input | A method requires additional user input. |
verified | Verification succeeded. Terminal state. |
failed | All methods were exhausted without success. Terminal state. |
cancelled | Cancelled by your server or the user. Terminal state. |
expired | The 24-hour expiry passed before completion. Terminal state. |
Create a session
/v1/verification_sessionsCreates a session and returns the full object, including the one-time client_secret your frontend needs.
Create sessions from your server
Use a secret key on your backend. Browser-side creation with a publishable key is deprecated —
responses carry a Stile-Deprecation: publishable-key-session-create header and require a
captcha_token. The widget's session-url mode gives you the server-side pattern with no extra
client code.
| Parameter | Type | Description |
|---|---|---|
typerequired | "identity" | "age" | "student" | The type of verification to perform. |
workflow_idrequired | string | ID of the published workflow to execute (wf_...). The workflow carries the use case, target jurisdictions, verification methods, and preferences — compliance is resolved from it. Passing a methods array is rejected; methods are configured on the workflow. |
return_url | string | URL to redirect to after successful verification. |
cancel_url | string | URL to redirect to if the user cancels. |
client_reference_id | string | Your internal ID for this user or transaction. Returned in webhook events. |
metadata | object | Up to 50 key-value pairs of arbitrary data. Values must be strings. |
idempotency_key | string | A unique key to prevent duplicate session creation. If a session was already created with this key, the existing session is returned. |
jurisdiction | string | Explicit jurisdiction override (e.g. "US-CA", "US-TX"). If omitted, derived from request geo headers. |
custom_user_message | string | Override the default user-facing message shown in the widget. Max 500 characters. |
email | string | User email for Verified Person lookup. Enables cross-site verification reuse when accept_existing is true. |
phone | string | User phone for Verified Person lookup. Used alongside or instead of email. |
accept_existing | boolean | When true, accepts existing cross-site verifications instead of requiring a new one. Requires email or phone. |
min_strength | string | Minimum credential strength for existing verifications. E.g. "document_capture" to only accept doc capture or stronger. |
max_age | string | Maximum age of existing verification. Format: "<number>d" (e.g. "30d" for 30 days). |
required_methods | string[] | Require ALL of these methods to be previously verified when reusing existing credentials. |
credential_hash | string | Credential hash from a prior verification, used to match and reuse an existing verified credential. |
delivery_jurisdiction | string | Optional delivery/shipping jurisdiction (e.g. "US-OR"). When provided, the response includes ip_jurisdiction and jurisdiction_mismatch to flag VPN/geolocation discrepancies. |
vp_token | string | Reusable VP token from a prior verification (the widget stores it in localStorage and passes it automatically). See Returning Users. |
skip_verification | boolean | Sandbox orgs only; rejected otherwise. Instantly creates a verified session — no document capture or liveness — so you can exercise webhooks end-to-end. Rejected with a 400 in a non-sandbox org. |
captcha_token | string | Cloudflare Turnstile token. Required only when creating sessions with a publishable key from the browser; not needed for secret keys. |
curl https://api.stile.id/v1/verification_sessions \
-H "Authorization: Bearer stile_sk_..." \
-H "Content-Type: application/json" \
-d '{
"type": "age",
"workflow_id": "wf_YOUR_WORKFLOW_ID",
"return_url": "https://yourapp.com/done",
"client_reference_id": "user_123"
}'import requests
res = requests.post(
"https://api.stile.id/v1/verification_sessions",
headers={"Authorization": "Bearer stile_sk_..."},
json={
"type": "age",
"workflow_id": "wf_YOUR_WORKFLOW_ID",
"return_url": "https://yourapp.com/done",
"client_reference_id": "user_123",
},
)
session = res.json()body := strings.NewReader(`{
"type": "age",
"workflow_id": "wf_YOUR_WORKFLOW_ID",
"return_url": "https://yourapp.com/done",
"client_reference_id": "user_123"
}`)
req, _ := http.NewRequest("POST", "https://api.stile.id/v1/verification_sessions", body)
req.Header.Set("Authorization", "Bearer stile_sk_...")
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)const session = await stile.verificationSessions.create({
type: "age",
workflow_id: "wf_YOUR_WORKFLOW_ID",
return_url: "https://yourapp.com/done",
client_reference_id: "user_123",
});
// Returns:
// {
// id: "vks_abc123",
// object: "verification_session",
// status: "created",
// type: "age",
// client_secret: "eyJ...", ← pass to frontend
// methods: ["mdl", "document_capture"], ← resolved from the workflow
// age_tier: "min_age_21", ← resolved from workflow + jurisdiction
// jurisdiction: "US-CA", ← auto-detected
// requires_email_otp: false, ← true when a returning user must prove email ownership
// expires_at: 1741651200,
// created: 1741564800,
// ip_jurisdiction: "US-CA", ← only when delivery_jurisdiction is set
// jurisdiction_mismatch: false, ← true if delivery ≠ IP jurisdiction
// }Idempotent retries
POST requests also accept an Idempotency-Key header. The same key with the same body replays the
original response; the same key with a different body returns a 409 idempotency_key_reuse.
Retrieve a session
/v1/verification_sessions/:idPass expand[]=results to include the full verification results array in the response.
curl "https://api.stile.id/v1/verification_sessions/vks_abc123?expand[]=results" \
-H "Authorization: Bearer stile_sk_..."import requests
res = requests.get(
"https://api.stile.id/v1/verification_sessions/vks_abc123",
headers={"Authorization": "Bearer stile_sk_..."},
params={"expand[]": "results"},
)
session = res.json()req, _ := http.NewRequest("GET", "https://api.stile.id/v1/verification_sessions/vks_abc123?expand[]=results", nil)
req.Header.Set("Authorization", "Bearer stile_sk_...")
res, _ := http.DefaultClient.Do(req)const session = await stile.verificationSessions.retrieve("vks_abc123", {
expand: ["results"],
});
// session.results — array of VerificationResult objectsWebhooks first, polling second
Treat webhook delivery as the source of truth for status changes — poll this endpoint only on cold start or for reconciliation.
Cancel a session
/v1/verification_sessions/:id/cancelCancellable while the session is still open (created, pending, or requires_input). A session in a terminal state (verified, failed, cancelled, or expired) returns a 409 session_terminal.
curl -X POST https://api.stile.id/v1/verification_sessions/vks_abc123/cancel \
-H "Authorization: Bearer stile_sk_..."import requests
res = requests.post(
"https://api.stile.id/v1/verification_sessions/vks_abc123/cancel",
headers={"Authorization": "Bearer stile_sk_..."},
)
session = res.json()req, _ := http.NewRequest("POST", "https://api.stile.id/v1/verification_sessions/vks_abc123/cancel", nil)
req.Header.Set("Authorization", "Bearer stile_sk_...")
res, _ := http.DefaultClient.Do(req)const session = await stile.verificationSessions.cancel("vks_abc123");
// session.status === "cancelled"List sessions
/v1/verification_sessionsReturns a paginated list of sessions for your organization, newest first.
| Parameter | Type | Description |
|---|---|---|
limit | number= 10 | Number of sessions to return. Between 1 and 100. |
starting_after | string | Session ID to start after (cursor-based pagination). Returns sessions created before this ID. |
ending_before | string | Session ID to end before (cursor-based pagination). Returns sessions created after this ID. |
status | string | Filter by session status (e.g. "verified", "failed", "created"). |
expand[] | string[] | Expansions to apply to each session in the list (e.g. "results"). |
curl "https://api.stile.id/v1/verification_sessions?limit=20" \
-H "Authorization: Bearer stile_sk_..."import requests
res = requests.get(
"https://api.stile.id/v1/verification_sessions",
headers={"Authorization": "Bearer stile_sk_..."},
params={"limit": 20},
)
data = res.json()req, _ := http.NewRequest("GET", "https://api.stile.id/v1/verification_sessions?limit=20", nil)
req.Header.Set("Authorization", "Bearer stile_sk_...")
res, _ := http.DefaultClient.Do(req)const { data, has_more } = await stile.verificationSessions.list({
limit: 20,
});
for (const session of data) {
console.log(session.id, session.status);
}Verification methods
The session's methods array is compiled from the published workflow — you configure methods in the workflow editor, not per request. These are the methods a workflow can use:
| Method | Value | Description |
|---|---|---|
| Mobile Driver's License | mdl | ISO 18013-5 mDL via OID4VP. Highest assurance. |
| Mobile Identity | mid | Government-issued digital identity credential. |
| EUDI PID | eudi_pid | European Digital Identity credential. |
| Facial Age Estimation | facial_age | AI age estimate from a selfie. No document required. |
| Selfie Liveness | selfie_liveness | Confirms a real person is present via a live selfie check. |
| Selfie Match | selfie_match | Matches a selfie against a previously verified photo. |
| Self Attestation | self_attestation | User self-declares their age or identity. Lowest assurance. |
| Document Capture | document_capture | Extracts and verifies data from a physical ID document photo. |
| NFC Passport | nfc_passport | Reads the passport chip via NFC (ICAO 9303). |
| Carrier Lookup | carrier_lookup | Phone carrier verification for age signals. |
| Open Banking | open_banking | Bank account data for identity signals. |
| Parental Consent | parental_consent | Collects consent from a parent or guardian. |
| Student Verification | student | Academic enrollment verification. |
Methods are automatically filtered by jurisdiction — for example, mDL is removed in states that don't recognize digital driver's licenses, even if the workflow includes it.
Compliance resolution
When you create a session, regulatory rules are resolved internally from the workflow's use case and the user's jurisdiction — the result lands in the session's compliance block. To inspect those rules yourself (e.g. to drop prohibited items from a cart), use the Compliance API.
Errors on this resource
All errors share the standard shape (error.type, error.code, error.message, error.param, error.request_id). These are the codes you'll most likely hit on session endpoints:
| Code | Status | Description |
|---|---|---|
captcha_required | 400 | A Cloudflare Turnstile token is required when creating sessions from the browser with a publishable key. Pass captcha_token. |
webhook_required | 400 | A live (non-sandbox) org requires at least one active webhook endpoint before sessions can be created. See Webhook Endpoints. |
email_not_verified | 400 | The returning user must prove email ownership via OTP before their prior verification can be reused. |
jurisdiction_unresolvable | 422 | No jurisdiction could be resolved for the request. Pass jurisdiction explicitly. |
use_case_prohibited | 422 | The workflow's use case is prohibited in the resolved jurisdiction. |
accept_existing_rate_limited | 429 | Too many accept_existing attempts — limited to 5 per email per 15 minutes. |
test_quota_exceeded | 402 | The sandbox quota (500 verifications per calendar month) is used up. Resets on the 1st. |
billing_suspended | 402 | Billing on your organization is suspended; requests are rejected until it is resolved. |
idempotency_key_reuse | 409 | The Idempotency-Key was already used with a different request body. |
session_not_found | 404 | No session with this ID exists for your organization. |
See Error Handling for the full catalog, status codes, and retry guidance.
Next steps
Listen for webhooks
Get verification_session.verified the moment it happens — webhook delivery is the source of truth.
Events API
The event objects behind every webhook delivery, with list and retrieve endpoints.
Verified Person
Look up returning users and skip verification when a strong credential already exists.
Error handling
Error shapes, status codes, and safe retry guidance for every endpoint.
Overview
Base URL, authentication, request and response conventions, pagination, the error object, idempotency, and rate limits — everything shared across the Stile HTTP API.
Compliance
Look up regulatory rules by product type and jurisdiction — required age tiers, prohibited products, allowed verification methods, and most-restrictive resolution for mixed carts.