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.
| Situation | Response |
|---|---|
| First call with a key | 201 with the result |
| Same key, first call finished | 201 with that verification's current result — not a frozen copy, so a later review decision is reflected |
| Same key, first call still running | 409 carrying the sessionId, so you can poll GET /v1/sessions/{session_id} for the outcome |
| Same key, different tenant | Unrelated — 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/sessionsdoes not take anIdempotency-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
| Field | Type | Required | Description |
|---|---|---|---|
userRef | text | Yes | Your identifier for the person being verified |
frontImage | file | Yes | Front of the document — the photo page of a passport, the front of a card |
backImage | file | No | Back of the document. For US licenses and state IDs this carries the barcode |
selfieImage | file | No | Omit to run document checks only — see Document-only verification |
dataCheck | text | No | true compares extracted data against prefill |
prefill | text | No | A 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:
| Channel | Behavior |
|---|---|
| Webhook | With a webhookUrl configured, the decided result is POSTed to it like any other result — see Result notifications |
| Polling | GET /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
| Status | Cause | How to handle |
|---|---|---|
401 / 403 | Missing or invalid token, or a token without the verifications scope | See Authentication |
422 | prefill is not valid JSON or violates the prefill schema (including document-number format) | Fix the payload — the detail names the problem |
422 | dataCheck=true with no prefill | Provide a prefill payload to compare against |
502 | The verification service was unavailable | Transient — 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 widget | Server-to-server | |
|---|---|---|
| Capture quality | Guided, with live framing and glare feedback | Yours to control |
| Failed capture | Customer is coached and retries | Returns a result; retrying is your decision |
| Result delivery | Webhook or signed redirect | The response body, plus later changes by webhook |
| Presentation-attack defense | Same checks | Same checks, but with no live capture context to draw on |
| Compliance evidence | Inyo retains the guided capture and optional selfie video | Only 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
- Checks & Decisions — the result payload in full
- Manual Review — handling
in_review - Receiving Results — webhook notifications for later changes
