मुख्य सामग्री पर जाएं

LLM के साथ बनाएं

Markdown

पूरा इंटीग्रेशन बनाने के लिए नीचे दिए गए किसी एक प्रॉम्प्ट को Claude Code, Codex या Cursor में पेस्ट करें।

# Integrate Unico — Human Token

You are integrating **Unico Human Token** into my application. The user completes a biometric verification and Unico returns a **stable, anonymous identifier** derived from their biometric data — no government ID, no name, no contact info, no document required. The same person always receives the same `humanToken`, regardless of device or session.

Use this prompt for: duplicate-person detection across different onboarding attempts, pseudonymous user recognition (loyalty, fraud rings), linkage across sessions without storing any PII, detecting the same biometric appearing under different identities.

## Choosing a flow

There are five flows. Fill in `Flow` below to select one:

| Flow | Capabilities | Who captures the selfie | Liveness | Result delivery |
|---|---|---|---|---|
| `api+humantoken` | Human Token | Your application (`imageBase64`) | **No** | Synchronous — returned in the response body |
| `api+liveness+humantoken` | Liveness + Human Token | Your application (`imageBase64`) | **Yes** | Synchronous — returned in the response body |
| `humantoken` | Human Token (Unico Journey) | Unico (via Web SDK or redirect) | **Yes** | Async — poll `GET /client/v1/process/{processId}` |
| `api+liveness+humantoken+revalidation` | Revalidation (API Only) | Your application (`imageBase64`) | **Yes** | Synchronous — returned in the response body |
| `humantoken+revalidation` | Revalidation (Unico Journey) | Unico (via Web SDK or redirect) | **Yes** | Async — poll `GET /client/v1/process/{processId}` |

Use `api+humantoken` or `api+liveness+humantoken` (API Only) when you already have a base64 selfie from your own capture pipeline and want an immediate synchronous result without a user-facing redirect. Use `humantoken` (Unico Journey) when you want Unico to handle the entire biometric capture experience.

Use the **revalidation** flows (`api+liveness+humantoken+revalidation` or `humantoken+revalidation`) when you want to verify whether a new biometric capture matches a previously obtained `humanToken`. These flows require a `references` array containing the prior `humanToken` and return a `humanTokenMatch` result (`"true"` or `"false"`) instead of a new `humanToken.value`.

## My application context

<my_stack>
Stack: [framework + language — e.g. Next.js 15 + TypeScript, Django, Go, Rails]
Flow: [api+humantoken | api+liveness+humantoken | humantoken | api+liveness+humantoken+revalidation | humantoken+revalidation]
Database: [Postgres | MySQL | Mongo | other]
</my_stack>

> **Flow options:**
> - `api+humantoken` — API Only, no liveness. Your backend sends a base64 selfie and gets the `humanToken` synchronously.
> - `api+liveness+humantoken` — API Only with liveness. Same as above, but includes a liveness check on the image.
> - `humantoken` — Unico Journey. The user is sent to the Unico platform (web SDK or redirect) for biometric capture with liveness, and the result is retrieved via polling.
> - `api+liveness+humantoken+revalidation` — API Only revalidation with liveness. Your backend sends a base64 selfie and a prior `humanToken` reference; returns whether they match.
> - `humantoken+revalidation` — Unico Journey revalidation. Same as the Journey flow, but requires a prior `humanToken` reference and returns a match result.

## Base URL

| Environment | Base URL |
|---|---|
| Production | `https://humantoken.unico.app` |

All endpoints use API key authentication via header:
```
x-unico-accesskey: <your_access_key>
```

Store the API key as `UNICO_ACCESS_KEY` in your secrets manager. Never expose it to the browser or commit it to source control.

## Step 1 — Create a process (backend)

### API Only — `api+humantoken` (no liveness)

Your backend sends the base64 selfie directly. The `humanToken` is returned synchronously — no SDK, redirect, or polling needed. There is no liveness check.

