Inyo

Server-to-Server Verification

When you already have a capture UI — or you are verifying images that were collected earlier — post them directly and get the result back in the response. No session, no widget, no customer link.


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 frontImage=@front.jpg \
  --form backImage=@back.jpg \
  --form selfieImage=@selfie.jpg

Returns 201 with the same normalized result the widget produces.

Idempotency-Key is required

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

This endpoint runs document extraction, the authenticity review, sanctions screening and the face checks synchronously, so a single call can take several seconds. Long enough that a client timeout is an ordinary event rather than an exotic one — and a blind retry would run all of that a second time, and bill you for it.

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 result.

SituationResponse
First call with a key201 with the result
Same key, first call finished201 with that verification's current result — not a frozen copy, so a later review decision is reflected
Same key, first call still running409 carrying the sessionId, so you can poll GET /v1/sessions/{session_id} for the outcome
Same key, different tenantUnrelated — keys are scoped to your tenant

The 409 matters more than it looks. If your request timed out you never received a sessionId, so you have no way to ask about the work already in progress. The conflict response hands it back:

{
  "detail": {
    "code": "IDEMPOTENCY_KEY_IN_FLIGHT",
    "message": "A request with this Idempotency-Key is still being processed. Poll GET /v1/sessions/{sessionId} for the outcome.",
    "sessionId": "81e04c30-2620-42f1-a3e0-68aef1f84043"
  }
}

So the correct behaviour on a timeout is: retry the same call with the same key. You will get either the result or a 409 telling you where to look. Never retry with a fresh key — that starts a second billed verification of the same person.

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.

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

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.

A missing, unreadable, or non-barcode back is a silent no-op: OCR values stand and no check changes. There is no downside to sending it, and a measurable accuracy gain when it decodes. 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 score. If you have a review threshold configured, there is nothing for it to compare against, and an unmeasured verification is treated as unmeasured, not confident — so it routes to in_review rather than auto-approving. Send a selfie, or leave the review threshold unset, if you need document-only verifications decided synchronously.


in_review Is Not a Final Answer

POST /v1/verifications returns the result synchronously, but a 201 response does not guarantee a terminal decision. status can be in_review, meaning an analyst has yet to decide.

This happens when you have configured a review threshold, or when held rejections are enabled and the checks rejected the document. With neither configured, every verification comes back decided.

When it does happen:

ChannelBehavior
WebhookWith a webhookUrl configured, the decided result is POSTed to it like any other result — see Result notifications
PollingGET /v1/sessions/{sessionId} returns the live status and result, updated in place, with result.manualReview naming who decided and why

Use sessionId from the response body as the identifier for both. No webhook is sent for the original synchronous answer — the 201 already delivered it. See Manual Review for the full picture.


Error Responses

StatusCauseHow to handle
401 / 403Missing or invalid token, or a token without the verifications scopeSee Authentication
422prefill is not valid JSON or violates the prefill schema (including document-number format)Fix the payload — the detail names the problem
422dataCheck=true with no prefillProvide a prefill payload to compare against
502The verification service was unavailableTransient — retry. This is an infrastructure failure, not a decline. Do not treat it as a negative outcome for the customer

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. Two settings are session-level by nature and have no effect here: the allowed document types and prefill.documentType locking both constrain what the widget offers a customer, so on this endpoint the document type is simply whatever the images turn out to be.

The differences that matter:

Hosted widgetServer-to-server
Capture qualityGuided, with live framing and glare feedbackYours to control
Failed captureCustomer is coached and retriesReturns a result; retrying is your decision
Result deliveryWebhook or signed redirectThe response body, plus later changes by webhook
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