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 only need the widget (<stile-button>) for the frontend and a webhook handler on the backend. You may never need to call the API directly.
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's a single environment — every organization is live by default. To build and test without billing or a webhook endpoint, enable sandbox mode on a testing organization. See Testing for what sandbox mode covers.
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)import Stile from "@stile/node";
const stile = new Stile("stile_sk_...", { baseUrl: "https://api.stile.id" });
const session = await stile.verificationSessions.create({
type: "age",
workflow_id: "wf_YOUR_WORKFLOW_ID",
});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 for most lists — webhook deliveries default to 20) |
starting_after | Cursor: return results after this ID |
ending_before | Cursor: return results before this ID. Supported on most list endpoints |
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 endpoints support the expand[] parameter to include related objects inline. For example, both retrieving and listing verification sessions accept expand[]=results to embed each session's per-method verification results:
# 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
All errors return a JSON body with this structure:
{
"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 | The broad category of error: invalid_request_error, authentication_error, rate_limit_error, or api_error. |
code | string | 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 | The request parameter the error relates to, when applicable. |
request_id | string | Unique ID for this request. Include it when contacting support. |
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 state (e.g. session_terminal, idempotency_key_reuse). |
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
409idempotency_key_reuse.
The Node.js SDK accepts the same key as a per-request option: stile.verificationSessions.create(params, { idempotencyKey: "order_12345" }).
Rate limiting
Requests are rate-limited per API key over a per-minute rolling window — 1,000 requests/minute for secret keys, 100/minute for publishable keys.
Rate limit headers are included in every response:
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 SDK
If you prefer a typed client for Node.js / TypeScript, the @stile/node package wraps every endpoint documented here. Every SDK method maps 1:1 to an HTTP call — choose whichever you prefer.
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.