Docs

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
});
ParameterTypeDescription
baseUrlstringOrigin for API requests. Set it explicitly to https://api.stile.id.
maxRetriesnumber= 2Retries for network failures and 429 rate limits. Structured API errors, including 5xx responses, are returned immediately.
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.

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 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 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",
});
ParameterTypeDescription
limitnumber= 10Number of sessions per page, between 1 and 100.
starting_afterstringReturn older sessions that follow this ID in the newest-first list.
ending_beforestringReturn newer sessions that precede this ID in the newest-first list.
statusstringFilter by session status, e.g. "verified".
client_reference_idstringFilter 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 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 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:

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). 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.

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