Inyo

Verification Sessions

A session represents one identity verification. Creating it returns a widgetUrl you deliver to your customer, and a sessionId you use to correlate the result.


Create a Session

Endpoint: POST /v1/sessions
Authentication: Bearer token with the sessions scope

curl --request POST \
  --url https://{FQDN}/v1/sessions \
  --header "Authorization: Bearer $ACCESS_TOKEN" \
  --header 'Content-Type: application/json' \
  --data '{
  "userRef": "user-123",
  "language": "es",
  "delivery": { "mode": "webhook" }
}'
{
  "sessionId": "9f1c8e42-7c3e-4f2b-9d7a-2b1e5c8f4a10",
  "status": "pending",
  "widgetUrl": "https://{FQDN}/verify/8Kd2mQ…"
}

Request Fields

FieldTypeRequiredDescription
userRefstring (1-255)YesYour identifier for the person being verified. Echoed back in every result
prefillobjectNoKnown data about the person β€” see Prefill
dataCheckbooleanNofalse (default) verifies the document as presented. true additionally compares extracted data against prefill β€” see Data Checks
deliveryobjectNoHow the result reaches you. Defaults to {"mode": "webhook"}
languagestringNoWidget language, xx or xx-XX (e.g. en, pt, es, pt-BR). Falls back to your configured default
maxCaptureAttemptsinteger (1-10)NoOverrides your tenant default for this session only. Applies per capture type β€” this many document attempts and this many selfie attempts

Response Fields

FieldDescription
sessionIdUse this to correlate results and to call GET /v1/sessions/{sessionId}
statusAlways pending on creation
widgetUrlThe customer-facing link. Contains a single-use code valid for 48 hours

Prefill

prefill carries what you already know about the person. It has two distinct effects.

FieldEffect
documentTypepassport, drivers_license, or identity_card. Locks the widget to this document β€” the type-selection screen is skipped, and a different document is rejected. Omit it to let the customer choose
documentNumberValidated for format at this call (see below), and available for cross-checking
issuingStateThe subdivision only β€” a US state code or name. Sharpens format validation for a licence. Requires issuingCountry: PA is ParΓ‘ as readily as Pennsylvania, so a subdivision sent on its own is rejected with 422. A country code here names nothing and is never compared against the document, because a passport or identity card evidences no subdivision at all
issuingCountryISO 3166-1 alpha-3 β€” three letters. The only field that names the issuing jurisdiction, and the one your accepted jurisdictions are checked against. A code we cannot place resolves to nothing, so a session declaring one is judged on the document alone
nationalityISO 3166-1 alpha-3 β€” three letters. Available for cross-checking against the document. Never resolves the issuing jurisdiction β€” nationality is not issuance, on any document type
firstName, lastNameAvailable for cross-checking
dateOfBirthYYYY-MM-DD. Available for cross-checking

Every prefill field is a string of at most 255 characters. Send a field only when you have a value for it: a blank or whitespace-only value is rejected rather than ignored, so that a field you did not supply is never reported as one the document disagreed with.

{
  "userRef": "user-123",
  "prefill": {
    "documentType": "drivers_license",
    "issuingCountry": "USA",
    "issuingState": "PA",
    "documentNumber": "31612967",
    "firstName": "Ana",
    "lastName": "Silva",
    "dateOfBirth": "1988-03-04"
  }
}

Prefill fields other than documentType do not change the decision unless you set dataCheck: true. Without it, the values are carried for comparison and reported in the result, but a mismatch does not route the session anywhere.

prefill.documentNumber is validated at this call, however: a number a rule can refute β€” a failed check digit, or a value no jurisdiction could issue β€” is rejected with 422 rather than accepted and failed later. A number that merely matches none of the shapes we hold for its jurisdiction is not refused; it is carried, and the result reports document_number_valid as undetermined. Check a number before you get here with the validator endpoint, which answers with the same three-valued verdict.


Data Checks

Set dataCheck: true to verify that the document belongs to the person you expected β€” not just that the document is genuine.

{
  "userRef": "user-123",
  "dataCheck": true,
  "prefill": {
    "firstName": "Ana",
    "lastName": "Silva",
    "dateOfBirth": "1988-03-04"
  }
}

