Returning User Verification
Verify once, reuse many times — VP tokens, credential lookup with OTP, and the strength, age-tier, and expiry rules that decide when a user must re-verify.
Most verification systems force users through the full camera + ID flow on every visit. Stile verifies once, then issues a reusable signed proof — a VP token — so returning users pass instantly with the same security guarantees. This guide covers the three-tier reverification model, how credentials travel across sites, and the rules (strength, age tier, expiry) that decide when a user must verify again.
The three-tier model
When a user arrives, the system checks three tiers in order. The first tier that succeeds is used — no unnecessary steps.
| Tier | User experience | When it's used |
|---|---|---|
| Tier 1 | Instant — no user interaction | User has a valid VP token in localStorage for this origin. |
| Tier 2 | Email + OTP code | User has verified before (on this or another site) but doesn't have a local token. |
| Tier 3 | Full camera + ID flow | First-time user, or existing credentials are expired or insufficient. |
Tier 1: VP tokens
A VP token is a signed JWT stored in the browser's localStorage, scoped to the current origin. It is issued automatically after a successful verification and contains:
- A verified person ID (opaque identifier)
- The credential method and age tier
- An expiry timestamp
- A signature from Stile's servers
The lifecycle:
- User completes a full verification (Tier 3) or the OTP reuse flow (Tier 2).
- Stile issues a VP token and the widget stores it in
localStorage. - On the next visit, the widget finds the token and sends it to Stile for validation.
- If valid, the user passes instantly — no camera, no OTP, no friction.
If you orchestrate this yourself, pass the token as vp_token when creating a session. A malformed, expired, or revoked token returns 400 vp_token_invalid — treat that as "no token" and fall through to Tier 2.
Server-side validation is required
The VP token is a convenience for fast client-side checks, but your server must always validate the token by checking the actual credentials in the database — not just the JWT claims. A tampered token will fail server-side validation.
Tier 2: Cross-site credential reuse
localStorage is scoped per origin: a VP token from shop-a.com is not accessible on shop-b.com. Cross-site reuse instead works through email/phone lookup + OTP:
User arrives without a token
A user visits shop-b.com for the first time. There's no VP token in localStorage for this origin, so Tier 1 fails silently.
User enters their email
The widget collects the address (or your backend passes email on session creation).
Stile finds an existing credential
The credential was created when the user verified on shop-a.com and is linked to that email.
User completes an OTP challenge
A one-time code proves they own the email address — and nothing more (see OTP is not proof of age).
A new VP token is issued for shop-b.com
No camera, no ID scan. The next visit passes at Tier 1.
Network deduplication required
Cross-site credential reuse requires networkDedup to be enabled for the receiving organization.
Contact support or enable it in the dashboard.
Check for an existing credential server-side
Before loading the widget at all, your backend can ask whether the user already holds a credential that meets your bar:
curl -X POST https://api.stile.id/v1/verified_person/lookup \
-H "Authorization: Bearer stile_sk_..." \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"min_strength": "document_capture"
}'const result = await stile.verifiedPersons.lookup({
email: "user@example.com",
min_strength: "document_capture",
max_age: "30",
});
if (result.verified) {
console.log(result.verified_person_id);
console.log(result.credentials);
// [{ method: "MDL", strength: "MDL", verified_at: "...", expires_at: "..." }]
}See the Verified Person API for the full parameter and response reference.
Reuse at session creation
You don't have to orchestrate the lookup yourself. Pass the reuse parameters on POST /v1/verification_sessions and Stile runs the flow as part of the session:
| Parameter | Effect |
|---|---|
email / phone | Identifies the user for the credential lookup. |
accept_existing | When true, accepts an existing credential instead of requiring a new verification. |
min_strength | Minimum credential strength to accept (see the ranking). |
max_age | Maximum age of the existing verification, as a duration string — e.g. "30d". |
required_methods | Require ALL of these methods to have been previously verified before reusing. |
If the workflow requires an ownership proof, the session's requires_email_otp field is true and the user must complete the OTP gate before reuse — otherwise requests fail with 400 email_not_verified or 400 otp_not_proven. When no reusable credential matches your filters, session creation returns 400 no_matching_vp. Repeated accept_existing attempts for the same email are rate-limited to 5 per 15-minute window (429 accept_existing_rate_limited).
Email binding
VP tokens are bound to the email address used during verification. This prevents token sharing:
- If user A verifies with
alice@example.com, their VP token is bound to that email. - If user B tries to use the same device, a different email triggers re-verification.
- Changing the email on an existing session forces a full Tier 3 verification.
Credential strength ranking
Not all verification methods carry the same weight. Stile ranks credential methods by strength:
| Rank | Method | Description |
|---|---|---|
| 1 | self_attestation | User self-declares their age (weakest). |
| 2 | facial_age | AI-based age estimation from a selfie. |
| 3 | carrier_lookup | Mobile carrier age check. |
| 4 | open_banking | Age derived from bank account data. |
| 5 | document_capture | Government-issued ID scan + selfie match. |
| 6 | mdl | Mobile driver's license (ISO 18013-5). |
| 7 | mid | Mobile identity document. |
| 8 | eudi_pid | EU Digital Identity wallet credential (strongest). |
Methods not listed map onto this ranking: selfie_match and selfie_liveness count at document_capture strength; student and parental_consent count at self_attestation strength.
A stronger credential always satisfies a weaker requirement: a user verified with document_capture (rank 5) satisfies a request for self_attestation (rank 1) without re-verifying. The reverse is never true — a self_attestation credential cannot satisfy a document_capture requirement, and the user must complete a step-up verification.
Age tier compatibility
Age tiers follow a strict hierarchy:
min_age_21 --> satisfies min_age_18, min_age_16, and min_age_13
min_age_18 --> satisfies min_age_16 and min_age_13
min_age_16 --> satisfies min_age_13
min_age_13 --> satisfies min_age_13 onlyA credential proving min_age_21 automatically satisfies any lower requirement. A weaker tier (e.g. min_age_16) can never satisfy a stronger one (e.g. min_age_21) — that, too, routes the user to a step-up verification.
Credential expiry
Credentials expire based on jurisdiction compliance rules. The default expiry is 365 days from the date of verification. When a credential expires:
- Tier 1 (VP token) stops working — the token is rejected during validation.
- Tier 2 (email lookup) finds the credential but sees it's expired.
- The user is routed to Tier 3 for a full re-verification.
Jurisdiction-specific expiry
Some jurisdictions require shorter credential lifetimes. Stile automatically applies the correct expiry based on the jurisdiction where the verification was performed.
You can also enforce a stricter recency bar than the credential's own expiry — pass max_age at lookup or session creation to refuse credentials older than your policy allows.
Step-up verification
A valid, non-expired credential can still be insufficient if the new request requires a stronger method or a higher age tier.
Example: A user verified with self_attestation (rank 1) visits a site that requires document_capture (rank 5). Their existing credential doesn't meet the requirement, so they complete a new verification with the stronger method.
After the step-up, the new (stronger) credential replaces the old one. The user now holds a document_capture credential that satisfies both document_capture and self_attestation requirements going forward.
OTP is not proof of age
A common misconception: completing an OTP challenge does not prove anything about the user's age or identity.
- OTP proves: "This person controls this email address."
- OTP enables: Reuse of an existing, previously-verified credential.
- OTP does not prove: Age, identity, or anything else.
The actual proof always comes from the underlying verification credential (ID scan, facial age estimation, etc.). The OTP is a gate that ensures only the rightful owner of the email can access the credential linked to it.
Next steps
Verified Person API
Full reference for the lookup endpoint, filters, and response shape.
Verification Sessions
Create sessions with accept_existing, min_strength, max_age, and vp_token.
Trust Reuse
Cross-operator reuse with explicit user consent and grant webhooks.
Testing & Sandbox
skip_verification produces a real VP token — test returning-user flows end to end.