stile
Guides

Testing & Sandbox

Build against sandbox mode, simulate verified sessions with skip_verification, test webhooks through a tunnel, and run the going-live checklist.

Sandbox mode lets you exercise your entire integration — session creation, webhooks, VP tokens, returning-user flows — without verifying a real user. This page covers sandbox mode, the skip_verification flag, local webhook testing, CI suites, and the checklist for moving an integration onto a live organization.

Sandbox mode

Stile runs a single environment: every organization is live by default. For building and testing, enable sandbox mode on a dedicated testing organization — an org-level setting you turn on through your dashboard org settings or by contacting support. The same stile_sk_… / stile_pk_… keys work everywhere; what changes is the organization they belong to.

Sandbox orgLive org
VerificationsReal or simulated — skip_verification completes sessions instantlyReal verifications only
Webhook endpointOptionalRequired — at least one active endpoint before sessions can be created
Webhook URL schemeHTTPS or HTTPHTTPS only
BillingFree, capped at 500 verifications/monthBilled beyond the free tier (100 verifications/month by default)

Both follow the same per-key rate limit — 1,000 requests/min for secret keys, 100/min for publishable keys. Use a sandbox org for development, CI pipelines, and staging environments. Point your integration at a live org when you're ready to verify real users — and follow the going-live checklist below when you do.

Sandbox quota

Sandbox mode is capped at 500 verifications per calendar month per organization. When you hit the cap, session creation returns 402 with code test_quota_exceeded; the quota resets on the 1st of the next month. Sandbox verifications are unbilled, so this cap keeps a runaway CI or load test in check — if you need more volume, run those against a live org.

Live orgs require webhooks

Creating a verification session in a live (non-sandbox) organization will fail with webhook_required if the org has no active webhook endpoints. Register one in the dashboard or via the Webhook Endpoints API before going live. Sandbox orgs are exempt.

The skip_verification flag

In a sandbox org, pass skip_verification: true when creating a session to skip the entire camera/ID scanning flow. The session transitions straight to verified, fires webhooks, and issues a VP token — all without user interaction.

This is useful for:

  • Testing webhook handlers end-to-end
  • Validating VP token flows and returning-user logic
  • Exercising checkout integrations in CI
  • Verifying email binding and OTP behavior
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",
    "email": "test@example.com",
    "skip_verification": true
  }'
import Stile from "@stile/node";

const stile = new Stile(process.env.STILE_API_KEY!); // stile_sk_...

const session = await stile.verificationSessions.create({
  type: "age",
  workflow_id: "wf_YOUR_WORKFLOW_ID",
  email: "test@example.com",
  skip_verification: true,
});

The response is identical to a real verified session — same shape, same webhook events, same VP token issuance. Whatever you build against it works unchanged against real verifications.

Sandbox only

The skip_verification flag is rejected in a live org — creating a session with skip_verification: true against a non-sandbox organization returns a 400 with code parameter_invalid. This is deliberate: there is no way to mint a verified session in a live org without a real verification.

Local webhook testing

During development your server runs on localhost, which isn't reachable by Stile's webhook infrastructure. Use a tunnel to expose it.

# 1. Start your local server
npm run dev  # listening on port 3000

# 2. In a separate terminal, start ngrok
ngrok http 3000

# 3. Copy the forwarding URL (e.g. https://abc123.ngrok-free.app)
# 4. Register it as a webhook endpoint:
curl -X POST https://api.stile.id/v1/webhook_endpoints \
  -H "Authorization: Bearer stile_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://abc123.ngrok-free.app/api/webhooks",
    "enabled_events": ["verification_session.verified"]
  }'
# 1. Start your local server
npm run dev  # listening on port 3000

# 2. Start the tunnel
cloudflared tunnel --url http://localhost:3000

# 3. Use the generated *.trycloudflare.com URL as your webhook endpoint

Once the endpoint is registered, create a session with skip_verification: true to trigger a real, signed verification_session.verified delivery against your local handler — including the Stile-Signature header, so you can test signature verification exactly as it runs in production.

Dashboard shortcut

You can also register webhook endpoints directly in the dashboard instead of using the API.

E2E testing in CI

skip_verification: true is designed for end-to-end suites — it lets you exercise the full integration without camera interactions:

  • Session creation and status transitions
  • Webhook delivery and signature verification against your real handler
  • VP token issuance and returning-user reuse
  • Email binding and OTP flows

A typical Playwright/Cypress flow:

  1. Create a session with skip_verification: true via your backend.
  2. Assert your webhook handler marked the order verified.
  3. Assert your UI unlocked.

Keep an eye on the 500/month sandbox quota if your suite runs on every commit.

Sandbox data hygiene

A few best practices to keep your sandbox organization clean:

  • Use unique emails per test run — append a timestamp or UUID (e.g. test+1712345678@example.com) to avoid collisions with previous runs.
  • Sandbox sessions expire after 24 hours — unverified sandbox sessions are automatically cleaned up, so you don't need to delete them manually.
  • Isolate testing in a sandbox org — keep your CI and manual testing in a dedicated sandbox organization, separate from the live org that serves real users.

Going live checklist

When your integration works end-to-end in a sandbox org, work through these steps before pointing it at a live organization.

Point at a live org

Use the stile_sk_… key for your live organization on the server and its stile_pk_… key in the widget. Keys are managed at dashboard.stile.id/api-keys — secret keys are shown once at creation, so store them in your environment, never in code. A live org is billed beyond the free tier (100 verifications/month by default).

Register a webhook endpoint (required)

A live org requires at least one active webhook endpoint before you can create sessions — without one, POST /v1/verification_sessions returns 400 with code webhook_required. Register a public HTTPS URL in the dashboard or via the Webhook Endpoints API, and save the signing secret. See the Webhooks guide for setup and retry behavior.

Verify webhook signatures

Treat the signed webhook — not client-side events — as the source of truth for granting access. Your handler must verify the Stile-Signature header against the raw request body before processing any event. Use stile.webhooks.fromRequest() from @stile/node, or grab a copy-paste handler for your stack from Webhook Verification.

Remove skip_verification

Audit your session-creation code paths for skip_verification: true. In a live org the flag is rejected with 400 parameter_invalid, so any leftover sandbox shortcut becomes a hard failure on your first production session. Gate it behind an environment check if your test suite still needs it.

Handle 402 and 429 responses

Make sure your error handling covers the billing and rate-limit cases: 402 means test_quota_exceeded in a sandbox org or billing_suspended in a live org — surface these rather than retrying. 429 (rate_limit_exceeded) is retryable: respect the Retry-After header and back off exponentially. The rate limit is the same everywhere (1,000 requests/min for secret keys, 100/min for publishable keys), so a load pattern that throttles in staging will throttle the same way in production — but the handling should be in place either way. See Error Handling for the full retry policy.

Next steps

On this page