Prepare the image to the [capture standard](https://developer.unico.io/developers/sdks-and-tools/capture-standard): one frontal face, JPEG, PNG or WebP, at most 800 KB, sent as plain base64 without a `data:image/...;base64,` prefix.

```bash
curl -X POST https://humantoken.unico.app/client/v1/process \
-H "x-unico-accesskey: $UNICO_ACCESS_KEY" \
-H "Content-Type: application/json" \
-d '{
"flow": "api+humantoken",
"imageBase64": "<base64-encoded-selfie>",
"companyBranchId": "<your-branch-id>"
}'
```

Response:
```json
{
"id": "6ed74799-4c2f-4023-b43f-a73cf7c847b2",
"state": "PROCESS_STATE_FINISHED",
"data": {
"humanToken": {
"value": "3f9a1c7e5b2d4086a1e3c5f7092b4d6e8a0c2e4f6183a5c7e9b1d3f5072a4c6e",
"status": "completed"
}
}
}
```

After this call, apply the result using the same logic as [Step 4](#step-4--apply-the-result-to-my-application). Skip Steps 2 and 3 — there is nothing to present to the user and no polling required.

---

### API Only — `api+liveness+humantoken` (with liveness)

Same as above, but includes a liveness check on the image. The selfie must be captured with liveness-compatible quality.

```bash
curl -X POST https://humantoken.unico.app/client/v1/process \
-H "x-unico-accesskey: $UNICO_ACCESS_KEY" \
-H "Content-Type: application/json" \
-d '{
"flow": "api+liveness+humantoken",
"imageBase64": "<base64-encoded-selfie>",
"companyBranchId": "<your-branch-id>"
}'
```

Response format is the same as `api+humantoken` above. Apply the result using [Step 4](#step-4--apply-the-result-to-my-application). Skip Steps 2 and 3.

---

### API Only — `api+liveness+humantoken+revalidation` (revalidation with liveness)

Your backend sends a base64 selfie along with a `references` array containing the prior `humanToken`. The response tells you whether the new capture matches the referenced token.

```bash
curl -X POST https://humantoken.unico.app/client/v1/process \
-H "x-unico-accesskey: $UNICO_ACCESS_KEY" \
-H "Content-Type: application/json" \
-d '{
"flow": "api+liveness+humantoken+revalidation",
"imageBase64": "<base64-encoded-selfie>",
"companyBranchId": "<your-branch-id>",
"references": [
{
"referenceType": "REFERENCE_TYPE_HUMAN_TOKEN",
"referenceContent": "3f9a1c7e5b2d4086a1e3c5f7092b4d6e8a0c2e4f6183a5c7e9b1d3f5072a4c6e"
}
]
}'
```

Response:
```json
{
"id": "a1b2c3d4-0000-0000-0000-abcdef012345",
"state": "PROCESS_STATE_FINISHED",
"data": {
"humanTokenMatch": "true"
}
}
```

`data.humanTokenMatch` is `"true"` if the new capture matches the referenced `humanToken`, or `"false"` if it does not. Apply the revalidation result using [Step 4](#step-4--apply-the-result-to-my-application). Skip Steps 2 and 3.

---

### Unico Journey — `humantoken`

The user completes the biometric capture on the Unico platform. The response returns the process `id`, a `token` for the Web SDK, and a `link` for the journey.

```bash
curl -X POST https://humantoken.unico.app/client/v1/process \
-H "x-unico-accesskey: $UNICO_ACCESS_KEY" \
-H "Content-Type: application/json" \
-d '{
"flow": "humantoken",
"callbackUri": "https://yourapp.com/human-token/callback",
"companyBranchId": "<your-branch-id>"
}'
```

Response:
```json
{
"id": "a1b2c3d4-0000-0000-0000-abcdef012345",
"state": "PROCESS_STATE_CREATED",
"token": "eyJhbGciOiJSUzI1NiJ9...",
"link": "https://humantoken.unico.app/..."
}
```

**Persist `id`** — needed to retrieve the result. `token` initializes the Web SDK. `link` is the URL for native app redirects. Proceed to Step 2.

---

### Unico Journey — `humantoken+revalidation` (revalidation)

Same as the Journey flow, but requires a `references` array containing the prior `humanToken`. The result (retrieved via polling) will contain `data.humanTokenMatch` instead of `data.humanToken.value`.

```bash
curl -X POST https://humantoken.unico.app/client/v1/process \
-H "x-unico-accesskey: $UNICO_ACCESS_KEY" \
-H "Content-Type: application/json" \
-d '{
"flow": "humantoken+revalidation",
"callbackUri": "https://yourapp.com/human-token/callback",
"companyBranchId": "<your-branch-id>",
"references": [
{
"referenceType": "REFERENCE_TYPE_HUMAN_TOKEN",
"referenceContent": "3f9a1c7e5b2d4086a1e3c5f7092b4d6e8a0c2e4f6183a5c7e9b1d3f5072a4c6e"
}
]
}'
```

Response format is the same as the `humantoken` Journey flow above (`id`, `state`, `token`, `link`). Proceed to Step 2. The revalidation result is retrieved via polling in Step 3b.

## Step 2 — Install the SDK that matches my stack

> **Only for Flow = `humantoken` or `humantoken+revalidation` (Unico Journey).** Skip this step for API Only flows.

| Stack | Package | Install |
|---|---|---|
| Web — SDK (React, Vue, Next.js, Nuxt, Svelte, vanilla JS) | `idpay-b2b-sdk` (min v2.2.3) | `npm install idpay-b2b-sdk` |
| Web — Redirect | none | No package needed |
| Android (Kotlin) | WebView (native) | No install — uses `android.webkit.WebView` (API 26+) |
| iOS (Swift / SwiftUI) | WebKit (native) | No install — system framework (iOS 14.3+) |
| Flutter | `webview_flutter` + `permission_handler` | See `pubspec.yaml` dependencies below |
| Backend-only | none | Call REST directly |

> **Important for web:** Register your HTTPS domain with the Unico team before going live. Do **not** embed the SDK via a plain `<iframe>` tag or inside a WebView.

## Step 3 — Present the flow to the user

> **Only for Flow = `humantoken` or `humantoken+revalidation` (Unico Journey).** Skip this step for API Only flows.

### Web — SDK

Use this when you want to embed the biometric capture directly inside your page without navigating the user away.

```javascript
import { ByUnicoSDK } from "idpay-b2b-sdk";

// 1. Initialize with the process token (pre-loads assets, detects device compatibility)
// `token` comes from the create-process response
const initResult = await ByUnicoSDK.init({ token: processToken });

if (initResult?.statusPreValidation !== "DEVICE_SUPPORTED") {
// Handle incompatible device — do not proceed
return;
}

// 2. Open the Human Token flow
ByUnicoSDK.open({
onFinish: (result) => {
// result = { transaction, type }
// type is 'FINISH' (completed) or 'CLOSE' (user dismissed)
if (result.type === 'FINISH') {
// Flow completed — fetch the result from your backend using the process id
} else {
// User closed the flow — handle as canceled
}
}
});
```

### Web — Redirect

Use this when you prefer not to install a JS SDK, or when you want the simplest possible integration. The user is sent to the Unico platform and returned to a callback URL you own. No SDK package required.

Pass `callbackUri` when creating the process. The Unico Journey redirects the user there when the flow ends.

**Option A — Full-page redirect** (recommended; mobile-friendly):

```javascript
// Receive `link` from your backend and redirect the user
window.location.href = processLink;
```

**Option B — New tab** (keeps your main page active):

```javascript
// Call window.open inside a user gesture (e.g. button click) to avoid popup blockers
const youTab = window.open(processLink, "_blank");

// Poll until the tab navigates back to your callback URL
const poll = setInterval(() => {
try {
if (youTab.closed) {
clearInterval(poll);
// User closed the tab before completing — handle as canceled
return;
}
if (youTab.location.href.startsWith("https://yourapp.com/human-token/callback")) {
clearInterval(poll);
youTab.close();
// Trigger your backend to retrieve the result (see Step 3b)
}
} catch {
// Cross-origin frames throw until the tab is back on your domain — ignore
}
}, 500);
```

**Handling the callback:**

When the flow ends (completed, expired, or canceled), the Unico Journey redirects to your `callbackUri` with `id` appended as a query parameter:

```
https://yourapp.com/human-token/callback?id=a1b2c3d4-0000-0000-0000-abcdef012345
```

Read `id` on the callback page and send it to your backend to retrieve the result:

```javascript
const params = new URLSearchParams(window.location.search);
const processId = params.get("id");
// Send processId to your backend → Step 3b
```

> The redirect happens for **all** outcomes — always retrieve the process state from the API. Never assume success based on the redirect alone.
> Register your HTTPS callback domain with the Unico team before going live.

### Android (WebView)

Requires INTERNET and CAMERA permissions. Android 8+ (API 26+).

```kotlin
// AndroidManifest.xml:
// <uses-permission android:name="android.permission.INTERNET" />
// <uses-permission android:name="android.permission.CAMERA" />

class HumanTokenActivity : ComponentActivity() {
private lateinit var webView: WebView
private val callbackUri = "https://yourapp.com/human-token/callback"

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val url = intent.getStringExtra("PROCESS_URL") ?: return finish()

webView = WebView(this).apply {
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
settings.mediaPlaybackRequiresUserGesture = false

webChromeClient = object : WebChromeClient() {
override fun onPermissionRequest(request: PermissionRequest) {
// Grant camera permission for the journey
request.grant(request.resources)
}
}

webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView, request: WebResourceRequest
): Boolean {
val requestUrl = request.url.toString()
if (requestUrl.startsWith(callbackUri)) {
// Journey complete — extract id and handle result
val processId = request.url.getQueryParameter("id")
handleResult(processId)
return true
}
return false
}
}
}

setContentView(webView)
webView.loadUrl(url)
}

private fun handleResult(processId: String?) {
// Send processId to your backend → Step 3b
finish()
}
}

// Launch:
startActivity(Intent(context, HumanTokenActivity::class.java).apply {
putExtra("PROCESS_URL", processLink)
})
```

### iOS (WKWebView)

Requires WebKit framework. iOS 14.3+. Camera permission is auto-granted for the journey origin on iOS 15+.

```swift
import UIKit
import WebKit

class HumanTokenViewController: UIViewController, WKNavigationDelegate, WKUIDelegate {
private var webView: WKWebView!
private let callbackUri = "https://yourapp.com/human-token/callback"

override func viewDidLoad() {
super.viewDidLoad()

let config = WKWebViewConfiguration()
config.allowsInlineMediaPlayback = true
if #available(iOS 15.0, *) {
// Camera permission auto-granted for the journey origin
}

webView = WKWebView(frame: view.bounds, configuration: config)
webView.navigationDelegate = self
webView.uiDelegate = self
view.addSubview(webView)
}

func startFlow(url: URL) {
webView.load(URLRequest(url: url))
}

func webView(
_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
) {
if let url = navigationAction.request.url,
url.absoluteString.hasPrefix(callbackUri) {
// Journey complete — extract id and handle result
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
let processId = components?.queryItems?.first(where: { $0.name == "id" })?.value
handleResult(processId: processId)
decisionHandler(.cancel)
return
}
decisionHandler(.allow)
}

private func handleResult(processId: String?) {
// Send processId to your backend → Step 3b
dismiss(animated: true)
}
}

// Launch:
let vc = HumanTokenViewController()
vc.startFlow(url: URL(string: processLink)!)
present(vc, animated: true)
```

### Flutter (WebView)

Add dependencies to `pubspec.yaml`:

```yaml
dependencies:
webview_flutter: ^4.0.0
permission_handler: ^11.0.0
```

```dart
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:permission_handler/permission_handler.dart';

class HumanTokenWebView extends StatefulWidget {
final String processUrl;
final String callbackUri;
final void Function(String? processId) onResult;

const HumanTokenWebView({
required this.processUrl,
required this.callbackUri,
required this.onResult,
super.key,
});

@override
State<HumanTokenWebView> createState() => _HumanTokenWebViewState();
}

class _HumanTokenWebViewState extends State<HumanTokenWebView> {
late final WebViewController _controller;

@override
void initState() {
super.initState();
_requestCameraPermission();

_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onNavigationRequest: (request) {
if (request.url.startsWith(widget.callbackUri)) {
final uri = Uri.parse(request.url);
final processId = uri.queryParameters['id'];
widget.onResult(processId);
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
),
)
..loadRequest(Uri.parse(widget.processUrl));
}

Future<void> _requestCameraPermission() async {
await Permission.camera.request();
}

@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(child: WebViewWidget(controller: _controller)),
);
}
}

// Launch:
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => HumanTokenWebView(
processUrl: processLink,
callbackUri: 'https://yourapp.com/human-token/callback',
onResult: (processId) {
// Send processId to your backend → Step 3b
Navigator.pop(context);
},
),
),
);
```

## Step 3b — Retrieve the result (backend, Unico Journey only)

> **Only for Flow = `humantoken` or `humantoken+revalidation`.** API Only flows return the result in the create-process response.

```bash
curl https://humantoken.unico.app/client/v1/process/$PROCESS_ID \
-H "x-unico-accesskey: $UNICO_ACCESS_KEY"
```

**Standard flow (`humantoken`) response:**
```json
{
"id": "6ed74799-4c2f-4023-b43f-a73cf7c847b2",
"state": "PROCESS_STATE_FINISHED",
"data": {
"humanToken": {
"value": "3f9a1c7e5b2d4086a1e3c5f7092b4d6e8a0c2e4f6183a5c7e9b1d3f5072a4c6e",
"status": "completed"
}
}
}
```

**Revalidation flow (`humantoken+revalidation`) response:**
```json
{
"id": "a1b2c3d4-0000-0000-0000-abcdef012345",
"state": "PROCESS_STATE_FINISHED",
"data": {
"humanTokenMatch": "true"
}
}
```

### Process states

| State | Meaning |
|---|---|
| `PROCESS_STATE_CREATED` | Process created, user has not started the journey |
| `PROCESS_STATE_FINISHED` | Process finished. `data.humanToken` (standard) or `data.humanTokenMatch` (revalidation) is populated when successful; otherwise `data` is `{}` |
| `PROCESS_STATE_FAILED` | Process failed. `data` is `{}` |
| `PROCESS_STATE_CANCELED` | Process was canceled. `data` is `{}` |

### `humanToken` result interpretation (standard flows)

| `status` | `value` | Meaning | Action |
|---|---|---|---|
| `pending` | — | Process created, user has not started | Wait and poll again |
| `processing` | — | User completed the flow; result being processed | Wait and poll again |
| `completed` | non-empty string | Stable face identifier ready | Look up `value` in your records: existing = returning user or duplicate; new = first-time person |
| `completed` | `""` (empty) | Biometric captured but could not generate a reliable identifier | Treat as inconclusive. Never store or match an empty value. Retry or reject per business rules |
| `expired` | — | Link expired before user completed | Create a new process |
| `canceled` | — | Process was canceled | Create a new process if needed |

### `humanTokenMatch` result interpretation (revalidation flows)

| `humanTokenMatch` | Meaning | Action |
|---|---|---|
| `"true"` | The new capture matches the referenced `humanToken` | Proceed — same person confirmed |
| `"false"` | The new capture does not match the referenced `humanToken` | Handle mismatch per business rules (deny, flag, re-verify) |

> **Important:** For standard flows, always check whether `data.humanToken.value` is non-empty before using it. For revalidation flows, read `data.humanTokenMatch` as a string (`"true"` or `"false"`).

## Step 4 — Apply the result to my application

```typescript
if (process.state !== "PROCESS_STATE_FINISHED") {
// handle non-terminal states (CREATED, FAILED, CANCELED)
return;
}

// Check if this is a revalidation flow
if (process.data.humanTokenMatch !== undefined) {
// Revalidation flow — data.humanTokenMatch is "true" or "false"
if (process.data.humanTokenMatch === "true") {
// Same person confirmed — proceed with the action
allowAction();
} else {
// Mismatch — handle per business rules
denyOrFlagAction();
}
return;
}

// Standard flow — data.humanToken.value
const { humanToken } = process.data;

if (!humanToken?.value) {
// Liveness check did not produce a reliable identifier
// Decide per business rules: retry the process or reject
return;
}

// Look up whether this humanToken has been seen before
const existing = await db.persons.findByHumanToken(humanToken.value);

if (existing) {
// Same biometric has been seen — possible duplicate or returning user
handleDuplicate(existing);
} else {
// First time this biometric appears — create a new record
await db.persons.create({
humanToken: humanToken.value,
createdAt: new Date(),
});
}
```

## Step 5 — Retrieve the selfie (optional)

After a process finishes, you can retrieve the captured selfie image. The selfie is available for **24 hours** after the process completes — after that, it is permanently deleted.

```bash
curl https://humantoken.unico.app/client/v1/process/$PROCESS_ID/selfie \
-H "x-unico-accesskey: $UNICO_ACCESS_KEY"
```

Response:
```json
{
"fileContents": "<base64-encoded-image>",
"contentType": "image/jpeg"
}
```

| Field | Type | Description |
|---|---|---|
| `fileContents` | string | Base64-encoded selfie image |
| `contentType` | string | MIME type — `image/jpeg` or `image/png` |

> **24-hour window:** The selfie is deleted after 24 hours. If you need to store it, fetch it right after the process completes and persist it in your own storage.
>
> **Not available for all processes:** Returns `404` if the process had no biometric capture (e.g. it expired or was canceled before the user completed the flow), or if the 24-hour window has passed.

Use this when you need the original selfie for audit, KYC records, or displaying a profile photo alongside the `humanToken`.

---

## Error reference

### Create Process (`POST /client/v1/process`)

| HTTP | Code | Message | Cause |
|---|---|---|---|
| 400 | 3 | `invalid flow` | Flow value not recognized |
| 400 | 3 | `INVALID_IMAGE` | Image is not valid base64 or appears to be an injection attempt |
| 400 | 3 | `IMAGE_TOO_LARGE` | Image exceeds the 800 KB limit |
| 400 | 3 | `UNSUPPORTED_IMAGE_FORMAT` | Image format is not PNG, JPEG or WebP |
| 400 | 3 | `MISSING_IMAGE` | Image is required for this flow and was not sent |
| 400 | 3 | `INVALID_REQUEST` | Request body is null or cannot be parsed |
| 400 | 3 | `MISSING_REFERENCES` | Revalidation flow requires a `references` array |
| 400 | 9 | `PROCESS_FAILED` | Process hit a terminal failure during creation |
| 403 | 7 | `PERMISSION_DENIED` | Credentials are valid but lack permission for this action |
| 429 | — | _(no body)_ | Rate limit — implement exponential back-off with jitter |
| 500 | 13 | `Internal failure! Try again later` | Internal error |

### Get Process (`GET /client/v1/process/{processId}`)

| HTTP | Code | Message | Cause |
|---|---|---|---|
| 400 | 3 | `process id is invalid` | Process ID format is invalid |
| 401 | — | — | Missing or invalid `x-unico-accesskey` |
| 404 | 5 | `process not found` | Process ID not found |
| 429 | — | _(no body)_ | Rate limit — implement exponential back-off with jitter |
| 500 | 99999 | `Internal failure! Try again later` | Internal error |

### Get Selfie (`GET /client/v1/process/{processId}/selfie`)

| HTTP | Cause |
|---|---|
| 400 | Missing or malformed `processId` |
| 401 | Missing or invalid `x-unico-accesskey` |
| 403 | Credentials lack permission for this process |
| 404 | Process not found, no capture occurred, or 24-hour window expired |
| 429 | Rate limit — implement exponential back-off with jitter |

## Best-practice checklist before you mark this done

- [ ] `UNICO_ACCESS_KEY` stored in secrets manager. Never committed.
- [ ] `x-unico-accesskey` header sent only from backend — never exposed to the browser.
- [ ] No personal data stored — this flow is anonymous by design.
- [ ] Empty `humanToken.value` handled explicitly — do not store or match against an empty string (standard flows).

**API Only (`api+humantoken`, `api+liveness+humantoken`, `api+liveness+humantoken+revalidation`):**
- [ ] Image follows the capture standard: one person, frontal and centered, eyes open, neutral expression, no glasses, hats or masks, even lighting, plain light background, in color and in focus.
- [ ] Image is JPEG, PNG or WebP, at most 800 KB, sent as plain base64 without a `data:image/...;base64,` prefix.
- [ ] Result read directly from the create-process response body — no polling needed.
- [ ] Revalidation flows include `references` array with `referenceType` and `referenceContent`.

**Unico Journey (`humantoken`, `humantoken+revalidation`):**
- [ ] Backend route that creates the process, persists `id`, and returns `token` + `link`.
- [ ] **Web — SDK:** `ByUnicoSDK.init({ token })` called with the process token. HTTPS domain registered with Unico team.
- [ ] **Web — Redirect:** `callbackUri` is HTTPS and its domain is registered with Unico team. `id` from callback query param is validated against a process your backend created (prevents forged callbacks). Result is always retrieved from the API — never assumed from the redirect alone.
- [ ] **Android:** WebView with INTERNET + CAMERA permissions. Monitors navigation for `callbackUri`. Android 8+ (API 26+).
- [ ] **iOS:** WKWebView with WebKit framework. Monitors navigation for `callbackUri`. iOS 14.3+.
- [ ] **Flutter:** `webview_flutter` + `permission_handler`. Monitors navigation for `callbackUri`.
- [ ] Backend result-fetch endpoint calls `GET /client/v1/process/{processId}` and reads `data.humanToken.value` (standard) or `data.humanTokenMatch` (revalidation).
- [ ] All process states handled case-sensitively.

## When you need more detail

- Documentation: `https://developer.unico.io/human-token`
- Get Selfie API: `https://developer.unico.io/human-token/api-reference/get-selfie`

Now build the integration. If anything in `## My application context` is missing, ask once at the top of your reply, then ship the complete change set.

- **API Only** (`api+humantoken`, `api+liveness+humantoken`): a single backend route that receives the base64 image, calls `POST /client/v1/process`, and applies the result synchronously. No frontend SDK, no redirect, no polling.
- **API Only Revalidation** (`api+liveness+humantoken+revalidation`): same as above, but include a `references` array with the prior `humanToken`. Read `data.humanTokenMatch` from the response.
- **Unico Journey** (`humantoken`): backend route to create the process, frontend SDK call or native WebView handling, result-fetch endpoint, and duplicate/first-seen logic based on `humanToken.value`. Handle all process states case-sensitively.
- **Unico Journey Revalidation** (`humantoken+revalidation`): same as Journey, but include a `references` array when creating the process. Read `data.humanTokenMatch` from the polling result.

Keep `UNICO_ACCESS_KEY` server-side only. Store no personal data beyond the `humanToken`. Always handle empty `humanToken.value` explicitly — never store or match against an empty string.