---
description: >-
  The normalized Inyo KYC result payload — document and person fields, the approved/declined/in_review/expired statuses, autoStatus, and how to read the four-field checks array and its groups.
---

# Checks & Decisions

Every verification — widget or [server-to-server](server-to-server.md) — resolves to the same normalized result. This page is the reference for reading it.

***

### The Result Payload

```json
{
  "sessionId": "9f1c8e42-7c3e-4f2b-9d7a-2b1e5c8f4a10",
  "userRef": "user-123",
  "status": "approved",
  "autoStatus": "approved",
  "document": {
    "type": "passport",
    "number": "A12345678",
    "personalNumber": null,
    "issuingState": "USA",
    "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", "detail": "no screen-recapture signals" },
    { "name": "document_readable",     "status": "passed", "group": "document", "detail": "all core fields extracted" },
    { "name": "document_number_valid", "status": "passed", "group": "document", "detail": "passport:USA — document number matches the USA passport format" },
    { "name": "document_mrz_valid",    "status": "passed", "group": "document", "detail": "all check digits valid" },
    { "name": "document_not_expired",  "status": "passed", "group": "document", "detail": "expirationDate 2033-09-30" },
    { "name": "document_authentic",    "status": "passed", "group": "document", "detail": "genuine — laminate sheen, crisp typography, natural depth" },
    { "name": "face_detected",         "status": "passed", "group": "selfie",   "detail": "eyes_open=True sunglasses=False frontal=True" },
    { "name": "liveness",              "status": "passed", "group": "selfie",   "detail": "heuristic (selfie mode): face confidence + quality vs threshold 80.0" },
    { "name": "face_match",            "status": "passed", "group": "selfie",   "detail": "CompareFaces similarity vs threshold 90.0" }
  ],
  "prefillMismatches": [],
  "prefillComparison": [],
  "provider": "inyo"
}
```

***

### Top-Level Fields

