Inyo

Server-to-Server Verification

When you already have a capture UI — or you are verifying images that were collected earlier — post them directly. No session, no widget, no customer link.

The call accepts the verification and answers with its identifier. The assessment runs behind the response.


Create a Verification

Endpoint: POST /v1/verifications
Authentication: Bearer token with the verifications scope
Content-Type: multipart/form-data

curl --request POST \
  --url https://{FQDN}/v1/verifications \
  --header "Authorization: Bearer $ACCESS_TOKEN" \
  --header "Idempotency-Key: $(uuidgen)" \
  --form userRef=user-123 \
  --form [email protected] \
  --form [email protected] \
  --form [email protected]

Returns 201:

{
  "sessionId": "81e04c30-2620-42f1-a3e0-68aef1f84043",
  "status": "pending"
}

That is the whole body. There is no result in it — document extraction, the authenticity review, sanctions screening and the face checks all run after you are answered. Keep sessionId: it is how you read the outcome and how you recognise the webhook when it arrives.


Getting the Outcome

Two channels, and you can use both.

ChannelHow
WebhookWith a webhookUrl configured for your account, the outcome is POSTed to it when the verification reaches a terminal state — see Receiving Results
ReadGET /v1/verifications/{verificationId} returns the current state, for confirming or recovering an outcome

A verification is accepted whether or not you have a destination registered — it runs, completes, and its outcome is retrievable either way. A webhook destination is arranged with your Inyo contact rather than set on the API, so this endpoint cannot report whether one exists. Register one: a tight retrieval loop is not the intended use of the read.


Retrieve a Verification

Endpoint: GET /v1/verifications/{verificationId}
Authentication: Bearer token with the verifications scope

curl --request GET \
  --url https://{FQDN}/v1/verifications/$VERIFICATION_ID \
  --header "Authorization: Bearer $ACCESS_TOKEN"
{
  "sessionId": "81e04c30-2620-42f1-a3e0-68aef1f84043",
  "userRef": "user-123",
  "status": "completed",
  "step": "done",
  "result": { "…": "the normalized result, updated in place" },
  "createdAt": "2026-09-14T14:02:11.481Z",
  "updatedAt": "2026-09-14T14:02:58.902Z"
}
FieldDescription
statuspending, completed, failed, or errored — the lifecycle position, never a verdict. The outcome, where there is one, is result.decision
resultThe full normalized result once the assessment finishes, null before then. Updated in place when an analyst decides

This path serves your verifications only. A widget session's identifier returns 404 here, and a verification's identifier returns 404 at GET /v1/sessions/{sessionId} — each door reads at its own path, under the scope that door issues.

The verification record carries no deliveryMode. On a widget session that field distinguishes a push from a browser redirect; a verification has neither, and reporting webhook on one would read as a promise that a destination exists when that is your configuration to know, not ours to assert.


Idempotency-Key is required

Every call must carry an Idempotency-Key header. A request without one is rejected with 422.

An assessment is real work — extraction, the authenticity review, screening and the face checks all run against external providers. If a response is lost in transit and you retry blindly, the key is what stops every one of them running a second time and leaving you two verification records for one person.

Use a value your own code can reproduce for the same logical verification: a UUID you generate and store, or an identifier from your system. Do not derive it from something that repeats, such as userRef alone — two genuine verifications of the same person would collide, and the second would return the first one's identifier.

SituationResponse
First call with a key201 with a new sessionId
Same key, same request201 with the same sessionId, whatever state the work has reached. Nothing is started twice
Same key, different images or subject422 IDEMPOTENCY_KEY_REUSED. Replaying it would hand back somebody else's verification, so the refusal is deliberate

A replay no longer distinguishes work in progress from work finished, because it no longer needs to: the first response already gave you the identifier, so a retry has nothing left to tell you that you do not have. Read status if you want to know where it got to.

POST /v1/sessions does not take an Idempotency-Key. Its response contains a single-use widget link that cannot be reissued, so a retry there creates a new session; a duplicate costs an unused link rather than a repeated verification.

When a fresh key is the right move

Retrying with the same key is what you want for a lost or failed response — it is free, and it returns the identifier you missed.

Mint a new key in one case: the verification ended errored. That is a platform fault on our side, not a decision about your customer. The old key is spent on it permanently and will keep returning that ending, so a new key is what runs a new verification of the same person.

Outside that, a fresh key starts a second verification of somebody you have already verified.


Form Fields

