Security Best Practices
Protect your integration with webhook verification, key management, and anti-fraud measures.
Webhooks are the source of truth
The client-side stile:verified event is a convenience signal for updating your UI. It can be spoofed by anyone with browser dev tools. Never grant access, unlock content, or fulfill orders based on client events alone.
Never trust the client
Always wait for the signed verification_session.verified webhook before granting access. The webhook is sent server-to-server over HTTPS and includes an HMAC-SHA256 signature that proves it came from Stile.
Webhook signature verification
Every webhook delivery includes a stile-signature header. Always verify it before processing the event. An unverified webhook endpoint is equivalent to no verification at all — anyone can POST a fake payload to your URL.
See the Webhook Signature Verification guide for the raw algorithm and copy-paste handlers in Node.js, Python, Go, Ruby, and PHP.
Key management
Stile uses two types of API keys:
| Key type | Prefix | Where to use |
|---|---|---|
| Secret key | vk_test_ / vk_live_ | Server-side only. Creates sessions, reads results, manages webhooks. |
| Publishable key | pk_test_ / pk_live_ | Safe for frontend code. Opens the verification widget. Cannot read results or manage resources. |
Never expose secret keys
Secret keys (vk_) must never appear in frontend code, client-side bundles, mobile apps, or public repositories. If a secret key is compromised, rotate it immediately in the dashboard.
Key rotation — You can rotate keys in the dashboard without downtime. Both the old and new key remain valid during a configurable grace period, giving you time to update your servers.
Rate limiting
API requests are rate-limited per key:
| Mode | Limit |
|---|---|
Test (vk_test_ / pk_test_) | 100 requests per minute |
Live (vk_live_ / pk_live_) | 1,000 requests per minute |
When you exceed the limit, the API returns 429 Too Many Requests with a Retry-After header indicating how many seconds to wait before retrying.
if (res.status === 429) {
const retryAfter = parseInt(res.headers.get("Retry-After") || "1", 10);
await new Promise((r) => setTimeout(r, retryAfter * 1000));
// retry the request
}See Error Handling for full retry-with-backoff examples in multiple languages.
PII handling
Stile is designed to minimize your PII exposure:
- Raw identity data is purged after verification — images, full names, dates of birth, and document numbers are not stored long-term.
- Only hashed anchors persist — email hashes, phone hashes, and document fingerprints are retained for deduplication and returning-user lookup.
- Verification outcomes persist — the age tier result (e.g.
over_21), credential method, and expiry are stored so returning users can skip re-verification.
This means you never need to store or handle raw identity documents yourself. The verification result tells you what you need to know (age tier, pass/fail) without exposing the underlying PII.
Anti-spoofing measures
Stile employs multiple layers to prevent identity fraud:
| Measure | What it catches |
|---|---|
| Barcode cross-reference | Printed photos of IDs — the barcode data must match the visual fields. |
| Head-turn liveness challenge | Flat images and photos held up to the camera — the user must turn their head to prove they are physically present. |
| Server-side frame analysis | SDK spoofing and injected video feeds — frames are analyzed server-side, not just on the client. |
| Face match | Stolen or borrowed IDs — the selfie must match the photo on the document. |
| Document expiry check | Expired documents are rejected. |
VPN and geolocation detection
If your use case involves physical delivery (e.g. alcohol, cannabis), pass the delivery_jurisdiction parameter when creating a session:
curl -X POST https://api.stile.dev/v1/verification_sessions \
-H "Authorization: Bearer vk_live_..." \
-H "Content-Type: application/json" \
-d '{
"type": "age",
"use_case": "alcohol_delivery",
"delivery_jurisdiction": "US-CA"
}'Stile compares the user's IP-based geolocation against the delivery jurisdiction. If there is a significant mismatch (e.g. the user's IP is in a different country), the session is flagged for review.
VPN detection is advisory
A geolocation mismatch does not automatically fail the session. It adds a flag to the verification result that your backend can use to apply additional checks or manual review.
Email OTP as ownership proof
When a returning user is looked up by email, they must complete an OTP challenge to prove they control that email address. This is an important distinction:
- OTP proves: "This person controls this email address."
- OTP does not prove: Age, identity, or any other verification claim.
The actual proof of age or identity comes from the underlying verification credential that was previously issued. The OTP simply gates access to that credential so a different person can't reuse it.
See Returning Users for the full reverification flow.
Related
- Webhook Signature Verification — raw algorithm and handlers in 5 languages
- Webhooks — setting up endpoints and handling events
- Error Handling — rate limit handling and retry logic