---
description: >-
  Handling in_review on both Inyo KYC entry points — the status and autoStatus matrix, review-threshold routing, held rejections, and the re-delivered result carrying manualReview.
---

# Manual Review

`in_review` means a person at Inyo decides the outcome. It is **not** a failure and **not** a final answer, and it can reach you on either entry point — including in the synchronous response from [`POST /v1/verifications`](server-to-server.md) and on a [redirect](results.md#redirect-mode) your customer arrives on.

***

### Why a Verification Is `in_review`

`autoStatus` tells you what the pipeline decided before any routing or human decision, so `in_review` is never ambiguous:

| `status` | `autoStatus` | What happened |
| -------- | ------------- | ------------- |
| `in_review` | `in_review` | A **soft check failed or a check came back `indeterminate`** — for example an MRZ check digit, a document-number format mismatch, or an unconfirmed sanctions name match |
| `in_review` | `approved` | A **borderline pass**, routed by your review threshold |
| `in_review` | `declined` | A **rejection held for a human**, because you enabled held rejections |

Read `autoStatus` before deciding how to treat a held session in your own product. A borderline pass and a held rejection are very different signals, even though both arrive as `in_review`.

***

### Routing a Borderline Approval

A configured **review threshold** catches auto-approvals whose confidence falls below your bar and routes them to review instead. It applies identically to widget sessions and server-to-server verifications — one setting, one routing decision, whichever entry point the verification arrived through.

Confidence is the **weakest score among the checks carrying a calibrated per-capture measure**, which depends on how the liveness result was obtained:

| Liveness result | Checks that count toward confidence | Floor | Operative band on defaults |
| --------------- | ----------------------------------- | ----- | -------------------------- |
| Selfie mode (default) | `face_match` | Your face-match threshold | (90, 100] |
| Streaming liveness session | `face_match`, `liveness` | The lower of the two thresholds | (80, 100] |

Both counted checks are hard checks that must already clear their own thresholds for a verification to be approved. **A review threshold at or below that floor can therefore never fire** — configuring one is rejected with a message naming the computed floor. And because the floor moves with your own thresholds, read the current values from each check's `detail` rather than assuming the platform defaults.

`liveness` is deliberately excluded in selfie mode: there its score is an image-quality heuristic over face confidence, sharpness, and brightness, so routing on it would turn this into a photo-quality gate rather than a confidence gate. It remains a hard check either way — a poor capture still declines; it just does not drive review routing.

**A verification with no selfie carries no biometric score at all.** That counts as *unmeasured*, not confident, so with a threshold configured it routes to review rather than auto-approving. Send a selfie, or leave the threshold unset, if you need document-only verifications decided synchronously.

***

### Holding a Rejection for a Human

The review threshold catches borderline *approvals*. A separate setting — off by default — catches *rejections*: a verification the checks rejected routes to `in_review` instead of `declined`, so an analyst confirms it before it is final. A false decline loses a real customer silently, and that is the case this exists for.

**What it covers is whatever a human could complete.** A rejection is held when the document was readable — expired, face mismatch, liveness, rejected jurisdiction, failed authenticity, screen recapture. It is **not** held when the core identity fields never came out of the document, because approving such a session would deliver a verified identity with nothing in it; those keep auto-declining, and an analyst cannot approve one either.

This behaves the same on both entry points. The widget offers a retake on recoverable capture problems and server-to-server does not, but that difference is about coaching a live customer — it does not change what your setting means.

> **The load is uncapped.** Every held rejection becomes an analyst task and nothing throttles that queue, so with this setting on, review volume scales with however much fraud you attract.

***

### How the Decision Reaches You

When an analyst decides, the stored result is updated in place and re-delivered.

| Channel | Behavior |
| ------- | -------- |
| Webhook | With a `webhookUrl` configured, the decided result is `POST`ed to it — signed exactly like any other delivery. This covers redirect-mode sessions and server-to-server verifications too, via [result notifications](results.md#result-notifications) |
| Polling | [`GET /v1/sessions/{sessionId}`](sessions.md#retrieve-a-session) returns the live `status` and `result`. This is the only channel if you have no `webhookUrl`, or if you have disabled notifications |

The delivered result carries a `manualReview` object:

```json
{
  "sessionId": "9f1c8e42-7c3e-4f2b-9d7a-2b1e5c8f4a10",
  "status": "approved",
  "autoStatus": "declined",
  "manualReview": {
    "action": "review",
    "reviewer": "analyst-7",
    "reason": "Expiry misread by OCR; document valid through 2029 on the physical card",
    "autoStatus": "declined"
  },
  "notifiedAt": "2026-07-31T14:41:09.882Z"
}
```

| Field | Description |
| ----- | ----------- |
| `action` | `review` for a decision on a queued session, `override` for a quality-control reversal of an already-completed one |
| `reviewer` | Who decided |
| `reason` | Why, in the reviewer's words |
| `autoStatus` | The pipeline's original verdict, preserved for audit |

`status` and `autoStatus` at the top level tell you the same story: after a decision, `status` is the human outcome and `autoStatus` still shows what the automation concluded.

***

### Completed Outcomes Can Change

A verification that already reached `approved` or `declined` can be reversed later by a quality-control override. It arrives through the same channel, with `manualReview.action` set to `override`.

This is why ordering matters: **apply the highest `notifiedAt` and ignore anything older** (see [Receiving Results](results.md#ordering-concurrent-notifications)). A session can have two deliveries in flight at once — one still retrying while a newer decision is dispatched.

***

### What to Build

| Requirement | Why |
| ----------- | --- |
| A **pending state** in your own model, distinct from approved and rejected | `in_review` is neither, and your customer may be waiting on a landing page that received it |
| An **idempotent result handler** keyed on `sessionId`, resolving by `notifiedAt` | The same session's result legitimately arrives more than once |
| The ability to **reverse a decision** you already acted on | A quality-control override can flip a completed outcome |
| A **reconciliation job** for non-terminal sessions | Delivery is best-effort; polling is authoritative |

If neither the review threshold nor held rejections are configured for your tenant, `in_review` still occurs — a failed soft check or an `indeterminate` check produces it — so handle the state regardless.

***

### Next Steps

* [Checks & Decisions](checks-and-decisions.md) — which checks are soft and route here
* [Receiving Results](results.md) — delivery, signatures, and ordering
