stile
Guides

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.

The three-tier returning-user model: a valid VP token passes instantly; otherwise an email or phone lookup with an OTP reuses an existing credential; otherwise the user completes a full verification

TierUser experienceWhen it's used
Tier 1Instant — no user interactionUser has a valid VP token in localStorage for this origin.
Tier 2Email + OTP codeUser has verified before (on this or another site) but doesn't have a local token.
Tier 3Full camera + ID flowFirst-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:

  1. User completes a full verification (Tier 3) or the OTP reuse flow (Tier 2).
  2. Stile issues a VP token and the widget stores it in localStorage.
  3. On the next visit, the widget finds the token and sends it to Stile for validation.
  4. 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:

ParameterEffect
email / phoneIdentifies the user for the credential lookup.
accept_existingWhen true, accepts an existing credential instead of requiring a new verification.
min_strengthMinimum credential strength to accept (see the ranking).
max_ageMaximum age of the existing verification, as a duration string — e.g. "30d".
required_methodsRequire 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:

RankMethodDescription
1self_attestationUser self-declares their age (weakest).
2facial_ageAI-based age estimation from a selfie.
3carrier_lookupMobile carrier age check.
4open_bankingAge derived from bank account data.
5document_captureGovernment-issued ID scan + selfie match.
6mdlMobile driver's license (ISO 18013-5).
7midMobile identity document.
8eudi_pidEU 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 only

A 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

On this page