---
description: >-
  Deliver the hosted Inyo KYC widget by link or native webview — bootstrap code lifetime, camera and secure-context requirements, capture retries, branding, and localization.
---

# Widget Delivery

The widget is a hosted web app that guides your customer through document and selfie capture. You do not build camera handling, framing guidance, or retry coaching — you deliver a URL.

***

### The URL

[`POST /v1/sessions`](sessions.md) returns:

```json
{ "widgetUrl": "https://{FQDN}/verify/8Kd2mQ…" }
```

The trailing segment is a **single-use bootstrap code**, not a session identifier. When the page loads, the browser exchanges it for a short-lived in-page token; the code itself grants nothing beyond starting that one verification.

| Property | Behavior |
| -------- | -------- |
| Lifetime | **48 hours** from session creation |
| Scope | One session. It exposes no tenant data and cannot be used to read a result |
| Expired link | The customer sees an expired-link message — create a new session |
| Already-completed session | Reopening a finished verification shows a completion message rather than starting over |

Because the link is time-limited, generate it when you are ready to send it — not in advance, in bulk.

***

### Three Ways to Deliver It

| Method | How | Good for |
| ------ | --- | -------- |
| **Redirect** | Send the customer's browser to `widgetUrl`, and use [redirect delivery](results.md#redirect-mode) to bring them back | Web onboarding flows |
| **Native webview** | Open `widgetUrl` in a system webview (`WKWebView`, Android `WebView`) with camera permission granted | Mobile apps that keep the customer in-app |
| **Link out** | Send the URL by SMS, WhatsApp, or email | Desktop or agent-assisted flows where the customer's phone has the better camera |

There is no JavaScript SDK, iframe API, or `postMessage` event stream. Completion is signalled server-side — by the [signed webhook](results.md#webhook-mode) or the [signed redirect](results.md#redirect-mode) — which is also what makes it trustworthy: the outcome your backend acts on never passes through the customer's browser unsigned.

***

### Camera Requirements

Camera access requires a **secure context**. In practice:

* Serve or open the widget over **HTTPS**. `http://localhost` and `http://127.0.0.1` also qualify, for local development.
* In a native webview, grant camera permission before loading the URL — the OS prompt inside a webview is easy for customers to miss.
* Capture is **camera-only** by default. A file picker appears only if file upload is enabled for your tenant, so a customer cannot upload a stored photo of a document unless you have asked us to allow it.

Images are capped at 10 MB per capture, which the widget's own encoding stays well within.

***

### What the Customer Sees

1. **Document type** — passport, driver's license, or identity card. Skipped when you set `prefill.documentType`, and limited to the types enabled for your tenant.
2. **Document capture** — a document-shaped mask (passport photo page and machine-readable zone, ID-1 card outline) with live framing, glare, and blur feedback. Two-sided documents prompt for the back.
3. **Confirmation** — extracted fields are shown so the customer can spot a misread before continuing.
4. **Selfie** — a face oval with capture guidance.
5. **Result** — approved, declined, or an under-review message. In redirect mode the customer is returned to your URL instead.

***

### Capture Retries

A capture that cannot be verified — too blurry to read, a photo of a screen, a face that does not match — returns the customer to the relevant step with specific feedback rather than failing the session outright.

| Behavior | Detail |
| -------- | ------ |
| Attempt limit | Your tenant default (3 unless configured otherwise), overridable per session with `maxCaptureAttempts` |
| On each failure | The customer sees targeted feedback for the specific problem and retries |
| On exhaustion | The session is **declined**, and you receive that result like any other, carrying a `capture_attempts` check that names the limit reached |

Raise the limit for a forgiving consumer flow; lower it where repeated failed captures are themselves a fraud signal.

***

### Branding and Localization

Both are configured per tenant at onboarding.

**Branding** applies your logo, company name, and primary, background, and text colors to the widget chrome, so the flow reads as part of your product. The **"Powered by Inyo 360" footer always renders** and is intentionally not configurable.

**Language** comes from the session's `language` field, falling back to your configured default. English, Portuguese, and Spanish ship complete; any `xx` or `xx-XX` code is accepted, and any string without a translation falls back to English individually rather than dropping the whole language. Every string the widget renders is served from Inyo's copy store, so wording — including retry feedback and result screens — can be adjusted for your tenant without a release on your side.

***

### Human-Presence Check

The widget may present a brief Cloudflare challenge when it starts, to keep automated traffic from consuming verifications. It is invisible or near-invisible to real customers and requires nothing from your integration.

***

### Tracking Progress

You do not need to watch the customer's progress — the result is pushed to you — but [`GET /v1/sessions/{sessionId}`](sessions.md#retrieve-a-session) exposes a `step` field (`document_front`, `document_back`, `selfie`, `done`) if you want to show an in-progress state in your own UI or measure drop-off between steps.

***

### Next Steps

* [Receiving Results](results.md) — signature verification for both delivery modes
* [Server-to-Server Verification](server-to-server.md) — the alternative when you own the capture UI
