Node.js SDK (private preview)
Private-preview reference for the typed Node.js and TypeScript client; public integrations should use the HTTP API.
Private preview, not public npm
@stile/node is not currently published for public installation. Do not run npm install @stile/node or generate an SDK import unless Stile has explicitly granted your organization
package access. Public integrations should follow the backend-agnostic
Quickstart and call the HTTP API directly.
For approved preview users, @stile/node is a server-side convenience client for the Stile API. Every method maps 1:1 to an HTTP endpoint, with typed parameters and responses, retries for network failures and 429 responses, and built-in webhook signature verification.
You can also use the HTTP API directly
The private-preview 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.
Access and installation
There is no public installation command yet. Approved preview users should use the registry, package version, and authentication instructions supplied with their invitation. Everyone else should use direct HTTPS; the public API contract is identical.
Server-only
The private-preview 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 a stile_sk_ secret key everywhere. There is one API environment, so current keys have no test/live segment. Each key belongs to one organization, so create separate keys for your sandbox and live organizations. For building and testing, ask Stile support to 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 network errors and 429 responses
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 | Retries for network failures and 429 rate limits. Structured API errors, including 5xx responses, are returned immediately. |
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.
Primary widget paths are mdl and document_capture, plus nfc_passport in supported native embeds. selfie_liveness and selfie_match are active biometric layers; self_attestation is an additional declaration step and should not be the only hosted-widget path. ip_analysis, device_risk, and geolocation are passive supplementary checks. Reserved values such as facial_age, mid, eudi_pid, carrier_lookup, open_banking, parental_consent, student, and standalone apple_wallet are rejected when a workflow is published.
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 by default, workflow-configurable
// 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 400 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",
risk_tier: "review",
});| Parameter | Type | Description |
|---|---|---|
limit | number= 10 | Number of sessions per page, between 1 and 100. |
starting_after | string | Return older sessions that follow this ID in the newest-first list. |
ending_before | string | Return newer sessions that precede this ID in the newest-first list. |
status | string | Filter by session status, e.g. "verified". |
client_reference_id | string | Filter by an exact client reference ID. |
risk_tier | "clear" | "review" | "block" | "bot" | Filter by the session's computed risk tier. |
starting_after and ending_before are mutually exclusive.
Webhook endpoints
Manage webhook endpoints in code, or from the dashboard. enabled_events accepts specific event types or ["*"] for everything.
The SDK's EventType union covers the complete event catalog. Verification-session events narrow event.data.object to VerificationSessionEventSnapshot, the compact webhook shape—not the full VerificationSession returned by the Sessions API. Review and trust-reuse events expose their resource as Record<string, unknown> until dedicated public resource types are added.
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 one mutable delivery record per event-endpoint pair and paginates with limit (default 10), starting_after, and ending_before. Retries update the record's attempt counter and latest response rather than creating history rows. The cursors are mutually exclusive. Filter by status (delivered, failed, or pending) and event_type when reconciling a specific failure:
// List
const { data } = await stile.webhookEndpoints.list();
// List delivery records
const { data: deliveries } = await stile.webhookEndpoints.listDeliveries("we_abc123", {
status: "failed",
});
// Inspect and retry one failed attempt
const delivery = await stile.webhookEndpoints.retrieveDelivery(
"we_abc123",
"cm5x7k9h20000qwerty123456",
);
await stile.webhookEndpoints.retryDelivery("we_abc123", delivery.id);
// Rotate a compromised secret (the new secret is returned once)
const endpoint = await stile.webhookEndpoints.rotateSecret("we_abc123");
console.log(endpoint.secret);
// Send a synthetic event while testing your handler
await stile.webhookEndpoints.sendTest("we_abc123", {
event_type: "verification_session.verified",
});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 supports limit, starting_after, ending_before, type, created_after, created_before, and session_id. The two cursor parameters are mutually exclusive.
// 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.
Risk
Score fraud signals before or alongside a verification session. At least one of ip, email, phone, or device_id is required; identifiers are hashed server-side before persistence.
const assessment = await stile.risk.score({
ip: "203.0.113.10",
email: "user@example.com",
claimed_country: "US-CA",
});
if (assessment.recommendation === "block") {
// Stop or route the transaction to manual review.
}
const sameAssessment = await stile.risk.retrieve(assessment.id);Verified persons
Check whether your organization already holds a reusable credential for an authenticated user. The lookup is organization-scoped; cross-operator reuse uses the separate, consent-based Trust Reuse flow.
const result = await stile.verifiedPersons.lookup({
email: "user@example.com",
methods: ["document_capture"],
min_strength: "document_capture",
max_age: "30", // days
});
if (result.verified) {
// This authenticated user already has a qualifying credential.
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) {
let event;
try {
event = await stile.webhooks.fromRequest(req, process.env.WEBHOOK_SECRET!);
} catch {
return new Response("Invalid signature", { status: 400 });
}
if (event.type === "verification_session.verified") {
console.log("Session verified:", event.data.object.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). The SDK always supplies type, code, statusCode, and message; param and requestId are optional because some API routes do not return them:
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 exhausted maxRetries; wait before trying again
} else if (err instanceof StileError) {
console.error(err.statusCode, err.type, err.code, err.message);
// Include err.requestId when contacting support
}
}The SDK does not automatically retry structured 5xx responses. If the operation is safe to retry, add application-level backoff and use an idempotency key for writes. The SDK's automatic 429 retry uses jittered exponential backoff; direct HTTP clients should honor Retry-After explicitly.
| 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.