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 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/peopledocuments[]) before you can open a session β otherwise the request returns422 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 |
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:
drivers_licensepassport- 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):
{
"sessionId": "9f1c8e42-7c3e-4f2b-9d7a-2b1e5c8f4a10",
"status": "pending",
"widgetUrl": "https://{FQDN}/verify/8Kd2mQ..."
}
The Flow
- Create the session and open
widgetUrlfor your user (link, redirect, or webview β see Widget Delivery). - The user photographs their document and takes a selfie in the widget.
- Inyo receives the signed verification result directly β you don't handle any document images.
- On a verified result, the person's document records are created/updated automatically with the verification verdict, and the
DocumentUpdatedEventswebhook fires. - Read the outcome from that webhook.
verificationStatusisVERIFIEDwhen the session was approved,REJECTEDwhen it was declined, andPENDINGwhen it was routed to manual review β in which case a second event follows with the analyst's decision. The event'sidis the session id throughout, so the two correlate.
The webhook is the fastest channel, and reading the session is the fallback when a delivery is missed. Subscribing to DocumentUpdatedEvents is still what makes the flow event-driven rather than poll-driven. The verdict also lands on the person's document as kycVerdict (readable via GET /people/{personId}): VERIFIED for an approved session, REJECTED for a declined one. A session still in review leaves the document showing whatever the declared-document check recorded when you created the person β which is not a statement about this session. Read the webhook for the session's own outcome.
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 |
422 | KYC_UNAVAILABLE | The KYC verification could not be completed β the service was unavailable, or KYC isn't configured for your tenant. Retry with backoff; if it persists, contact Inyo support. |
Get a Verification Session
Endpoint: GET /organizations/{tenant}/v2/people/{personId}/kycSession/{sessionId}
Authentication: Tenant-level
Read one session β the fallback when you miss a webhook delivery. You always hold the id: it comes back from session creation and rides on every DocumentUpdatedEvents as id.
curl --request GET \
--url https://{FQDN}/organizations/$TENANT/v2/people/$PERSON_ID/kycSession/$SESSION_ID \
--header "x-api-key: $API_KEY"
| Field | Notes |
|---|---|
sessionId | Same value the DocumentUpdatedEvents id carries |
status | PENDING, VERIFIED, REJECTED, PENDING_REVIEW, EXPIRED, or ORPHANED |
result | The normalized result in full, including checks[]. Null until the first outcome arrives, so a PENDING session returns a status and no result. |
completedAt | Null while undecided, and for PENDING_REVIEW β which reports progress, not an outcome |
createdAt | When the session was created |
widgetUrl | Only while the session is undecided. The token is in the URL path, so a spent one is not returned. |
404 when the session id is unknown, belongs to another person, or belongs to another organization.
Legacy: Direct Document Uploads (Deprecated)
β οΈ Deprecated. Use KYC Verification Sessions 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, DRIVERS_LICENSE, ...) are accepted. The full set:
| Wire value | Canonical value(s) | Description |
|---|---|---|
passport | PASSPORT | Passport |
driversLicense | DRIVERS_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 valuenationalId, 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 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 |
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):
{
"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
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.
These endpoints cover uploaded documents only. They resolve
{documentId}as an upload, so a document verified through a KYC verification session has no record here and both routes return404. Read a session's outcome from theDocumentUpdatedEventswebhook, and the resulting verdict fromkycVerdicton the person's document.
Get Current Verification Status
Endpoint: GET /organizations/{tenant}/documents/{documentId}/verificationStatus/current
Authentication: Tenant-level
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
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
Lists uploads, so a person whose documents were all verified through KYC sessions comes back as an empty array. Their declared documents and verdicts are on the person record.
Endpoint: GET /organizations/{tenant}/people/{personId}/documents
Authentication: Tenant-level
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 for the payload reference.
Best Practices
- Upload high-quality images β blurry or cropped images will be rejected.
- Pass
idNumberandexpirationDatewhen you have them β they enrich the compliance record and speed up review. - Check compliance level after verification β once a document is
VERIFIED, callGET /participants/{id}/complianceLevelsto see if the sender has been upgraded. - Handle rejections gracefully β surface the rejection
reasonand prompt the user to re-upload. - Use webhooks instead of polling β register for
DocumentUpdatedEventsto be notified immediately.
Test Data
For sandbox testing, see Test Data and Sandbox Testing β certain name/address combinations trigger specific compliance behaviors (approval, hold, rejection).
