Applicant Lookup API
Once a user has verified, these two endpoints let you look up what you know about them — their verification history, the data extracted from their documents, and their current status.
- Resolve Facemap ID — turn your own user identifier into a facemap ID.
- Get Applicant Data — everything recorded against one facemap.
Base URL
https://api2.cycurid.com
Authentication
Both endpoints use Basic Authentication with your merchant credentials:
- Username:
{{merchantApiKey}} - Password:
{{merchantSecret}}
Authorization: Basic base64(merchantApiKey:merchantSecret)
Every lookup is scoped to your own merchant account — you can only read applicants that verified against your flows.
Facemaps, Users, and Registrations
Three ideas underpin both responses, and mixing them up is the most common source of confusion:
| Term | What it is |
|---|---|
| Facemap ID | The person. A biometric identifier that stays stable across every verification that face performs. |
| externalUserId | Your label for that person, within one group. Not a person — the same person carries a different one in each group they verify into. |
| Registration | One person in one group: an externalUserId + groupingId pair, with its own status, its own attempts, and its own extracted data. |
So one facemap can hold several registrations. A user who verifies into two groups produces two registrations of the same face, each with its own status. Within a single group, one face can only register once — a second attempt to register the same face under the same group is rejected as a duplicate.
This is why neither response puts anything person-shaped at the top level: a
single externalUserId or set of extracted fields up there could only describe
one of the registrations, and you'd have no way to tell which.
Resolve Facemap ID
GET /v2/reports/applicants/byExternalUserId/:externalUserId
Turns one of your user identifiers into the facemap ID that identifies the person behind it. Use this first when you only have your own user ID, then pass the result to Get Applicant Data.
Path Parameters
| Field | Type | Required | Description |
|---|---|---|---|
externalUserId | string | Yes | The identifier you sent as externalUserId on /v2/idv/verify. URL-encode it if it contains reserved characters. Maximum 500 characters. |
The identifier may also be passed as a query parameter instead of a path segment, which avoids encoding issues:
GET /v2/reports/applicants/byExternalUserId?externalUserId=user-12345
Example
curl 'https://api2.cycurid.com/v2/reports/applicants/byExternalUserId/user-12345' \
-u '{{merchantApiKey}}:{{merchantSecret}}'
Response
{
"externalUserId": "user-12345",
"facemapId": "6a7e334fb881b8010f7d1ffc",
"applicants": [
{
"facemapId": "6a7e334fb881b8010f7d1ffc",
"groupingId": "group_e5f6g7h8",
"status": "approved",
"registered": true,
"createdAt": "2026-08-13 14:13:48",
"lastActive": "2026-08-14 09:22:55"
}
]
}
| Field | Description |
|---|---|
facemapId | The most recently active facemap — the one to pass to Get Applicant Data. |
applicants[] | One entry per (facemap, group) pair, most recently active first. |
applicants[].registered | false means the facemap appears only in the verification logs and has no entry in the user mapping yet. status is null in that case. |
applicants[].status | See Status Values. |
An externalUserId is pinned to a single facemap within your merchant account,
so in a healthy dataset applicants[] contains exactly one distinct
facemapId. Seeing more than one is worth investigating — it usually means the
user was deleted and re-registered against a different face.
Get Applicant Data
GET /v2/reports/applicants/:facemapId
Returns everything recorded against one facemap, broken down by registration.
Path Parameters
| Field | Type | Required | Description |
|---|---|---|---|
facemapId | string | Yes | The facemap ID, as returned by Resolve Facemap ID. |
Example
curl 'https://api2.cycurid.com/v2/reports/applicants/6a7e334fb881b8010f7d1ffc' \
-u '{{merchantApiKey}}:{{merchantSecret}}'
Response
{
"facemapId": "6a7e334fb881b8010f7d1ffc",
"createdAt": "2026-08-13 14:12:47",
"merchantId": "1",
"registrationCnt": 2,
"registrations": [
{
"externalUserId": "user-12345",
"groupingId": "group_e5f6g7h8",
"status": "approved",
"createdAt": "2026-08-13 14:13:48",
"lastActive": "2026-08-14 09:22:55",
"flowId": "flow_a1b2c3d4",
"flowName": "Standard Onboarding",
"requiredDocuments": ["PASSPORT", "DRIVERS", "ID_CARD"],
"info": {
"firstName": "CHRISTIAN",
"lastName": "SMITH",
"dob": "07/16/1989",
"phone": "+15145551234",
"email": "christian.smith@example.com",
"country": "CAN",
"nationality": "CAN",
"addresses": [
{
"street": "200 Rue Sainte-Catherine Ouest",
"town": "Montreal",
"state": "QC",
"postCode": "H2X 1L1",
"country": "CAN"
}
],
"idDocs": [
{
"idDocType": "DRIVERS",
"country": "CAN",
"number": "S1234-56789-01234",
"validUntil": "2028-09-04",
"firstName": "CHRISTIAN",
"lastName": "SMITH",
"dob": "07/16/1989"
}
]
},
"verifications": [
{
"logId": 571,
"createdAt": "2026-08-14 09:22:55",
"status": "approved",
"success": true,
"reasons": [],
"flowId": "flow_a1b2c3d4",
"flowName": "Standard Onboarding",
"documentTypes": "DRIVERS",
"sessionId": "4a98055c-4398-42fe-ad37-578dc1310b22"
}
]
},
{
"externalUserId": "user-67890",
"groupingId": "group_x1y2z3w4",
"status": "pending",
"createdAt": "2026-08-13 14:12:47",
"lastActive": "2026-08-13 14:12:47",
"flowId": "flow_a1b2c3d4",
"flowName": "Standard Onboarding",
"requiredDocuments": ["PASSPORT"],
"info": { "...": "..." },
"verifications": [{ "...": "..." }]
}
]
}
Top-Level Fields
| Field | Description |
|---|---|
facemapId | The facemap you asked for. |
createdAt | When this face was first seen, across all registrations. |
merchantId | Your merchant account ID. |
registrationCnt | Number of entries in registrations. |
registrations[] | One entry per registration, most recently active first. |
Registration Fields
| Field | Description |
|---|---|
externalUserId | Your identifier for this person in this group. |
groupingId | The group this registration belongs to. null if the verification was submitted without one. |
status | See Status Values. |
createdAt / lastActive | First and most recent activity for this registration. |
flowId / flowName | The flow used on the most recent attempt. |
requiredDocuments | Document types this registration's flow asked for. |
info | Applicant data — see The info Object. |
verifications[] | Every attempt on this registration, newest first. |
Verification Fields
| Field | Description |
|---|---|
logId | Identifier for this attempt, usable with the portal's verification log. |
createdAt | When the attempt was submitted. |
status | The outcome of this single attempt — see Status Values. |
success | Whether the attempt passed. |
reasons | Why it did not pass, in the pipeline's own words. Empty on a clean attempt. |
documentTypes | Documents submitted on this attempt. |
sessionId | The Web SDK session, if the attempt came through a link. |
reasons collects the message from every stage that recorded one — liveness and
face match, document authenticity, data extraction, the age rule, and sanctions
screening — because any of them can sink an attempt:
"reasons": [
"Liveness Detection Failure: The selfie does not appear to be a live person. Please retake your selfie in good lighting.",
"No MRZ detected in the provided image"
]
An empty reasons on a failed attempt means no stage stored a message. Nothing
is invented to fill the gap.
The info Object
info is assembled from the applicant data submitted with this registration's
attempts, newest first per field — so a later attempt that omitted a field
inherits it from an earlier one. Fields that were never submitted are omitted
rather than returned as null.
info.idDocs is different: it holds what was read off the document by MRZ
and OCR. Those names and dates can legitimately differ from the ones the
applicant typed into the form, and that difference is often the interesting
part.
Status Values
status appears on a registration and on each individual verification. The
values are the same, but the scope differs: on a verification it is the outcome
of that one attempt; on a registration it is the person's standing overall.
| Value | Meaning |
|---|---|
approved | Verification passed, or a reviewer approved it. |
declined | A person declined this applicant from the portal. |
failed | The verification pipeline rejected the attempt — bad document, failed liveness, and so on. |
pending | Submitted and awaiting a manual review decision. |
null | Registered, but no verification attempt recorded yet. |
declined vs failedThese are different events. declined is a human decision; failed is an
automated outcome. If you are deciding whether to let someone retry, failed
usually means "try again", while declined usually does not.
A registration's status is not simply the newest attempt's status. The most
recent real decision wins — a failed retry after an approval does not
un-approve anyone — and an unresolved attempt outranks a failure, because
someone who failed and then retried is waiting rather than failed. Only a
registration with nothing but failures reads failed.
Error Responses
400 — Validation Error
{
"code": 400,
"description": "Validation failed",
"errors": ["externalUserId - Required"]
}
404 — Not Found
{
"code": 404,
"description": "No applicant found for externalUserId user-12345"
}
500 — Lookup Failed
{
"code": 500,
"description": "Applicant lookup for externalUserId user-12345 was incomplete: SDK registration lookup failed. Cannot confirm whether this applicant exists."
}
A 404 means the applicant does not exist. A 500 means a source could not be
read, so their existence could not be determined — never treat it as absence.
Re-onboarding a user who already exists will be rejected as a duplicate face.
Next Steps
- IDV API Reference — submit verifications
- Web SDK Link — check the result of a hosted session
- Flows & Groups — how groups shape registrations