stile
HTTP API

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/lookup from your backend and branch on the result yourself.
  • Session-level reuse — pass accept_existing (plus email or phone, and an optional min_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

POST/v1/verified_person/lookup

Call 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.

ParameterTypeDescription
emailstringUser email. At least one of email or phone is required.
phonestringUser phone number. At least one of email or phone is required.
methodsstring[]Require ALL of these methods to have been previously verified for a match.
min_strengthstringMinimum 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_agestringMaximum 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"
    }
  ]
}
FieldTypeDescription
objectstringAlways "verified_person_lookup".
verifiedbooleanWhether a matching credential exists that satisfies your filters.
verified_person_idstringOpaque identifier for the verified person (vp_...), or null if no match.
credentialsarrayMatching 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:

RankStrengthMethod
1self_attestationUser declaration
2facial_ageAI age estimation
3carrier_lookupMobile carrier verification
4open_bankingBank account verification
5document_capturePhysical ID scan + OCR
6mdlMobile Driver's License
7midMobile ID
8eudi_pidEU 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:

ParameterEffect
email / phoneIdentifies the user for the Verified Person lookup.
accept_existingWhen true, accepts an existing credential instead of requiring a new verification.
min_strengthMinimum credential strength to accept, using the ranking above.
max_ageMaximum age of the existing verification. Format "30d" — note the d suffix on sessions.
required_methodsRequire 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

On this page