---
description: >-
  Verify a sender's identity documents — create a KYC verification session (recommended) with a hosted capture widget, or use the deprecated direct document upload endpoints; track verification via webhooks.
---

# Verifying Documents

To advance a sender to higher compliance levels (Level 2 and above), their identity documents must be verified. There are two ways to get documents verified:

* **KYC Verification Session (recommended)** — create a session and hand your user a hosted widget URL. The widget captures the document photos and a selfie, verifies them, and the result flows back to the sender's record automatically.
* **Direct document upload (deprecated)** — POST document image files yourself and wait for OCR/manual review.

> ⚠️ **The direct upload endpoints are deprecated.** New integrations should use KYC Verification Sessions. Existing upload integrations keep working, but the session flow adds selfie liveness, document-portrait face matching, and presentation-attack defense that plain file uploads cannot provide.

***

### KYC Verification Sessions (Recommended)

**Endpoint:** `POST /organizations/{tenant}/v2/people/{personId}/kycSession`\
**Authentication:** Tenant-level (`x-api-key`)

Creates an [Inyo360 Identity Verification](../../kyc/README.md) session for the person. You supply only widget-presentation parameters — the document to verify is **resolved automatically from the person's declared documents**, and the session is prefilled from the person record (name, date of birth, document number, issuing state, nationality), so the user only has to capture their document and selfie.

> **Declare the document first.** The person must have a non-expired, KYC-verifiable document on file (added via `POST /v2/people` `documents[]`) before you can open a session — otherwise the request returns `422 NO_VERIFIABLE_DOCUMENT`.

#### Request Body

Both fields are required:

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `language` | string | Yes | Widget language, `xx` or `xx-XX` (e.g. `en`, `es`, `pt-BR`) |
| `redirectUrl` | string (URL) | Yes | Where to send the user after they finish the widget flow |

```bash
curl --request POST \
  --url https://{FQDN}/organizations/$TENANT/v2/people/$PERSON_ID/kycSession \
  --header 'Content-Type: application/json' \
  --header "x-api-key: $API_KEY" \
  --data '{
  "language": "en",
  "redirectUrl": "https://your-app.example/kyc/done"
}'
```

#### How the Document Is Selected

Inyo walks the person's declared documents in priority order and verifies the first eligible one:

1. `drivers_license`
2. `passport`
3. The identity-card family, most-specific first: `dni`, `cc`, `cpf`, `consular_id`, `voter_id`, `id`

