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. Public integrations can call the API from any language; approved preview users can also use the private-preview Node.js SDK. 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. The default is 24 hours after creation; a workflow can configure a different timeout. 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. |
under_review | Verification is held for manual review. Do not grant or deny access until a review event resolves it. |
verified | Verification succeeded. Terminal unless a later review hold is opened. |
failed | All methods were exhausted, or a review rejected the session. Terminal state. |
cancelled | Cancelled by your server or the user. Terminal state. |
expired | The configured session expiry passed before completion. Terminal state. |
When a session enters under_review, listen for session_review.approved, session_review.rejected, or session_review.escalated. Keep the related transaction paused until approval or rejection; escalation leaves it under review.
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. |
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 an organization-scoped Verified Person lookup 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 an existing credential created by this organization. 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
Send a unique Idempotency-Key header when creating a session. The same key with the same body
replays the original response; the same key with a different body returns a 400
idempotency_key_reuse.
Retrieve a session
/v1/verification_sessions/:idPass expand[]=results to include the full verification results array in the response.
Publishable-key reads need the session capability
A browser/widget request authenticated with a publishable key must also send the session's
client_secret in X-Stile-Session-Secret. Publishable-key requests cannot use expand[] to
retrieve verification evidence. Server-side secret-key requests do not send this header.
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"). |
client_reference_id | string | Return sessions whose client reference ID exactly matches this value. |
search | string | Search session IDs and client reference IDs, case-insensitively. |
risk_tier | string | Filter by "clear", "review", "block", or sessions flagged as "bot". |
Use only one cursor per request: starting_after moves toward older sessions and ending_before moves toward newer sessions.
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.
Primary widget paths:
| Method | Value | Description |
|---|---|---|
| Mobile Driver's License | mdl | ISO 18013-5 mDL via OID4VP. Highest assurance. |
| Document Capture | document_capture | Extracts and verifies data from a physical ID document photo. |
| NFC Passport | nfc_passport | Reads an ICAO 9303 passport chip in a supported native embed. Standard browser Web NFC cannot perform this read. |
Runnable active layers and declarations:
| Method | Value | Description |
|---|---|---|
| Selfie Liveness | selfie_liveness | Active biometric layer that confirms a real person is present. Configure it with a primary method. |
| Selfie Match | selfie_match | Active biometric layer that matches a selfie against a verified portrait. Configure it with a primary method. |
| Self Attestation | self_attestation | Additional declaration step. Do not configure it as the only hosted-widget path. |
Runnable supplementary checks:
| Check | Value | Description |
|---|---|---|
| IP Analysis | ip_analysis | Evaluates IP-derived risk signals. |
| Device Risk | device_risk | Evaluates device and automation risk. |
| Geolocation | geolocation | Adds a location signal for workflow policy and review decisions. |
Active layers, declarations, and passive checks can appear in session.methods, but none of them replaces the renderable primary required by a hosted-widget path. Passive checks do not mint a reusable identity credential.
The following values are reserved for planned methods and cannot be published in a runnable workflow yet: mid, eudi_pid, facial_age, carrier_lookup, open_banking, parental_consent, student, and standalone apple_wallet. The workflow validator rejects them as not implemented.
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 | More than 10 accept_existing attempts for one email in a 15-minute window. |
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 | 400 | The Idempotency-Key was already used with a different request body. Use the original body or a new key. |
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.