Node.js SDK
The typed Node.js and TypeScript client for the Stile API — resource methods, automatic retries, idempotency, and built-in webhook signature verification.
@stile/node is the server-side client for the Stile API. Every method maps 1:1 to an HTTP endpoint, with typed parameters and responses, automatic retries, and built-in webhook signature verification.
You can also use the HTTP API directly
The Node.js SDK is a thin convenience wrapper — every method maps 1:1 to an HTTP endpoint documented in the HTTP API reference. Use the HTTP API directly if you're working in Python, Ruby, Go, PHP, or any other language. For webhook signature verification in any language, see the verification guide.
Installation
npm install @stile/nodepnpm add @stile/nodeyarn add @stile/nodeServer-only
The Node.js SDK is server-only — it authenticates with your secret key, which must never reach the browser. For frontend integration, use the Widget SDK.
Initialization
Create a single client instance and reuse it throughout your application. Pass your API key as the first argument — keys are managed at dashboard.stile.id/api-keys.
import Stile from "@stile/node";
const stile = new Stile(process.env.STILE_API_KEY!);Use the same stile_sk_ secret key everywhere — there's one environment, so there are no separate test and live keys. For building and testing, enable sandbox mode on a dedicated testing organization.
Configuration
The constructor accepts an options object as its second argument:
const stile = new Stile(process.env.STILE_API_KEY!, {
baseUrl: "https://api.stile.id",
maxRetries: 2, // default, retries on 5xx, network errors, and 429s
timeout: 30_000, // default, 30 seconds
});| Parameter | Type | Description |
|---|---|---|
baseUrl | string | Origin for API requests. Set it explicitly to https://api.stile.id. |
maxRetries | number= 2 | Automatic retries for failed requests. The SDK retries 5xx responses, network errors, and 429 rate limits. |
timeout | number= 30000 | Per-request timeout in milliseconds (30 seconds). |
Resources
Each resource on the client maps to one section of the HTTP API reference:
| Resource | Methods | Reference |
|---|---|---|
stile.verificationSessions | create, retrieve, cancel, list | Verification Sessions |
stile.webhookEndpoints | create, retrieve, update, del (alias delete), list, listDeliveries | Webhook Endpoints |
stile.events | retrieve, list | Events |
stile.compliance | check | Compliance |
stile.verifiedPersons | lookup | Verified Person |
stile.webhooks | fromRequest, constructEvent | Webhook verification |
Verification sessions
Create a session
Pass the workflow_id of a published workflow — it carries the use case, jurisdictions, and verification methods, and the API resolves the required age tier internally based on the user's jurisdiction. Don't pass a methods array: methods are configured on the workflow, and the API rejects requests that combine the two.
const session = await stile.verificationSessions.create({
type: "age",
workflow_id: "wf_YOUR_WORKFLOW_ID",
return_url: "https://yourapp.com/verify/done",
cancel_url: "https://yourapp.com/verify/cancel",
client_reference_id: "user_123",
// Optional: pass delivery address jurisdiction for VPN mismatch detection
delivery_jurisdiction: "US-OR",
});
// session.id — e.g. "vks_abc123"
// session.client_secret — pass to the frontend widget
// session.methods — resolved from the workflow
// session.age_tier — e.g. "min_age_21", resolved from workflow + jurisdiction
// session.expires_at — Unix timestamp (24h from now)
// session.status — "created"
// session.ip_jurisdiction — "US-CA" (detected from IP, if delivery_jurisdiction was set)
// session.jurisdiction_mismatch — true if delivery ≠ IP jurisdictionFor the full list of create parameters, see the Verification Sessions reference.
To prevent duplicate sessions when a request is retried, pass an idempotency key as the second argument — it's sent as the Idempotency-Key header:
const session = await stile.verificationSessions.create(
{ type: "age", workflow_id: "wf_YOUR_WORKFLOW_ID" },
{ idempotencyKey: "order_12345" },
);Replaying the same key with the same body returns the original response. Reusing the key with a different body fails with a 409 idempotency_key_reuse error.
Retrieve a session
const session = await stile.verificationSessions.retrieve("vks_abc123");
// Include the full verification results array:
const expanded = await stile.verificationSessions.retrieve("vks_abc123", {
expand: ["results"],
});Cancel a session
const session = await stile.verificationSessions.cancel("vks_abc123");
// session.status === "cancelled"List sessions
const { data, has_more } = await stile.verificationSessions.list({
limit: 20,
starting_after: "vks_xyz",
status: "verified",
});| Parameter | Type | Description |
|---|---|---|
limit | number= 10 | Number of sessions per page, between 1 and 100. |
starting_after | string | Cursor — return results after this session ID. |
ending_before | string | Cursor — return results before this session ID. |
status | string | Filter by session status, e.g. "verified". |
expand | string[] | Expand related data on each session, e.g. ["results"]. |
Webhook endpoints
Manage webhook endpoints in code, or from the dashboard. enabled_events accepts specific event types or ["*"] for everything.
Live orgs require a webhook endpoint
Session creation fails with a 400 webhook_required error until your organization has at least
one active webhook endpoint. Sandbox organizations are exempt.
Create an endpoint
const endpoint = await stile.webhookEndpoints.create({
url: "https://yourapp.com/api/webhooks",
enabled_events: ["verification_session.verified", "verification_session.failed"],
description: "Production webhook",
});
// endpoint.secret — save this to verify signaturesRetrieve, update, delete
The update method accepts url, enabled_events, status ("enabled" or "disabled"), description, and metadata. del() is also exported under the alias delete().
// Retrieve
const ep = await stile.webhookEndpoints.retrieve("we_abc123");
// Update
await stile.webhookEndpoints.update("we_abc123", {
enabled_events: ["*"],
status: "enabled",
});
// Delete
await stile.webhookEndpoints.del("we_abc123");List endpoints and deliveries
listDeliveries returns the delivery attempts for one endpoint and paginates with limit (default 20), starting_after, and ending_before:
// List
const { data } = await stile.webhookEndpoints.list();
// List delivery attempts
const { data: deliveries } = await stile.webhookEndpoints.listDeliveries("we_abc123");Events
Every webhook delivery carries an event, and the events API is the durable record behind it. Treat webhook delivery as the source of truth — poll events only on cold start or for reconciliation. list filters by limit, starting_after, and type.
// Retrieve a single event
const event = await stile.events.retrieve("evt_abc123");
// List events, optionally filtered by type
const { data } = await stile.events.list({
limit: 20,
type: "verification_session.verified",
});Compliance
Check product-level compliance rules for one or more products in a jurisdiction. Returns per-product details and a most-restrictive merged summary. Useful for checking prohibited products or inspecting compliance details before creating a session.
// Check what's allowed in the user's jurisdiction
const compliance = await stile.compliance.check({
use_cases: ["alcohol_delivery", "tobacco_nicotine"],
jurisdiction: "US-CA",
});
if (compliance.most_restrictive.any_prohibited) {
// Some products can't be sold here
console.log("Prohibited:", compliance.most_restrictive.prohibited_use_cases);
// Remove prohibited items from the cart or show an error
}
// Create a session — the workflow carries the use case, and the API
// resolves the age tier internally
const session = await stile.verificationSessions.create({
type: "age",
workflow_id: "wf_YOUR_WORKFLOW_ID",
client_reference_id: "order_456",
});If you're using the widget SDK client-side, the session's workflow resolves compliance automatically. The server-side flow above is for backends that need to inspect the rules — e.g. to drop prohibited items from a cart before starting verification.
Verified persons
Check if a user has been previously verified across any site in the Stile network. Call this from your backend before loading the SDK to skip verification for returning users — see the Returning Users guide.
const result = await stile.verifiedPersons.lookup({
email: "user@example.com",
methods: ["document_capture"],
min_strength: "document_capture",
max_age: "30", // days
});
if (result.verified) {
// User already verified — grant access, skip SDK
console.log(result.verified_person_id, result.credentials);
} else {
// Create a session and load the SDK
}The response carries verified, verified_person_id, and a credentials array — each credential has method, strength, verified_at, and expires_at, with method and strength values in uppercase (e.g. "MDL"). At least one of email or phone is required.
Webhook signature verification
Verify incoming webhook signatures to prevent processing spoofed events. Both methods compute the HMAC timing-safe, reject signature timestamps older than 5 minutes, and return the parsed event. The SDK provides two methods:
fromRequest() — for any Web API framework
Works with Next.js, Hono, Cloudflare Workers, Bun, Deno, and any framework using the standard Request object:
export async function POST(req: Request) {
const event = await stile.webhooks.fromRequest(req, process.env.WEBHOOK_SECRET!);
if (event.type === "verification_session.verified") {
console.log("Session verified:", event.data.id);
}
return Response.json({ received: true });
}constructEvent() — low-level
For frameworks that don't use the standard Request object, pass the raw body and header directly:
const event = stile.webhooks.constructEvent(
rawBody, // string or Buffer
signatureHeader, // stile-signature header value
process.env.WEBHOOK_SECRET!,
);Always use raw body
JSON body parsers modify the request body before it reaches your handler, which invalidates the signature. Always pass the raw, unmodified request body.
Both methods throw a WebhookSignatureError when verification fails — respond with HTTP 400 when you catch one:
| Code | Meaning |
|---|---|
missing_header | The request has no Stile-Signature header. |
invalid_header | The header is present but malformed. |
timestamp_expired | The signature timestamp is older than the 5-minute tolerance. |
signature_mismatch | The computed HMAC doesn't match — wrong secret or modified body. |
Error handling
Every API error throws a StileError (or a subclass) carrying type, code, statusCode, param, and requestId:
import { StileError, StileAuthenticationError, StileRateLimitError } from "@stile/node";
try {
await stile.verificationSessions.create({ type: "identity", workflow_id: "wf_..." });
} catch (err) {
if (err instanceof StileAuthenticationError) {
// 401 — invalid or expired API key
} else if (err instanceof StileRateLimitError) {
// 429 — the SDK already retried (maxRetries); back off before trying again
} else if (err instanceof StileError) {
console.error(err.statusCode, err.type, err.code, err.message);
// Include err.requestId when contacting support
}
}| Class | Thrown when | Properties |
|---|---|---|
StileError | Base class for every API error response. | type, code, statusCode, param, requestId |
StileAuthenticationError | 401 — invalid or expired API key. | Extends StileError |
StileRateLimitError | 429 — rate limited after the SDK's automatic retries. | Extends StileError |
WebhookSignatureError | Webhook signature verification failed; respond with HTTP 400. | code — see codes above |
For a complete guide to error types, retry strategies, and idempotency, see Error Handling.
Next steps
Webhooks guide
Endpoint requirements, the retry schedule, and the full event catalog.
Error handling
Error codes, retry strategies, and idempotency across the API.
HTTP API reference
The endpoints behind every SDK method, with cURL, Python, and Go examples.
Widget SDK
Pair your backend session creation with the frontend verification widget.