FieldTypeRequiredDescription
userReftextYesYour identifier for the person being verified
frontImagefileYesFront of the document — the photo page of a passport, the front of a card
backImagefileNoBack of the document. For US licenses and state IDs this carries the barcode
selfieImagefileNoOmit to run document checks only — see Document-only verification
dataChecktextNotrue compares extracted data against prefill
prefilltextNoA JSON string with the same schema as session prefill

Note that prefill here is a JSON string inside a multipart field, not a nested object:

  --form 'prefill={"firstName":"Ana","lastName":"Silva","dateOfBirth":"1988-03-04"}' \
  --form dataCheck=true

Every field is read before you are answered, so a malformed prefill is still a 422 on the call itself rather than something you discover later.


Send the Back of the Card

For US driver's licenses and state IDs, the back carries a PDF417 barcode encoding the cardholder data. Inyo decodes it on receipt, and because the barcode is self-verifying — the symbology carries its own error correction — it outranks visual-zone OCR for the fields it contains. Decoding is rotation-independent, so an upside-down back still reads.

OCR values stand when the back does not decode, so there is no downside to sending it and a measurable accuracy gain when it does. A back too soft to read publishes document_barcode_valid as not_evaluated with reason no_barcode, which costs nothing; a back carrying a barcode that is not an identity record publishes it as failed. Passports carry a machine-readable zone on the photo page instead, so frontImage alone is sufficient for them.


Document-only Verification

Omit selfieImage to verify the document without biometrics. The document checks run — readability, format, expiry, authenticity, jurisdiction — and no face checks appear in checks[].

One consequence to plan for: a document-only verification carries no biometric evidence and is decided on its document checks alone. If your risk model needs the face compared to the document, send a selfie.


in_review Is Not a Final Answer

Under managed decisioning decision can be in_review, meaning an analyst has yet to decide. status is completed either way — it is the lifecycle position, not a verdict.

Which score bands are sent for review is set for your account, so handle inconclusive and in_review both on this entry point.

The assessment's own outcome and the analyst's later decision reach you the same way: a webhook if you have a destination, and the read either way. There is no longer a first answer that arrives differently from the ones after it. See Manual Review for the full picture.


When the Platform Fails

A verification whose assessment cannot run ends errored. It carries whatever checks completed before the fault, publishes no decision, and reaches no reviewer — there is nothing anyone can act on.

errored is distinct from failed, and the difference is who retries:

StatusMeaningWhat to do
failedThe capture was never usable — on the widget, the customer ran out of attemptsThe person tries again
erroredA fault on our side stopped the assessmentYou retry the call, with a new Idempotency-Key

You never have to read the check rows to tell them apart.


Error Responses

StatusCauseHow to handle
401 / 403Missing or invalid token, or a token without the verifications scopeSee Authentication
404No verification of yours carries that id — including a widget session's idRead a widget session at GET /v1/sessions/{sessionId} instead
422prefill is not valid JSON or violates the prefill schema (including document-number format)Fix the payload — errors[] names each field and why it was refused
422dataCheck=true with no prefillProvide a prefill payload to compare against
422errorCode: "IDEMPOTENCY_KEY_REUSED" — the key was already used for a different requestUse a new key
502We could not accept the verificationTransient — retry with the same key. Nothing was decided about your customer
503Too many verifications are already in progressTransient — retry with the same Idempotency-Key. Nothing was started, so the key is still free and the retry is not a second verification. Wait the number of seconds in the Retry-After header first

An assessment that fails after you were answered does not produce an error response — you already have a 201. It ends the verification errored, which you see on the read or the webhook.


Widget or Server-to-Server?

Both entry points share the same pipeline and the same result shape, and your thresholds, review routing, accepted jurisdictions, and enrichment options apply identically. The allowed document types are session-level by nature and have no effect here — they constrain what the widget offers a customer. prefill.documentType does have an effect: it is the type document_type_match judges the images against, so declaring one and sending a different document fails that check. Omit it and the check publishes not_evaluated, and the document type is then whatever the images turn out to be with nothing comparing it.

The differences that matter:

Hosted widgetServer-to-server
Capture qualityGuided, with live framing and glare feedbackYours to control
Failed captureCustomer is coached and retriesEnds failed; retrying is your decision
Result deliveryWebhook or signed redirectWebhook when configured, and the read either way
Reading the recordGET /v1/sessions/{sessionId}GET /v1/verifications/{verificationId}
Presentation-attack defenseSame checksSame checks, but with no live capture context to draw on
Compliance evidenceInyo retains the guided capture and optional selfie videoOnly the images you send

The retry difference is about coaching a live customer — it does not change what your configuration means or how the decision is reached.


Next Steps