Docs

Verified Person

Look up a reusable credential previously created by your organization and avoid unnecessary verification for returning users.

The Verified Person API answers one organization-scoped question: has this user already completed a verification with us that still meets our requirements? Look up a credential by email or phone from your backend, or let session creation perform the lookup and ownership-proof flow for you.

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 use direct HTTPS unless they are explicitly labeled private preview.

Look up a verified person

POST/v1/verified_person/lookup

Call this from your backend with your secret key. If an already-authenticated user has a credential at the required strength, you can reuse it without opening the widget.

Authenticate the user first

An email address or phone number is an identifier, not proof of ownership. Only branch directly on this lookup for a user your application has already authenticated. For user-supplied identifiers, use session-level reuse so Stile can require the workflow's OTP gate before accepting a credential.

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. The table is a compatibility taxonomy and includes reserved method values; it does not mean every row can be configured in a new workflow. For example, a document_capture credential (rank 5) satisfies a min_strength: "facial_age" lookup (rank 2), even though facial_age is not currently a runnable workflow method. 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 same-organization credential matches, the widget can skip 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 to 10 per 15-minute window. The eleventh attempt 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