Verified Person
Look up whether a user already holds a verified credential — on your site or across the Stile network — and skip re-verification for returning users.
The Verified Person API answers one question: has this user already been verified? A user who completed verification on your site — or on another site in the Stile network — holds a reusable credential. Look it up by email or phone before starting a new session, and skip the camera-and-ID flow entirely for returning users.
Two ways to use it:
- Manual lookup — call
POST /v1/verified_person/lookupfrom your backend and branch on the result yourself. - Session-level reuse — pass
accept_existing(plusemailorphone, and an optionalmin_strength) when creating a session and let Stile run the lookup and reuse flow for you. See Combine with session-level reuse below.
Examples show cURL, Python, Go, and Node.js. The Node.js examples assume an initialized @stile/node client.
Look up a verified person
/v1/verified_person/lookupCall this from your backend with your secret key before loading the widget. If the user is already verified at the required strength, you can skip the widget entirely.
| Parameter | Type | Description |
|---|---|---|
email | string | User email. At least one of email or phone is required. |
phone | string | User phone number. At least one of email or phone is required. |
methods | string[] | Require ALL of these methods to have been previously verified for a match. |
min_strength | string | Minimum credential strength to accept. E.g. "document_capture" to only accept doc capture or stronger (MDL, MID, EUDI PID). See the strength ranking below. |
max_age | string | Maximum age of the verification in days. E.g. "30" for the last 30 days. |
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"
}'import requests
res = requests.post(
"https://api.stile.id/v1/verified_person/lookup",
headers={"Authorization": "Bearer stile_sk_..."},
json={
"email": "user@example.com",
"min_strength": "document_capture",
},
)
result = res.json()
if result["verified"]:
print(result["verified_person_id"])body := strings.NewReader(`{"email":"user@example.com","min_strength":"document_capture"}`)
req, _ := http.NewRequest("POST", "https://api.stile.id/v1/verified_person/lookup", body)
req.Header.Set("Authorization", "Bearer stile_sk_...")
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)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: "..." }]
}Response
{
"object": "verified_person_lookup",
"verified": true,
"verified_person_id": "vp_abc123",
"credentials": [
{
"method": "MDL",
"strength": "MDL",
"verified_at": "2025-03-01T12:00:00.000Z",
"expires_at": "2026-03-01T12:00:00.000Z"
}
]
}| Field | Type | Description |
|---|---|---|
object | string | Always "verified_person_lookup". |
verified | boolean | Whether a matching credential exists that satisfies your filters. |
verified_person_id | string | Opaque identifier for the verified person (vp_...), or null if no match. |
credentials | array | Matching credentials: method, strength, verified_at, expires_at. |
Response values are UPPERCASE
Credential method and strength values in the response are UPPERCASE (e.g. "MDL"), while
request parameters like min_strength use lowercase (e.g. "document_capture"). Normalize
accordingly when comparing.
No match found
When no matching credential exists, verified is false and credentials is empty:
{
"object": "verified_person_lookup",
"verified": false,
"verified_person_id": null,
"credentials": []
}Treat this as a first-time user: create a verification session and run the normal flow.
Credential strength
When using min_strength, credentials are ranked from weakest to strongest:
| Rank | Strength | Method |
|---|---|---|
| 1 | self_attestation | User declaration |
| 2 | facial_age | AI age estimation |
| 3 | carrier_lookup | Mobile carrier verification |
| 4 | open_banking | Bank account verification |
| 5 | document_capture | Physical ID scan + OCR |
| 6 | mdl | Mobile Driver's License |
| 7 | mid | Mobile ID |
| 8 | eudi_pid | EU Digital Identity |
A credential at a given strength satisfies any request at that level or below. For example, a document_capture credential (rank 5) satisfies a min_strength: "facial_age" request (rank 2). The reverse is never true — a weaker credential cannot satisfy a stronger requirement, and the user must complete a step-up verification.
Methods not listed above map onto this ranking: selfie_match and selfie_liveness count at document_capture strength; student and parental_consent count at self_attestation strength.
Combine with session-level reuse
You don't have to orchestrate reuse yourself. Pass the reuse parameters on POST /v1/verification_sessions and Stile runs the lookup as part of the session:
| Parameter | Effect |
|---|---|
email / phone | Identifies the user for the Verified Person lookup. |
accept_existing | When true, accepts an existing credential instead of requiring a new verification. |
min_strength | Minimum credential strength to accept, using the ranking above. |
max_age | Maximum age of the existing verification. Format "30d" — note the d suffix on sessions. |
required_methods | Require ALL of these methods to have been previously verified before reusing. |
Two max_age formats
The lookup endpoint takes max_age as a number of days ("30"); session creation takes a
duration string ("30d"). Don't swap them.
When a returning user matches, the widget skips the camera flow. If the workflow requires it, the user first proves ownership of the email with a one-time code — the session's requires_email_otp field tells you when this gate applies. The OTP proves control of the email address only; the age or identity proof always comes from the underlying credential.
Repeated accept_existing attempts for the same email are rate-limited: 5 attempts per 15-minute window, after which session creation returns 429 accept_existing_rate_limited. See Error Handling for the full code list.
For the end-to-end returning-user model — VP tokens, email/phone lookup with OTP, and full verification as the fallback — read the Returning Users guide.
Next steps
Returning Users
The three-tier reuse flow: VP tokens, email lookup + OTP, and full verification.
Verification Sessions
Create sessions with accept_existing, min_strength, and max_age.
Trust Reuse
Cross-operator reuse, consent, and the trust_reuse_grant webhook events.
Node.js SDK
Typed verifiedPersons.lookup() and the rest of the API surface.
Compliance
Look up regulatory rules by product type and jurisdiction — required age tiers, prohibited products, allowed verification methods, and most-restrictive resolution for mixed carts.
Risk
Score the fraud risk of an IP, email, phone, or device — standalone, before or alongside a verification session.