stile
HTTP API

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.

ParameterTypeDescription
idstringUnique identifier for the session, prefixed vks_.
objectstringAlways "verification_session".
statusstringLifecycle state of the session. See the statuses table below.
type"identity" | "age" | "student"The type of verification this session performs.
methodsstring[]Verification methods compiled from the published workflow, lowercase (e.g. ["mdl", "document_capture"]). Configured in the workflow editor, never per request.
current_methodstring | nullThe method the user is currently working through. null when no method is active.
client_reference_idstring | nullYour internal ID for this user or transaction, echoed back in webhook events.
return_urlstring | nullWhere the user is redirected after successful verification.
cancel_urlstring | nullWhere the user is redirected if they cancel.
client_secretstring | nullSecret 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.
metadataobjectThe key-value pairs you attached at creation. Values are strings.
expires_atnumberWhen the session expires — 24 hours after creation. Unix seconds.
completed_atnumber | nullWhen the session completed. null until then.
cancelled_atnumber | nullWhen the session was cancelled, if it was.
creatednumberWhen the session was created. Unix seconds.
updatednumberWhen the session was last updated. Unix seconds.
verification_pathstring | nullHow 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_devicestring | null"mobile" while a phone holds the desktop-to-mobile handoff lock; null otherwise. Lets your desktop UI show a waiting state.
device_locked_atnumber | nullWhen the handoff lock was taken. null when no device holds the lock.
age_tierstring | nullResolved 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.
jurisdictionstring | nullResolved jurisdiction code — ISO 3166-1 alpha-2, optionally with a region (e.g. "US-CA").
jurisdiction_audit_declaredstring | nullAudit trail: the jurisdiction your request declared.
jurisdiction_audit_ip_derivedstring | nullAudit trail: the jurisdiction derived from the user's IP.
jurisdiction_audit_resolvedstring | nullAudit trail: the final jurisdiction the session resolved to.
jurisdiction_audit_sourcestring | nullAudit trail: which input the resolved jurisdiction came from.
jurisdiction_audit_mismatchboolean | nullAudit trail: whether the declared and IP-derived jurisdictions disagreed.
physical_check_requiredbooleanWhether the resolved compliance rule requires a supplementary physical ID check.
user_messagestring | nullUser-facing message shown in the widget. Reflects custom_user_message when set.
complianceobject | nullResolved 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_otpbooleantrue when the workflow requires a returning user to prove email ownership via OTP before reuse. Recomputed on every retrieve. See Returning Users.
resultsarrayThe session's verification results. Included only when you pass expand[]=results.
ip_jurisdictionstringCreate response only, when delivery_jurisdiction was passed: the jurisdiction derived from the request IP.
jurisdiction_mismatchbooleanCreate response only, when delivery_jurisdiction was passed: true when the delivery jurisdiction and IP-derived jurisdiction disagree — a VPN/geolocation red flag.

Session statuses

StatusDescription
createdSession created, waiting for the user to start verification.
pendingUser has opened the verification UI.
processingVerification is actively being processed by a method.
requires_inputA method requires additional user input.
verifiedVerification succeeded. Terminal state.
failedAll methods were exhausted without success. Terminal state.
cancelledCancelled by your server or the user. Terminal state.
expiredThe 24-hour expiry passed before completion. Terminal state.

Create a session

POST/v1/verification_sessions

Creates 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.

