Check existing applicants
Reconciling a list of your own users against the verifications you already hold
You have a list of email addresses from your own database and you want to know which of those people have verified with you. One call answers up to a thousand of them.
curl https://api.trustwix.com/v1/verifications/lookup \
-H "Authorization: Bearer TWK_..." \
-H "Content-Type: application/json" \
-d '{
"emails": ["ana@example.com", "bo@example.com", "cai@example.com"]
}'{
"object": "list",
"mode": "live",
"data": [
{
"object": "verification_lookup",
"email": "ana@example.com",
"found": true,
"attempts": 2,
"ever_approved": true,
"first_attempt_at": "2026-03-01T10:00:00.000Z",
"latest": {
"id": "vs_2f8a1c9b4e7d",
"status": "rejected",
"verdict": "reject",
"score": 0.41,
"reason_codes": ["FACE_MISMATCH"],
"flow": "flw_id_and_liveness",
"mode": "live",
"created_at": "2026-07-01T10:00:00.000Z",
"updated_at": "2026-07-01T10:04:12.000Z",
"expires_at": null
}
},
{
"object": "verification_lookup",
"email": "bo@example.com",
"found": false,
"attempts": 0,
"ever_approved": false,
"first_attempt_at": null,
"latest": null
}
]
}You get exactly one result per entry, in the order you sent them, echoed back verbatim. Nothing is
collapsed or reordered, so data[i] always answers emails[i] and you can map the response onto
your own list by index without normalising anything first. Send the same address twice and you get
two identical rows.
Matching ignores case and surrounding whitespace, so Ana@Example.COM and ana@example.com are
the same person and will return the same verification.
Read ever_approved, not just latest.status
latest is the most recent attempt. ever_approved looks across every attempt this address has.
Ana above is the case that catches people out. She passed in March, tried again in July after
changing her name, and that second attempt was rejected. Her latest.status is rejected and her
ever_approved is true. Whether she should keep her access depends on your own policy, but you
have to see both numbers to make that decision, and a reconciliation script that reads only
latest.status will lock out every customer who ever re-verified and failed.
attempts gives you the rest of the picture. A high count against ever_approved: false is somebody
struggling with the flow, and it is worth a look at whether the flow itself is the problem.
What found: false actually tells you
It tells you we hold no verification carrying that address in this environment, right now. It does not tell you the person never verified. Three quite different situations produce the same answer.
They never started one. The straightforward case, and usually the one you are looking for.
We were never told their address. This is the one to check first if a reconciliation run comes
back almost entirely empty. An address reaches a verification in exactly two ways: you send
applicant.email when you create it, or the applicant completes an email step in the hosted flow. If
you do neither, we hold sessions that ran perfectly and completed normally, with no address on them,
and every lookup you run will miss all of them.
curl https://api.trustwix.com/v1/verifications \
-H "Authorization: Bearer TWK_..." \
-H "Content-Type: application/json" \
-d '{
"flow": "flw_id_and_liveness",
"applicant": { "reference_id": "your-user-123", "email": "ana@example.com" }
}'Their retention window has elapsed. When a verification passes out of retention we destroy the stored identity and the search fingerprint in the same statement. A record we have promised to forget cannot still answer "yes, that person verified with us", so it stops being findable at the same moment it stops existing. This is the retention promise working rather than a gap to route around.
Do not treat `found: false` as permission to delete
If your own records say someone verified and a lookup says found: false, the most likely
explanations are the second and third above, not that your records are wrong. Reconcile in the
direction of adding what you are missing, never of deleting what you already hold.
Sandbox and live are separate
A lookup answers within the environment of the key that made it. A live key never sees sandbox
sessions, and the reverse. The mode on the envelope tells you which environment answered, so every
found: false in a response means "not in this one".
If you tested with real colleagues' addresses in sandbox and then look them up with a live key, they come back as not found. That is correct.
Checking one address
The single-address form is a filter on the list endpoint, and it returns the full verification resource rather than a summary.
curl "https://api.trustwix.com/v1/verifications?email=ana%40example.com" \
-H "Authorization: Bearer TWK_..."Same exact matching, newest first, and the usual cursor pagination if that address has more verifications than fit on a page. Prefer the batch endpoint for more than a handful: it keeps the addresses out of the URL and costs you far less of your rate limit.
A request body is not automatically private
Keeping addresses out of the URL keeps them out of proxy access logs, browser history and
Referer headers. It does not keep them out of anything that logs request bodies. Redact these
payloads in your own application logs, tracing and error reporting.
Limits, and what a run of 3,000 looks like
One call carries up to 1,000 addresses. Larger lists are more than one call.
The rate limit charges by size rather than by call, at one token per 50 addresses out of your budget of 100 per minute. A full 1,000-address call costs 20 tokens, so a 3,000-address reconciliation is three calls costing 60 and clears comfortably inside a single minute.
Permissions
verification:read, which is on every key by default. The response carries no applicant identity: no
name, no date of birth, no document number, no images. It answers only the question you already knew
the answer to, since you supplied the address. To read who somebody is, use
applicant data on a single verification, which needs the
applicant:read scope.
When the answer is unavailable
If a lookup cannot be answered you get a 503 with the code LOOKUP_UNAVAILABLE, never an empty
result. The distinction is deliberate and it matters: an empty result and an unanswered question look
identical to a script, and quietly treating one as the other means re-onboarding thousands of
customers who were already approved. Retry with backoff, and treat the addresses in that call as
unknown rather than as unverified.