Inyo

Getting Started

This guide walks you through one complete identity verification on the Inyo sandbox. By the end you will have created a session, verified a document and selfie through the hosted widget, and received a signed result on your own endpoint.


Prerequisites

  • Sandbox credentials issued by Inyo during onboarding:
    • client_id and client_secret for the client-credentials grant
    • Your sandbox base URL, and confirmation that your egress IP ranges are allowlisted
    • A webhook_secret — used to verify the signature on delivered results
  • A webhook endpoint, publicly reachable over HTTPS, registered with Inyo as your webhook_url. During development a tunnel (ngrok, Cloudflare Tunnel) works; alternatively use redirect delivery and skip the webhook entirely.
  • A phone with a camera for the widget. Camera access requires a secure context, so open the widget over HTTPS — desktop browsers work too.
  • A REST client (cURL, Postman, or similar).

No credentials yet? Get in touch with our sales team to request sandbox access.


Step 1: Get an Access Token

curl --request POST \
  --url https://{FQDN}/oauth/token \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data grant_type=client_credentials \
  --data client_id=$KYC_CLIENT_ID \
  --data client_secret=$KYC_CLIENT_SECRET
{
  "access_token": "eyJhbGciOiJSUzI1NiIs…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "sessions verifications"
}

Store access_token and reuse it until it expires — see Authentication for refresh handling.


Step 2: Create a Verification Session

user_ref is your own identifier for the person being verified. It is echoed back in every result, so use whatever your system already keys users by.

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

Persist session_id against your user now — it is how you will correlate the incoming result.

delivery.mode defaults to webhook. If you have no webhook_url configured, this call returns 422; use {"mode": "redirect", "redirect_url": "https://you.example/kyc-done"} instead.


Step 3: Send the Customer to the Widget

Open widget_url in the customer's browser or a native webview, or deliver it by SMS or email. The link carries a single-use code and is valid for 48 hours.

The customer selects a document type, photographs the document, takes a selfie, and sees the outcome. You write no camera code — see Widget Delivery for webview and branding details.


Step 4: Receive the Signed Result

When the verification finishes, Inyo POSTs the result to your webhook_url with an X-Inyo-Signature header:

POST /kyc-result HTTP/1.1
Content-Type: application/json
X-Inyo-Signature: 4c1f9a…
{
  "session_id": "9f1c8e42-7c3e-4f2b-9d7a-2b1e5c8f4a10",
  "user_ref": "user-123",
  "status": "approved",
  "auto_status": "approved",
  "document": {
    "type": "Passport",
    "number": "A12345678",
    "issuing_state": "USA",
    "issuing_country": "USA",
    "expiration_date": "2033-09-30"
  },
  "person": {
    "first_name": "ALEX",
    "last_name": "MORGAN",
    "date_of_birth": "1988-03-04",
    "nationality": "USA"
  },
  "checks": [
    { "name": "document_readable", "passed": true, "score": null, "detail": "all core fields extracted" },
    { "name": "mrz_valid", "passed": true, "score": null, "detail": "all check digits valid" },
    { "name": "document_not_expired", "passed": true, "score": null, "detail": "expiration_date 2033-09-30" },
    { "name": "face_match", "passed": true, "score": 98.7, "detail": "CompareFaces similarity vs threshold 90.0" }
  ],
  "prefill_mismatches": [],
  "provider": "inyo"
}

Verify the signature before trusting the payload. It is an HMAC-SHA256 of the raw request body bytes, keyed with your webhook_secret:

import crypto from "node:crypto";

const expected = crypto
  .createHmac("sha256", process.env.KYC_WEBHOOK_SECRET)
  .update(rawBody)                       // raw bytes, before JSON.parse
  .digest("hex");

const signature = req.headers["x-inyo-signature"];
const valid =
  signature &&
  crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));

Parsing the body and re-serializing it changes whitespace and key order and will break the signature. Full details, including redirect mode, are on Receiving Results.

Respond 2xx to acknowledge. Inyo retries up to three times with backoff, then stops.


Step 5: Confirm Server-Side

GET /v1/sessions/{session_id} is the authoritative record. Use it to reconcile, to recover a dropped webhook, or whenever you need certainty rather than a push:

curl --request GET \
  --url https://{FQDN}/v1/sessions/$SESSION_ID \
  --header "Authorization: Bearer $ACCESS_TOKEN"
{
  "session_id": "9f1c8e42-7c3e-4f2b-9d7a-2b1e5c8f4a10",
  "user_ref": "user-123",
  "status": "approved",
  "step": "done",
  "delivery_mode": "webhook",
  "result": { "…": "the same normalized result" },
  "created_at": "2026-07-31T14:02:11.481Z",
  "updated_at": "2026-07-31T14:04:57.902Z"
}

Handle These Three Cases Before You Go Live

CaseWhy it mattersWhere to read
status is in_reviewNot a final answer — a human decides, and you are notified later. It can arrive by webhook and on a redirect.Manual Review
Two results for one sessionAn analyst decision or a quality-control reversal re-delivers the result. Apply the highest notified_at, not the latest arrival.Receiving Results
A check failed but you want the detailpassed is the outcome; per-tenant thresholds are quoted in each check's detail. Never gate on the ai_authenticity score.Checks & Decisions

Next Steps

PageWhat it covers
Verification SessionsPrefill, document-type locking, data checks, capture limits
Widget DeliveryWebview embedding, branding, localization
Server-to-Server VerificationUsing your own capture UI instead of the widget
Tenant ConfigurationThresholds, accepted documents and jurisdictions
Sandbox & Test DataDriving approved, declined, and in_review outcomes