ParameterTypeDescription
typerequired"identity" | "age" | "student"The type of verification to perform.
workflow_idrequiredstringID 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_urlstringURL to redirect to after successful verification.
cancel_urlstringURL to redirect to if the user cancels.
client_reference_idstringYour internal ID for this user or transaction. Returned in webhook events.
metadataobjectUp to 50 key-value pairs of arbitrary data. Values must be strings.
idempotency_keystringA unique key to prevent duplicate session creation. If a session was already created with this key, the existing session is returned.
jurisdictionstringExplicit jurisdiction override (e.g. "US-CA", "US-TX"). If omitted, derived from request geo headers.
custom_user_messagestringOverride the default user-facing message shown in the widget. Max 500 characters.
emailstringUser email for Verified Person lookup. Enables cross-site verification reuse when accept_existing is true.
phonestringUser phone for Verified Person lookup. Used alongside or instead of email.
accept_existingbooleanWhen true, accepts existing cross-site verifications instead of requiring a new one. Requires email or phone.
min_strengthstringMinimum credential strength for existing verifications. E.g. "document_capture" to only accept doc capture or stronger.
max_agestringMaximum age of existing verification. Format: "<number>d" (e.g. "30d" for 30 days).
required_methodsstring[]Require ALL of these methods to be previously verified when reusing existing credentials.
credential_hashstringCredential hash from a prior verification, used to match and reuse an existing verified credential.
delivery_jurisdictionstringOptional delivery/shipping jurisdiction (e.g. "US-OR"). When provided, the response includes ip_jurisdiction and jurisdiction_mismatch to flag VPN/geolocation discrepancies.
vp_tokenstringReusable VP token from a prior verification (the widget stores it in localStorage and passes it automatically). See Returning Users.
skip_verificationbooleanSandbox 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_tokenstringCloudflare 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

GET/v1/verification_sessions/:id

Pass 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 objects

Webhooks 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

POST/v1/verification_sessions/:id/cancel

Cancellable 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

GET/v1/verification_sessions

Returns a paginated list of sessions for your organization, newest first.

ParameterTypeDescription
limitnumber= 10Number of sessions to return. Between 1 and 100.
starting_afterstringSession ID to start after (cursor-based pagination). Returns sessions created before this ID.
ending_beforestringSession ID to end before (cursor-based pagination). Returns sessions created after this ID.
statusstringFilter 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:

MethodValueDescription
Mobile Driver's LicensemdlISO 18013-5 mDL via OID4VP. Highest assurance.
Mobile IdentitymidGovernment-issued digital identity credential.
EUDI PIDeudi_pidEuropean Digital Identity credential.
Facial Age Estimationfacial_ageAI age estimate from a selfie. No document required.
Selfie Livenessselfie_livenessConfirms a real person is present via a live selfie check.
Selfie Matchselfie_matchMatches a selfie against a previously verified photo.
Self Attestationself_attestationUser self-declares their age or identity. Lowest assurance.
Document Capturedocument_captureExtracts and verifies data from a physical ID document photo.
NFC Passportnfc_passportReads the passport chip via NFC (ICAO 9303).
Carrier Lookupcarrier_lookupPhone carrier verification for age signals.
Open Bankingopen_bankingBank account data for identity signals.
Parental Consentparental_consentCollects consent from a parent or guardian.
Student VerificationstudentAcademic 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:

CodeStatusDescription
captcha_required400A Cloudflare Turnstile token is required when creating sessions from the browser with a publishable key. Pass captcha_token.
webhook_required400A live (non-sandbox) org requires at least one active webhook endpoint before sessions can be created. See Webhook Endpoints.
email_not_verified400The returning user must prove email ownership via OTP before their prior verification can be reused.
jurisdiction_unresolvable422No jurisdiction could be resolved for the request. Pass jurisdiction explicitly.
use_case_prohibited422The workflow's use case is prohibited in the resolved jurisdiction.
accept_existing_rate_limited429Too many accept_existing attempts — limited to 5 per email per 15 minutes.
test_quota_exceeded402The sandbox quota (500 verifications per calendar month) is used up. Resets on the 1st.
billing_suspended402Billing on your organization is suspended; requests are rejected until it is resolved.
idempotency_key_reuse409The Idempotency-Key was already used with a different request body.
session_not_found404No session with this ID exists for your organization.

See Error Handling for the full catalog, status codes, and retry guidance.

Next steps

On this page