With dataCheck: true:

  • Extracted data is compared field by field against the prefill payload.
  • The result carries prefillComparison (per-field outcomes, including fuzzy name-match scores) and prefillMismatches (the field names that disagreed).
  • A mismatch adds a failing document_data_match check and is reported on the result.

Names are compared with fuzzy matching against a configurable similarity threshold, so ordinary spelling and transliteration variance does not create false mismatches. dataCheck: true without a prefill payload is a 422 β€” there would be nothing to compare against.


Delivery

ModeFieldsBehavior
webhook (default)β€”The result is POSTed to your configured webhookUrl
redirectredirectUrl (required)The customer is returned to your URL with the outcome and a signature
{ "delivery": { "mode": "redirect", "redirectUrl": "https://you.example/kyc-done" } }

Requesting webhook mode without a webhookUrl configured for your tenant is a 422. See Receiving Results for signature verification in both modes.


Error Responses

All of these return 422 with errorCode: "validation_error"; errors[] names the field and the cause:

CauseNotes
delivery.mode is webhook but no webhookUrl is configuredAsk Inyo to register your endpoint, or use redirect mode
delivery.redirectUrl missing in redirect modeRequired whenever mode is redirect
documentType is not enabled for your tenantThe message lists the document types you may request
The document's issuing jurisdiction is not accepted for that document typeThe message lists the jurisdictions you accept
prefill.documentNumber was refuted by its ruleOnly a provable failure β€” a check digit, or a value no jurisdiction could issue. The message names the rule, never the number
dataCheck: true with no prefill payloadProvide at least one comparable prefill field
A prefill field is blank or whitespace-onlyOmit the field instead. A blank is not "no value" β€” it would be compared against the document and reported as a mismatch
A prefill field is longer than 255 charactersSame bound as userRef
prefill.issuingState sent with no prefill.issuingCountryA subdivision names nothing on its own β€” the same code exists in more than one country
issuingCountry or nationality is not three lettersCountry names and two-letter codes are refused β€” send the ISO 3166-1 alpha-3 code. Specimen and stateless codes (UTO, XXA, XKX) are accepted, so the check is the shape, not a country list
dateOfBirth is not a real date in YYYY-MM-DD1987-13-45 matches the shape and is not a date
An unrecognised field name anywhere in the requestRefused rather than ignored, so a misspelled dataCheck cannot silently turn the comparison off. Both camelCase and snake_case spellings of real fields are accepted
userRef or a prefill field contains a NUL (\u0000)The value cannot be stored. Accents, punctuation and whitespace within a value are all fine β€” this is the one character that is not

Authentication and authorization failures return 401 or 403 β€” see Authentication.


Retrieve a Session

Endpoint: GET /v1/sessions/{sessionId}
Authentication: Bearer token with the sessions scope

This is the authoritative record of a widget session. Read it when you need a guarantee rather than a push, to recover a webhook you may have missed, or to read the outcome of a session that went to manual review.

A verification created with POST /v1/verifications is not served here β€” it has a read of its own at GET /v1/verifications/{verificationId}, reachable with the verifications scope. Passing one here returns 404.

curl --request GET \
  --url https://{FQDN}/v1/sessions/$SESSION_ID \
  --header "Authorization: Bearer $ACCESS_TOKEN"
{
  "sessionId": "9f1c8e42-7c3e-4f2b-9d7a-2b1e5c8f4a10",
  "userRef": "user-123",
  "status": "completed",
  "step": "done",
  "deliveryMode": "webhook",
  "result": { "…": "the normalized result, updated in place" },
  "createdAt": "2026-07-31T14:02:11.481Z",
  "updatedAt": "2026-07-31T14:04:57.902Z"
}
FieldDescription
statuspending, completed, or failed β€” the lifecycle position, never a verdict. The outcome, where you get one, is result.decision
stepHow far the customer got: document_front, document_back, selfie, or done
deliveryModewebhook or redirect β€” how this session's outcome reaches you, chosen at creation
resultThe full normalized result once available, null before then. Updated in place when an analyst decides

Sessions are strictly tenant-scoped. Another tenant's sessionId returns 404 β€” never a partial disclosure.


Next Steps