Checks & Decisions
Every verification — widget or server-to-server — resolves to the same normalized result. This page is the reference for reading it.
The Result Payload
{
"sessionId": "9f1c8e42-7c3e-4f2b-9d7a-2b1e5c8f4a10",
"userRef": "user-123",
"status": "completed",
"decision": "approved",
"document": {
"type": "passport",
"number": "A12345678",
"personalNumber": null,
"issuingState": null,
"issuingCountry": "USA",
"issuingCountryName": "United States",
"expirationDate": "2033-09-30",
"dateOfIssue": null
},
"person": {
"firstName": "ALEX",
"lastName": "MORGAN",
"fullName": "ALEX MORGAN",
"dateOfBirth": "1988-03-04",
"gender": "F",
"nationality": "USA",
"address": null,
"city": null,
"region": null,
"postalCode": null
},
"checks": [
{ "name": "document_scene_clear", "status": "passed", "group": "document", "severity": "low", "detail": "no screen-recapture signals" },
{ "name": "document_readable", "status": "passed", "group": "document", "severity": "critical", "detail": "all core fields extracted" },
{ "name": "document_authentic", "status": "passed", "group": "document", "severity": "high", "detail": "genuine — laminate sheen, crisp typography, natural depth" },
{ "name": "document_not_expired", "status": "passed", "group": "document", "severity": "critical", "detail": "expirationDate 2033-09-30" },
{ "name": "document_number_valid", "status": "passed", "group": "document", "severity": "medium", "detail": "passport:USA — document number matches the USA passport format" },
{ "name": "document_mrz_valid", "status": "passed", "group": "document", "severity": "low", "detail": "all check digits valid" },
{ "name": "face_detected", "status": "passed", "group": "selfie", "severity": "medium", "detail": "One face, fully visible and facing the camera." },
{ "name": "liveness", "status": "passed", "group": "selfie", "severity": "high", "detail": "heuristic (selfie mode): face confidence + quality vs threshold 80.0" },
{ "name": "face_match", "status": "passed", "group": "selfie", "severity": "high", "detail": "portrait-to-selfie similarity vs threshold 90.0" }
],
"prefillMismatches": [],
"prefillComparison": [],
"provider": "inyo",
"trustIndex": 100
}
Top-Level Fields
| Field | Description |
|---|---|
sessionId | The identifier. Read a widget session with GET /v1/sessions/{sessionId}, a verification with GET /v1/verifications/{verificationId} |
userRef | Your identifier, echoed back exactly as you supplied it |
status | Where the session is in its lifecycle — see Status and decision |
decision | What Inyo concluded. Present only under managed decisioning, and only for a session that reached an assessment — the key is absent otherwise, not null. See Status and decision |
document | Extracted document data |
person | Extracted identity data |
checks | The individual checks that produced the decision — see Reading checks[] |
prefillMismatches | Names of prefill fields that disagreed with the document. Empty unless you sent prefill |
prefillComparison | Per-field comparison detail — see Prefill comparison |
selfieDescription | Structured appearance description. Present only when enabled for your tenant — the key is absent otherwise, not null |
screening | Sanctions-listing matches behind a failed screen. Present only when there is something to adjudicate — the key is absent on a clear screen, and absent entirely unless screening is enabled for your tenant. See Sanctions Screening |
trustIndex | The verification's score, 0-100 |
provider | inyo for a real verification, inyo-simulated for one a reserved document number dictated — see Sandbox |
notifiedAt | Present only on delivered notifications, not on the stored result. Use it to order concurrent deliveries — see Receiving Results |
document
| Field | Description |
|---|---|
type | passport, drivers_license, or identity_card — the same vocabulary you send in prefill.documentType |
number | Document number as read from the document |
personalNumber | Secondary national identifier where the document carries one |
issuingState | The subdivision as printed — a US state for licenses. null on a passport, which carries no subdivision; read issuingCountry for the issuer |
issuingCountry | Resolved ISO 3166-1 alpha-3 country |
issuingCountryName | Display name for issuingCountry (for example United States). Falls back to the code itself when it is not a recognized country, and is null only when issuingCountry is null |
expirationDate, dateOfIssue | YYYY-MM-DD, or null when the document does not carry it |
person
firstName, lastName, fullName, dateOfBirth, gender, nationality, address, city, region, postalCode.
Any field the document does not carry — or that could not be read — is null. Address fields are commonly null on passports and populated from driver's licenses. Names arrive as printed on the document, which for passports means uppercase.
Status and Decision
These are two different fields. status says where the session is; decision says what Inyo concluded, and only exists if you asked us to conclude anything.
status — always present
status | Meaning | What to do |
|---|---|---|
pending | Created, not yet completed | Wait |
completed | The customer finished and the session was assessed | Read checks, trustIndex, and decision if you have it |
failed | The capture was never usable — on the widget, the customer ran out of attempts | Create a new session. There is no decision on a failed session |
errored | A platform fault stopped the assessment. Reachable only on server-to-server verifications | Retry the call with a new Idempotency-Key. There is no decision, and nothing reached a reviewer |
status never names a verdict.
decision — managed decisioning only
decision | Meaning | What to do |
|---|---|---|
approved | The verification cleared | Proceed with onboarding |
declined | The verification did not clear | Do not proceed. The failed rows in checks say why |
inconclusive | The evidence did not settle it either way — final | Do not proceed. It is not a decision against the customer, so a fresh, clearer capture may settle it |
in_review | A human is deciding — not final | Treat as pending. See Manual Review |
Two decisioning modes. Under non-managed decisioning — the default — Inyo publishes the assessment and you decide: checks and trustIndex are there, and decision is absent from the payload entirely. Under managed decisioning Inyo also reaches a verdict, which is the decision field, and may route a session to a human reviewer first.
The key is absent, not
null."decision" in payloaddistinguishes "you were never going to be told one" from a decision we could not reach — there is no such thing as the latter.
Reading checks[]
Every check is exactly six fields: name, status, group, severity, reason, detail.
| Field | What to do with it |
|---|---|
status | "passed", "failed", "undetermined", or "not_evaluated". Authoritative — do not re-derive it from detail |
severity | "critical", "high", "medium" or "low", heaviest first — how much weight this check carries on this verification. It is a weight, never a destination, and it is configurable, so read decision for the outcome rather than deriving one from it |
group | document, selfie, screening, or policy. A name's group never changes, so filter on this rather than keeping your own name lists |
reason | A stable token on undetermined and not_evaluated rows — provider_unavailable, no_mrz, and so on. null elsewhere |
detail | Prose for a human, including the bar where a check has one. Never branch on it — the wording may change, the status will not |
The set varies. Checks appear only when applicable — a machine-readable zone needs a document that carries one, face checks need a selfie, several are enabled per account. Iterate the array; never index it positionally.
Two ways a check reaches no verdict, and neither is a finding against the customer. undetermined means it ran and could not conclude; not_evaluated means it never got a verdict at all — a provider we could not reach, input the document did not carry, a capture rejected before its turn. Both are charged a fraction of the check's weight rather than ignored, and reason names which. An absent row is a third thing again: the check was never applicable.
The Checks
One row per check. severity is configurable, so what any one failure costs your verifications is set for your account.
| Check | Group | What it checks | Present |
|---|---|---|---|
document_readable | document | The core identity fields — number, name, date of birth — all came off the document | Always |
document_not_expired | document | The expiration date is not in the past | When the document carries one |
document_authentic | document | A forensic review of every submitted side finds no recapture, print, photocopy or forgery. Sides are merged into this one row, and detail names each side's verdict. A rejection is read twice before it stands | When enabled for your account (default on) |
document_scene_clear | document | No phone, monitor or screen is visible anywhere in the frame. It reads the whole image, so a display behind a genuine document trips it too | Always |
document_mrz_valid | document | The machine-readable zone's check digits validate | When the document has an MRZ |
document_barcode_valid | document | The barcode on the back carries a well-formed identity record. A back too soft to read publishes not_evaluated with reason no_barcode and costs nothing | When the document is a US driver's license or state identification card |
document_sources_agree | document | The document's machine-readable data — a passport's zone, a US license's barcode — agrees with what its face prints. A disagreement names the fields that differ, never their values | When the document yielded machine-readable data and its face yielded fields to compare |
document_number_valid | document | The number matches the known format for its jurisdiction. Advisory — the format tables are published-but-imperfect | When a rule exists for that jurisdiction |
document_data_match | document | The data you sent agrees with the document | When you set dataCheck: true and the document yielded an identity |
document_type_match | document | The document is the type you declared. The type is read from the images and compared against prefill.documentType; with no declared type the check publishes not_evaluated rather than a verdict | Always |
face_detected | selfie | Exactly one usable face — not eyes closed, sunglasses, or a heavily turned head | When a selfie was captured |
liveness | selfie | The selfie is a live person rather than a reproduction | When a selfie was captured |
face_match | selfie | The selfie is the same person as the document portrait | When a selfie was captured |
age_match | selfie | The document's date of birth agrees with the age estimated from the selfie, within a tolerance set for your account. An estimate is a signal, not proof | When appearance description is enabled (default off), the selfie yielded a usable face, and the document yielded a date of birth |
sanctions_clear | screening | The name does not match an OFAC SDN listing whose date of birth also agrees. A match it cannot confirm publishes undetermined — see Sanctions Screening | When screening is enabled for your account (default off) and the document yielded an identity |
dmv_record_match | screening | Extracted US license data agrees with the issuing DMV's record | When DMV verification is enabled (default off) and the document yielded an identity |
jurisdiction_accepted | policy | The issuing country is one you accept for that document type | When you have configured accepted jurisdictions |
capture_attempts | policy | The customer did not exhaust the retry limit — see Widget Delivery | Widget flow only |
Prefill Comparison
When you send prefill, the result reports how each comparable field lined up. prefillMismatches is the short list of field names that disagreed; prefillComparison carries the detail:
"prefillComparison": [
{ "field": "lastName", "sent": "Silva", "received": "SILVA SANTOS", "match": true, "method": "subset", "score": 0.95, "threshold": 0.85 },
{ "field": "dateOfBirth", "sent": "1988-03-04", "received": "1988-03-04", "match": true, "method": "exact", "score": null, "threshold": null },
{ "field": "issuingState", "sent": "Florida", "received": "FL", "match": true, "method": "normalized", "score": null, "threshold": null }
]
| Field | Description |
|---|---|
field | One of firstName, lastName, dateOfBirth, documentType, documentNumber, nationality, issuingState, issuingCountry |
sent / received | Your value, and the value read from the document |
match | Whether they are considered equal |
method | For names: exact, subset (a multi-part surname containing yours, scored 0.95), fuzzy (similarity scoring), or empty. For states and document types: normalized. Otherwise exact |
score / threshold | Name similarity and the acceptance bar, null for non-name fields |
Fields absent on either side are not compared, so they never appear — a passport's issuingState is one of them, since no passport evidences a subdivision. Send the country as issuingCountry, which is compared. Names use fuzzy matching — accents stripped, multi-part surnames accepted as subset matches, OCR near-misses tolerated up to the threshold — so ordinary spelling variance does not create false mismatches. States and document types are normalized first, which is why Florida matches FL.
A mismatch only affects the decision when you set dataCheck: true, which adds the check document_data_match. Without it, the comparison is informational.
Selfie Description
null unless the appearance description is enabled for your tenant. When present, it carries observable attributes of the selfie for later re-verification comparison:
"selfieDescription": {
"estimatedAgeLow": 30,
"estimatedAgeHigh": 40,
"skinTone": "light",
"eyeColor": "brown",
"hairColor": "dark brown",
"hairStyle": "shoulder length, straight",
"facialHair": "none",
"eyewear": "none",
"headwear": "none",
"distinguishingFeatures": ["small scar above left eyebrow"],
"notes": null
}
Any attribute that cannot be observed is null.
Appearance descriptions are sensitive personal data. They carry GDPR and biometric-privacy (for example BIPA) exposure, and the selfie is processed by an external vision service. Confirm you have a lawful basis, a data-processing agreement in place, and a documented retention justification before asking us to enable this.
Sanctions Screening
Off by default, and enabled per tenant by Inyo on request. When on, the name extracted from the document is screened against the OFAC Specially Designated Nationals (SDN) list.
A name match is not a person
Two common name tokens appear in many listed names, so a name hit alone never declines. Confirmation is by date of birth, and the outcome is the status of the one sanctions_clear check:
| Outcome | sanctions_clear | Effect |
|---|---|---|
| No listing at or above your threshold | "passed" | unaffected |
| A listing matches and its date of birth agrees | "failed" | charged at this check's full weight |
| A listing matches but no candidate's date of birth agrees | "undetermined" | charged less — an unconfirmed hit is not a finding |
Nothing ambiguous resolves toward approval. Gate on status. The acceptance threshold is configured per tenant — ask your Inyo contact what yours is set to.
The screening block
Present only when a hit was delivered — that is, alongside a sanctions_clear that is failed (a confirmed match) or undetermined (an unconfirmed one). It is absent on a clear screen, so its presence means "there is something to adjudicate", not "screening ran".
"screening": {
"matches": [
{
"name": "SAMPLE, Ali Hassan",
"sourceList": "us_ofac",
"programs": ["SDGT"],
"entityId": "12345",
"score": 91.5,
"dobAgrees": true
}
]
}
| Field | Meaning |
|---|---|
name | The listed entity's name as published |
sourceList | Always us_ofac today |
programs | OFAC sanctions programs the listing falls under |
entityId | The listing's identifier, for looking it up at source |
score | Name similarity, 0-100 |
dobAgrees | true, false, or null when undecidable — the document's date of birth could not be read, or the listing publishes none |
At most five matches are delivered — enough to tell a plausible single match from an obvious collision cluster. Only candidates at or above your threshold appear.
One quirk worth knowing when you adjudicate: OFAC publishes imprecise dates collapsed to 1 January, so a candidate listed on that day is compared by year alone. For anyone genuinely born on 1 January that loosens matching, in the declining direction.
Your customer is never told that a sanctions match is what rejected them. The declining response is byte-identical to any other unretryable failure.
If screening is unavailable
Screening fails open: a provider outage skips the check rather than blocking or declining the verification. Nothing in the delivered result distinguishes a session that shipped unscreened from one that screened clean — there is no sanctions_clear row at all in that case. If you need positive evidence that every verification was screened, assert on the presence of that check rather than on the absence of a match.
Next Steps
- Manual Review — what
in_reviewmeans and how a decision changes - Receiving Results — how this payload reaches you
