stile
SDKs

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/node
pnpm add @stile/node
yarn add @stile/node

Server-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
});
ParameterTypeDescription
baseUrlstringOrigin for API requests. Set it explicitly to https://api.stile.id.
maxRetriesnumber= 2Automatic retries for failed requests. The SDK retries 5xx responses, network errors, and 429 rate limits.
timeoutnumber= 30000Per-request timeout in milliseconds (30 seconds).

Resources

Each resource on the client maps to one section of the HTTP API reference:

ResourceMethodsReference
stile.verificationSessionscreate, retrieve, cancel, listVerification Sessions
stile.webhookEndpointscreate, retrieve, update, del (alias delete), list, listDeliveriesWebhook Endpoints
stile.eventsretrieve, listEvents
stile.compliancecheckCompliance
stile.verifiedPersonslookupVerified Person
stile.webhooksfromRequest, constructEventWebhook 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 jurisdiction

For 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",
});
ParameterTypeDescription
limitnumber= 10Number of sessions per page, between 1 and 100.
starting_afterstringCursor — return results after this session ID.
ending_beforestringCursor — return results before this session ID.
statusstringFilter by session status, e.g. "verified".
expandstring[]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 signatures

Retrieve, 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:

CodeMeaning
missing_headerThe request has no Stile-Signature header.
invalid_headerThe header is present but malformed.
timestamp_expiredThe signature timestamp is older than the 5-minute tolerance.
signature_mismatchThe 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
  }
}
ClassThrown whenProperties
StileErrorBase class for every API error response.type, code, statusCode, param, requestId
StileAuthenticationError401 — invalid or expired API key.Extends StileError
StileRateLimitError429 — rate limited after the SDK's automatic retries.Extends StileError
WebhookSignatureErrorWebhook 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

On this page