Inyo

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.


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/people documents[]) before you can open a session β€” otherwise the request returns 422 NO_VERIFIABLE_DOCUMENT.

Request Body

Both fields are required:

FieldTypeRequiredDescription
languagestringYesWidget language, xx or xx-XX (e.g. en, es, pt-BR)
redirectUrlstring (URL)YesWhere 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:

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

{
  "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).
  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 fires.
  5. Read the outcome from that webhook. verificationStatus is VERIFIED when the session was approved, REJECTED when it was declined, and PENDING when it was routed to manual review β€” in which case a second event follows with the analyst's decision. The event's id is 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

HTTPErrorCause
404NOT_FOUNDPerson not found in your tenant
422NO_VERIFIABLE_DOCUMENTThe person has no non-expired, KYC-verifiable document on file β€” declare one via POST /v2/people (documents[]) first
422VALIDATION_ERRORlanguage or redirectUrl missing/malformed
422KYC_UNAVAILABLEThe 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"
FieldNotes
sessionIdSame value the DocumentUpdatedEvents id carries
statusPENDING, VERIFIED, REJECTED, PENDING_REVIEW, EXPIRED, or ORPHANED
resultThe normalized result in full, including checks[]. Null until the first outcome arrives, so a PENDING session returns a status and no result.
completedAtNull while undecided, and for PENDING_REVIEW β€” which reports progress, not an outcome
createdAtWhen the session was created
widgetUrlOnly 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 valueCanonical value(s)Description
passportPASSPORTPassport
driversLicenseDRIVERS_LICENSEDriver's license
nationalIdDNI, CC, IDNational ID β€” DNI (Spain/Argentina/Peru), CC (Colombia cΓ©dula), or generic government ID
cpfCPFCPF (Brazil)
consularIdCONSULAR_IDConsular ID (matrΓ­cula consular)
voterIdVOTER_IDVoter ID
ssnSSNUS Social Security card
itinITINUS ITIN (IRS)
otherOTHEROther 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 below.

Form Fields

FieldTypeRequiredDescription
filefileYesThe document image β€” pdf, jpg, jpeg, or png, max 10 MB
idNumberstringNoThe document number printed on the document
issuerstringNoIssuing authority or state
expirationDatestringNoFormat 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 return 404. Read a session's outcome from the DocumentUpdatedEvents webhook, and the resulting verdict from kycVerdict on 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

StatusDescription
PENDINGDocument uploaded, awaiting verification
VERIFIEDDocument accepted β€” compliance level may upgrade
REJECTEDDocument 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 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 and Sandbox Testing β€” certain name/address combinations trigger specific compliance behaviors (approval, hold, rejection).