Overview
Base URL, authentication, request and response conventions, pagination, the error object, idempotency, and rate limits — everything shared across the Stile HTTP API.
The Stile API is a standard REST API: predictable resource URLs, JSON request and response bodies, conventional HTTP status codes, and Bearer-token authentication. Any language that can make HTTP requests can integrate — no SDK required. This page covers the conventions shared by every endpoint; the resource pages linked at the bottom document each endpoint in detail.
Widget handles the client side
Most integrations use the public CDN <stile-frame> widget in the browser, an
authenticated backend endpoint that creates sessions through this HTTP API, and a signed
webhook handler that confirms results.
Base URL
https://api.stile.id/v1/All endpoint paths in this reference are relative to this base. The API version (v1) is part of the URL path.
Authentication
Pass your secret key as a Bearer token in the Authorization header:
curl https://api.stile.id/v1/verification_sessions \
-H "Authorization: Bearer stile_sk_YOUR_SECRET_KEY" \
-H "Content-Type: application/json"There are two kinds of keys:
| Key type | Format | Where it belongs |
|---|---|---|
| Secret | stile_sk_... | Server-side only. Full API access. |
| Publishable | stile_pk_... | Frontend-safe. Limited scope. |
Keys are managed at dashboard.stile.id/api-keys; secret keys are shown once at creation. There is one API environment, and every organization is live by default. To build and test without billing or a required webhook endpoint, ask Stile support to enable sandbox mode on a dedicated testing organization. Sandbox changes organization behavior, not the base URL or key format.
Keep secret keys on the server
Never embed a stile_sk_ key in browser code or a mobile app. Anything client-side should use the
widget's session-url mode — your backend creates the session and hands the page the session's
client_secret. See Security.
See Authentication for the full breakdown of key types and scopes.
Making a request
Here's the same API call — creating a verification session — in every supported language:
curl -X POST 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"}'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"},
)
session = res.json()body := strings.NewReader(`{"type":"age","workflow_id":"wf_YOUR_WORKFLOW_ID"}`)
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 response = await fetch("https://api.stile.id/v1/verification_sessions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.STILE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
type: "age",
workflow_id: "wf_YOUR_WORKFLOW_ID",
}),
});
const session = await response.json();Every session runs inside a published workflow — workflow_id is required, and the workflow carries the use case, jurisdictions, and verification methods. See Verification Sessions for the full parameter reference.
Request format
- Content-Type:
application/jsonfor all POST requests - Query parameters: for GET requests and filtering
# POST — JSON body
curl -X POST 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"}'
# GET — query parameters
curl "https://api.stile.id/v1/verification_sessions?limit=10&status=verified" \
-H "Authorization: Bearer stile_sk_..."Response format
Responses are JSON. Every resource includes an object field identifying its type. Persistent objects also carry an id with a resource-specific prefix (vks_ for sessions, evt_ for events) and Unix-second timestamps (created, updated, expires_at, and so on):
{
"id": "vks_abc123",
"object": "verification_session",
"status": "verified",
"type": "age",
"created": 1741564800
}Pagination
List endpoints use cursor-based pagination:
| Parameter | Description |
|---|---|
limit | Number of results (1-100; default 10) |
starting_after | In a newest-first list, return the older records that follow this ID |
ending_before | In a newest-first list, return the newer records that precede this ID |
starting_after and ending_before are mutually exclusive. Supplying both returns a 400 parameter_invalid response.
List responses share a common envelope with object: "list" and a has_more flag indicating whether more results exist:
{
"object": "list",
"url": "/v1/verification_sessions",
"has_more": true,
"data": [...]
}To paginate forward, pass the last item's id as starting_after:
curl "https://api.stile.id/v1/verification_sessions?limit=10&starting_after=vks_abc123" \
-H "Authorization: Bearer stile_sk_..."Expanding responses
Some retrieve endpoints support expand[] to include related objects inline. A verification-session retrieve supports results and document expansions:
# Include verification results in the session response
curl "https://api.stile.id/v1/verification_sessions/vks_abc123?expand[]=results" \
-H "Authorization: Bearer stile_sk_..."The error object
API errors return a top-level error object. Core public routes return the complete structure below; a few older routes may omit type, code, param, or request_id, so treat those fields as optional:
{
"error": {
"type": "invalid_request_error",
"code": "parameter_invalid",
"message": "No such verification_session: 'vks_unknown'",
"param": "id",
"request_id": "req_abc123"
}
}| Parameter | Type | Description |
|---|---|---|
type | string | undefined | The broad category of error: invalid_request_error, authentication_error, rate_limit_error, or api_error. |
code | string | undefined | A stable, machine-readable string identifying the exact error (e.g. parameter_invalid, resource_missing). Branch on this, not on message. |
message | string | A human-readable explanation for debugging. Wording may change — don't parse it or show it to end users. |
param | string | undefined | The request parameter the error relates to, when applicable. |
request_id | string | undefined | Unique ID for this request. Include it when contacting support. If omitted from the body, check X-Request-Id. |
Error types
| Type | Meaning |
|---|---|
invalid_request_error | The request was malformed or can't be processed as sent. |
authentication_error | The API key is missing, invalid, or lacks the required scope. |
rate_limit_error | Too many requests — back off and retry. |
api_error | Something failed on Stile's side. Safe to retry with backoff. |
HTTP status codes
| Status | Meaning |
|---|---|
200, 201 | Success. |
400 | Bad request — malformed or missing parameters (e.g. parameter_invalid). |
401 | Authentication failed — missing or invalid API key (api_key_invalid). |
402 | Billing issue (billing_suspended, test_quota_exceeded). |
403 | The key isn't allowed to do this (e.g. publishable_key_scope). |
404 | Resource doesn't exist (resource_missing, session_not_found). |
409 | Conflict with current resource state (e.g. session_terminal). |
422 | Request understood but rejected (e.g. use_case_prohibited, jurisdiction_unresolvable). |
429 | Rate limited (rate_limit_exceeded) — retry after the Retry-After header. |
500 | Server error — retry with backoff. |
Retry 429 (after Retry-After) and 5xx responses with exponential backoff — min(500ms * 2^attempt + jitter, 30s) is a good schedule. Never auto-retry other 4xx errors; fix the request instead. See Error Handling for resource-specific codes and recovery strategies.
Idempotency
POST requests accept an Idempotency-Key header to prevent duplicates:
curl -X POST https://api.stile.id/v1/verification_sessions \
-H "Authorization: Bearer stile_sk_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order_12345" \
-d '{"type": "age", "workflow_id": "wf_YOUR_WORKFLOW_ID"}'Pick a key tied to the operation you're protecting (an order ID, a user ID plus action). The semantics:
- Same key, same body — the original response is replayed; no duplicate is created.
- Same key, different body — the request is rejected with
400idempotency_key_reuse.
Send the same value in the Idempotency-Key header from every backend language.
Rate limiting
Requests are rate-limited per API key in fixed one-minute windows — 1,000 requests/minute for secret keys, 100/minute for publishable keys.
Authenticated requests processed by the rate limiter include these headers:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 997
X-RateLimit-Reset: 1741564920When exceeded, the API returns 429 with a Retry-After header. Wait the specified seconds before retrying.
Node.js private preview
@stile/node is an unpublished private preview. Public integrations should use the HTTP contract documented here. If Stile has granted your organization package access, see the private-preview reference.
API playground
The core endpoints in this reference have interactive counterparts in the API Playground section of these docs, generated from Stile's OpenAPI spec (/openapi.yaml). Use it to explore request and response schemas alongside the prose reference here.
Explore the reference
Verification Sessions
Create, retrieve, list, and cancel verification sessions — the core resource.
Compliance
Check jurisdiction rules for your use cases before creating sessions.
Verified Person
Look up returning users by email or phone to reuse prior verifications.
Webhook Endpoints
Manage webhook endpoints, inspect deliveries, and rotate signing secrets.
Events
Retrieve and list the event objects behind every webhook delivery.