Docs

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.

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. The default is 24 hours after creation; a workflow can configure a different timeout. 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.
under_reviewVerification is held for manual review. Do not grant or deny access until a review event resolves it.
verifiedVerification succeeded. Terminal unless a later review hold is opened.
failedAll methods were exhausted, or a review rejected the session. Terminal state.
cancelledCancelled by your server or the user. Terminal state.
expiredThe 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

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.
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 an organization-scoped Verified Person lookup when accept_existing is true.
phonestringUser phone for Verified Person lookup. Used alongside or instead of email.
accept_existingbooleanWhen true, accepts an existing credential created by this organization. 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

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

GET/v1/verification_sessions/:id

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

Synthetic dashboard example showing recent verification sessions with status, verification method, age result, and completion duration

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").
client_reference_idstringReturn sessions whose client reference ID exactly matches this value.
searchstringSearch session IDs and client reference IDs, case-insensitively.
risk_tierstringFilter 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:

MethodValueDescription
Mobile Driver's LicensemdlISO 18013-5 mDL via OID4VP. Highest assurance.
Document Capturedocument_captureExtracts and verifies data from a physical ID document photo.
NFC Passportnfc_passportReads an ICAO 9303 passport chip in a supported native embed. Standard browser Web NFC cannot perform this read.

Runnable active layers and declarations:

MethodValueDescription
Selfie Livenessselfie_livenessActive biometric layer that confirms a real person is present. Configure it with a primary method.
Selfie Matchselfie_matchActive biometric layer that matches a selfie against a verified portrait. Configure it with a primary method.
Self Attestationself_attestationAdditional declaration step. Do not configure it as the only hosted-widget path.

Runnable supplementary checks:

CheckValueDescription
IP Analysisip_analysisEvaluates IP-derived risk signals.
Device Riskdevice_riskEvaluates device and automation risk.
GeolocationgeolocationAdds 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:

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_limited429More than 10 accept_existing attempts for one email in a 15-minute window.
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_reuse400The Idempotency-Key was already used with a different request body. Use the original body or a new key.
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