Documents with an expiration date in the past are skipped (a person with an expired driver's license and a valid passport gets the passport); documents without an expiration date on file are trusted. `ssn`, `itin`, and `other` are never selected — they don't map to the KYC document vocabulary.

Data cross-checking is always on: the KYC layer compares the data extracted from the captured document against the prefilled person record.

**Response (201):**

```json
{
  "sessionId": "9f1c8e42-7c3e-4f2b-9d7a-2b1e5c8f4a10",
  "status": "pending",
  "widgetUrl": "https://{FQDN}/verify/8Kd2mQ..."
}
```

#### The Flow

1. Create the session and open `widgetUrl` for your user (link, redirect, or webview — see [Widget Delivery](../../kyc/widget-delivery.md)).
2. The user photographs their document and takes a selfie in the widget.
3. Inyo receives the signed verification result directly — **you don't handle any document images**.
4. On a verified result, the person's document records are created/updated automatically with the verification verdict, and the `DocumentUpdatedEvents` [webhook](../webhooks.md#4-documentupdatedevents) fires.
5. Check the sender's new level via `GET /participants/{id}/complianceLevels`.

Sessions move through these states on the Inyo side: `PENDING` → `VERIFIED`, `REJECTED`, `PENDING_REVIEW` (manual review at the KYC layer), or `EXPIRED`.

#### Errors

| HTTP | Error | Cause |
| ---- | ----- | ----- |
| `404` | `NOT_FOUND` | Person not found in your tenant |
| `422` | `NO_VERIFIABLE_DOCUMENT` | The person has no non-expired, KYC-verifiable document on file — declare one via `POST /v2/people` (`documents[]`) first |
| `422` | `VALIDATION_ERROR` | `language` or `redirectUrl` missing/malformed |
| `502` | `KYC_UPSTREAM_ERROR` | The KYC service is temporarily unavailable — retry the request |

***

## Legacy: Direct Document Uploads (Deprecated)

> ⚠️ **Deprecated.** Use [KYC Verification Sessions](#kyc-verification-sessions-recommended) instead. These endpoints remain available for existing integrations. Uploaded documents are verified by AI OCR (where enabled for your tenant) or by the Inyo compliance team.

***

### Identity Documents

**Endpoint:** `POST /organizations/{tenant}/people/{personId}/documents/documentId/{subtype}/upload`\
**Authentication:** Tenant-level (`x-api-key`)\
**Content-Type:** `multipart/form-data`

The `{subtype}` path segment is the **document type**. Both wire format (`passport`, `driversLicense`, ...) and canonical uppercase (`PASSPORT`, `DRIVER_LICENSE`, ...) are accepted. The full set:

| Wire value | Canonical value(s) | Description |
| ---------- | ------------------ | ----------- |
| `passport` | `PASSPORT` | Passport |
| `driversLicense` | `DRIVER_LICENSE` | Driver's license |
| `nationalId` | `DNI`, `CC`, `ID` | National ID — DNI (Spain/Argentina/Peru), CC (Colombia cédula), or generic government ID |
| `cpf` | `CPF` | CPF (Brazil) |
| `consularId` | `CONSULAR_ID` | Consular ID (matrícula consular) |
| `voterId` | `VOTER_ID` | Voter ID |
| `ssn` | `SSN` | US Social Security card |
| `itin` | `ITIN` | US ITIN (IRS) |
| `other` | `OTHER` | Other government-issued document |

> The three national-ID variants (`DNI`, `CC`, `ID`) collapse to the single wire value `nationalId`, so clients don't have to handle country-specific naming.

An unknown subtype returns `422 INVALID_SUBTYPE` with the `allowed` list in the response. `PROOF_OF_FUNDS` is deliberately **not** accepted here — use the dedicated [source of funds endpoint](#source-of-funds) below.

#### Form Fields

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `file` | file | Yes | The document image — `pdf`, `jpg`, `jpeg`, or `png`, max 10 MB |
| `idNumber` | string | No | The document number printed on the document |
| `issuer` | string | No | Issuing authority or state |
| `expirationDate` | string | No | Format `YYYY-MM-DD` |

```bash
curl --request POST \
  --url https://{FQDN}/organizations/$TENANT/people/$PERSON_ID/documents/documentId/PASSPORT/upload \
  --header "x-api-key: $API_KEY" \
  --form "file=@/path/to/passport.jpg" \
  --form "idNumber=AB1234567" \
  --form "expirationDate=2030-12-31"
```

**Response (201):**

```json
{
  "id": "4ec66735-216b-4ab4-b1d7-00558baa6d85",
  "subtype": "PASSPORT",
  "fileName": "passport.jpg",
  "verificationStatus": "PENDING",
  "createdAt": "2026-08-04T12:00:00+00:00",
  "idNumber": "AB1234567",
  "issuer": null,
  "expirationDate": "2030-12-31"
}
```

***

### Source of Funds

Proof of the sender's source of funds (bank statement, pay stub), required for higher compliance tiers.

**Endpoint:** `POST /organizations/{tenant}/people/{personId}/documents/sourceOfFunds/upload`\
**Authentication:** Tenant-level (`x-api-key`)\
**Content-Type:** `multipart/form-data`

```bash
curl --request POST \
  --url https://{FQDN}/organizations/$TENANT/people/$PERSON_ID/documents/sourceOfFunds/upload \
  --header "x-api-key: $API_KEY" \
  --form "file=@/path/to/bank-statement.pdf"
```

Same file rules as identity documents (`pdf`/`jpg`/`jpeg`/`png`, max 10 MB). The upload is stored under the `PROOF_OF_FUNDS` type.

***

### Document Verification Status

After uploading, documents are verified asynchronously — by AI OCR (if enabled for your tenant) within minutes, otherwise by the compliance team.

#### Get Current Verification Status

**Endpoint:** `GET /organizations/{tenant}/documents/{documentId}/verificationStatus/current`\
**Authentication:** Tenant-level

```bash
curl --request GET \
  --url https://{FQDN}/organizations/$TENANT/documents/$DOCUMENT_ID/verificationStatus/current \
  --header "x-api-key: $API_KEY"
```

#### Get Verification Status History

**Endpoint:** `GET /organizations/{tenant}/documents/{documentId}/verificationStatus`\
**Authentication:** Tenant-level

```bash
curl --request GET \
  --url https://{FQDN}/organizations/$TENANT/documents/$DOCUMENT_ID/verificationStatus \
  --header "x-api-key: $API_KEY"
```

Returns entries with `status`, `reason`, `verifiedBy`, and `createdAt` — the `reason` field explains rejections so you can guide the user to re-upload.

#### List a Person's Documents

**Endpoint:** `GET /organizations/{tenant}/people/{personId}/documents`\
**Authentication:** Tenant-level

```bash
curl --request GET \
  --url https://{FQDN}/organizations/$TENANT/people/$PERSON_ID/documents \
  --header "x-api-key: $API_KEY"
```

***

### Verification Statuses

| Status | Description |
| ------ | ----------- |
| `PENDING` | Document uploaded, awaiting verification |
| `VERIFIED` | Document accepted — compliance level may upgrade |
| `REJECTED` | Document rejected — check the `reason` and re-upload |

***

### Webhook Notifications

Register for the `DocumentUpdatedEvents` webhook to be notified when a document's verification status changes — one event on upload (`PENDING`) and another when verification completes (`VERIFIED`/`REJECTED`). See [Webhooks](../webhooks.md#4-documentupdatedevents) for the payload reference.

***

### Best Practices

* **Upload high-quality images** — blurry or cropped images will be rejected.
* **Pass `idNumber` and `expirationDate`** when you have them — they enrich the compliance record and speed up review.
* **Check compliance level after verification** — once a document is `VERIFIED`, call `GET /participants/{id}/complianceLevels` to see if the sender has been upgraded.
* **Handle rejections gracefully** — surface the rejection `reason` and prompt the user to re-upload.
* **Use webhooks** instead of polling — register for `DocumentUpdatedEvents` to be notified immediately.

***

### Test Data

For sandbox testing, see [Test Data](test-data.md) and [Sandbox Testing](../sandbox-testing.md) — certain name/address combinations trigger specific compliance behaviors (approval, hold, rejection).