| Field | Description |
| ----- | ----------- |
| `sessionId` | The verification's identifier. Use it with [`GET /v1/sessions/{sessionId}`](sessions.md#retrieve-a-session) |
| `userRef` | Your identifier, echoed back exactly as you supplied it |
| `status` | Where the verification landed — see [Statuses](#statuses) |
| `autoStatus` | What the pipeline decided **before** any review routing or human decision — see [Manual Review](manual-review.md) |
| `document` | Extracted document data |
| `person` | Extracted identity data |
| `checks` | The individual checks that produced the decision — see [Reading `checks[]`](#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](#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](#sanctions-screening) |
| `provider` | `inyo` for a real verification, `inyo-mock` for a simulated one — see [Sandbox](sandbox-and-test-data.md) |
| `manualReview` | **Present only after a human decision.** See [Manual Review](manual-review.md) |
| `notifiedAt` | **Present only on delivered notifications**, not on the stored result. Use it to order concurrent deliveries — see [Receiving Results](results.md#ordering-concurrent-notifications) |

#### `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` | Issuing authority as printed — a US state for licenses, a country code on passports |
| `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.

***

### Statuses

| `status` | Meaning | What to do |
| -------- | ------- | ---------- |
| `pending` | Created, not yet completed | Wait. No decision exists yet |
| `approved` | Every check passed | Proceed with onboarding |
| `declined` | At least one **hard** check failed | Do not proceed. The failing check's `detail` explains why |
| `in_review` | A human decides — **not final** | Treat as pending. See [Manual Review](manual-review.md) |
| `expired` | The customer never completed it within the link's lifetime | Create a new session if you still need the verification |

***

### Reading `checks[]`

Every check is exactly four fields: `name`, `status`, `group`, `detail`.

| Rule | Detail |
| ---- | ------ |
| **`status` is authoritative** | `"passed"`, `"failed"`, or `"indeterminate"` — the check's outcome. Do not re-derive it — `detail` explains the outcome, it does not define it. A failed **hard** check declines the verification; a failed **soft** check — and **any** `indeterminate` — routes it to `in_review` |
| **`group` says where the check sits** | `document`, `selfie`, `screening`, or `policy`. A name's group never changes, so filter on `group` rather than maintaining your own name lists |
| **`detail` quotes the bar** | Where a check has a threshold, `detail` names it. **The `liveness` and `face_match` thresholds are per tenant**, so read them here rather than assuming the platform defaults |
| **The set varies** | Checks appear only when applicable: `document_mrz_valid` needs a machine-readable zone, face checks need a selfie, and several are enabled per tenant. Iterate the array; do not index it positionally or assume a fixed length |

Today `sanctions_clear` is the only check that publishes `indeterminate` — an unconfirmed sanctions name match, see [Sanctions Screening](#sanctions-screening) — but handle the status generically rather than special-casing the name: any `indeterminate` check means a human decides.

**`document_authentic` merges every reviewed side into one row.** It is a **hard** check — a `failed` row declines the verification outright. On a card, the front and a submitted back are both reviewed and merged into this one row: any failing side fails it, and `detail` carries each side's verdict as `classification — what the examiner saw`, side-prefixed when more than one side was reviewed (`"front: genuine — …; back: photocopy — …"`) and unprefixed when only one was. The review is fail-open per side — a side that could not be judged simply drops out, and if no side produced a verdict the row is absent.

**`age_match` only publishes when appearance description is enabled for your tenant and the document yielded a date of birth.** It passes when the document-derived age at capture falls inside the selfie description's estimated age range, widened by a tolerance Inyo configures for your tenant (5 years by default, and quoted in `detail` as `±5`), and fails otherwise. The tolerance absorbs the imprecision of an age estimate; set to `0` it compares against the estimated range exactly. Because an age estimate is a signal rather than proof, a failure never declines — it is a **soft** check that routes to `in_review`. Absent whenever appearance description is off, the document carries no date of birth, or the description itself failed open (an outage never blocks the verification) — none of which is itself a signal.

***

### The Checks

**Hard checks** fail the verification — any one of them failing yields `declined`.

| Check | Group | Fails when | Present |
| ----- | ----- | ---------- | ------- |
| `document_scene_clear` | `document` | A phone, monitor, or screen is visible in frame — the document is being photographed off a display | Always |
| `document_authentic` | `document` | The forensic document review judges any reviewed side — the front, plus a submitted card back — a recapture, print, photocopy, or forgery | When enabled for your tenant (default on), and at least one side could be reviewed |
| `document_readable` | `document` | The core identity fields (number, name, date of birth) could not all be extracted | Always |
| `document_not_expired` | `document` | The expiration date is in the past | When the document carries an expiration date |
| `jurisdiction_accepted` | `policy` | The document's issuing country is outside the jurisdictions you accept for that document type | When you have configured accepted jurisdictions |
| `sanctions_clear` | `screening` | The name on the document matches an OFAC SDN listing **and** a matching listing's date of birth agrees. A name match that could **not** be confirmed publishes this same check as `indeterminate` instead, routing to `in_review` — see [Sanctions Screening](#sanctions-screening) | When sanctions screening is enabled for your tenant (default off) |
| `face_detected` | `selfie` | Not exactly one usable face — eyes closed, sunglasses, or a heavily turned head | When a selfie was captured |
| `liveness` | `selfie` | Liveness confidence is below your threshold | When a selfie was captured |
| `face_match` | `selfie` | Similarity between the document portrait and the selfie is below your threshold | When a selfie was captured |

**Soft checks** route the verification to `in_review` rather than declining it.

| Check | Group | Fails when | Present |
| ----- | ----- | ---------- | ------- |
| `document_mrz_valid` | `document` | An MRZ check digit does not validate — an OCR error or tampering | When the document has a machine-readable zone |
| `document_number_valid` | `document` | The number does not match the known format for its jurisdiction. **Advisory** — the format tables are published-but-imperfect, so this never auto-declines | When a rule exists for that jurisdiction |
| `dmv_record_match` | `screening` | Extracted US license data disagrees with the issuing DMV's record | When DMV verification is enabled for your tenant (default off) |
| `document_data_match` | `document` | Data you sent disagrees with the document | When you set `dataCheck: true` |
| `age_match` | `selfie` | The document-derived age at capture falls outside the selfie description's estimated age range widened by your tenant's tolerance (5 years by default) | When appearance description is enabled for your tenant (default off) and the document yielded a date of birth |

One further check appears only in the widget flow: `capture_attempts` (group `policy`) fails when the customer exhausted the retry limit, which declines the session — see [Widget Delivery](widget-delivery.md#capture-retries).

***

### 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:

```json
"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` |
| `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. 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 `document_data_match` soft check. 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:

```json
"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` | `status` |
| ------- | ----------------- | -------- |
| No listing at or above your threshold | `"passed"` | unaffected |
| A listing matches **and** its date of birth agrees | **`"failed"`** — hard, declines | `declined` |
| A listing matches but no candidate's date of birth agrees | **`"indeterminate"`** | `in_review` |

Nothing ambiguous resolves toward approval: an unconfirmable hit goes to a human rather than through. 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 `indeterminate` (an unconfirmed one). It is absent on a clear screen, so its presence means "there is something to adjudicate", not "screening ran".

```json
"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](manual-review.md) — `in_review`, `autoStatus`, and `manualReview`
* [Receiving Results](results.md) — how this payload reaches you
