> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gu1.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Embedded Biometric Session

> Start a hosted biometric re-authentication session after approved KYC — iframe-ready session URL, webhooks, and Gu1 final verdict.

## Overview

**Embedded Biometric** lets you re-verify that the person completing a flow is the same individual who passed KYC earlier. Unlike the synchronous [Biometric Check](/en/use-cases/kyc/biometric) (where your app uploads a selfie), this flow uses a **hosted capture UI** that Gu1 returns as `sessionUrl`. Your app embeds that URL in an iframe (or opens a redirect) so liveness and face capture run in a controlled environment.

<Warning>
  **Use `status`** from the API or webhook as the biometric result. Gu1 runs the full session lifecycle and may set `rejected` after organization policies (cross-entity checks, face-match score thresholds).
</Warning>

## Prerequisites

1. **Approved KYC in Gu1** for the person entity (`status: approved` on a session-based validation).
2. **Reference portrait available** from that KYC (selfie stored when the validation was approved). Required for session create (`NO_PORTRAIT` if missing).
3. **KYC enabled** for your organization (same API key and permissions as session-based validation). Gu1 provisions the internal hosted-capture configuration; integrators do not set workflows or extra secrets. If your org is not provisioned yet, create may return `BIOMETRIC_WORKFLOW_NOT_CONFIGURED` (400) — contact your Gu1 account team.
4. **Gu1 Biometric** active for your organization (`global_gueno_biometric_kyc`). If not enabled, create returns `NOT_ENABLED` (403) — request activation from Gu1.

## Flow

```mermaid theme={null}
sequenceDiagram
    participant App as Your Backend
    participant API as gu1 API
    participant UI as Hosted Biometric UI
    participant WH as Your webhookUrl

    App->>API: POST /api/kyc/biometric/sessions
    API-->>App: sessionUrl, iframeAllow, id

    App->>UI: iframe src=sessionUrl
    UI->>API: Provider webhook (status update)
    API->>API: Gu1 rules + final status

    alt webhookUrl in request
        API->>WH: POST biometric.session_*
    end
    API->>App: Org webhook (if configured)
```

## Create Session

```
POST https://api.gu1.ai/api/kyc/biometric/sessions
```

### Body

Provide **exactly one** entity identifier:

<ParamField body="entityId" type="string">
  UUID of the person entity with approved KYC.
</ParamField>

<ParamField body="entityExternalId" type="string">
  Your external ID for the entity (`entities.externalId` in Gu1).
</ParamField>

<ParamField body="entityTaxId" type="string">
  Tax or document number (CUIT, CPF, DNI, etc.). Gu1 resolves it with normalized match on `entities.tax_id`. The entity **must exist** in Gu1 (404 if not found).
</ParamField>

<Info>
  **Sandbox entity preview:** `GET /api/entities/by-tax-id/{taxId}` may return a **synthetic** person (`sandboxMock: true`, `id: null`) for catalog test document numbers when no real row exists. That preview is **read-only** — you still need a persisted entity before `POST /sessions`.
</Info>

<ParamField body="webhookUrl" type="string">
  Optional HTTPS endpoint Gu1 POSTs to on **terminal** statuses (`approved`, `rejected`, `abandoned`, `expired`). Signed with your KYC webhook secret when configured.
</ParamField>

<ParamField body="callback" type="string">
  Optional redirect URL after the user finishes in the hosted UI.
</ParamField>

<ParamField body="language" type="string">
  UI language, e.g. `es`, `en`, `pt`.
</ParamField>

<ParamField body="workflowId" type="string">
  Optional override for the biometric workflow ID (otherwise from org KYC settings).
</ParamField>

### Response `201`

```json theme={null}
{
  "id": "bio-session-uuid",
  "entityId": "entity-uuid",
  "status": "pending",
  "sessionUrl": "https://verify.example.com/session/...",
  "iframeAllow": "camera; microphone; fullscreen; autoplay; encrypted-media",
  "mode": "face_match",
  "hostedSessionId": "hosted-session-id"
}
```

<Note>
  In **sandbox mock**, `status` may be `approved` or `rejected` immediately (no iframe). See [Sandbox mock — Embedded biometric](/en/use-cases/kyc/sandbox-mock-data#embedded-biometric-sandbox-mock).
</Note>

## Recommended Integration Flow

1. Ensure the person entity has **KYC `approved`** (session-based validation).
2. Call **`POST /api/kyc/biometric/sessions` once** per user intent. Store `id`, `sessionUrl`, and `hostedSessionId`.
3. If the response is **`409 ACTIVE_SESSION_EXISTS`**, cancel with `POST .../sessions/{activeSessionId}/cancel`, then create again (do **not** retry in a loop without handling 409).
4. If `status` is **`pending`**, embed `sessionUrl` in an iframe (or redirect).
5. Wait for **`biometric.session_*` webhooks** (or poll `GET .../sessions/:id` / `POST .../sync` if needed).
6. Use **`status`** and optional `rejectionCode` as the final biometric result.

<Warning>
  **Do not** treat repeated `POST /sessions` as the normal path. Retries after timeouts or double-clicks can hit idempotency edge cases. Always handle `409`, cancel active sessions explicitly, and prefer webhooks over aggressive polling.
</Warning>

## Create Errors (`POST /sessions`)

| HTTP | `error`                        | When                                                    | What to do                                                 |
| ---- | ------------------------------ | ------------------------------------------------------- | ---------------------------------------------------------- |
| 400  | `NO_KYC`                       | No approved KYC for the entity                          | Complete KYC first                                         |
| 400  | `NO_PORTRAIT`                  | Approved KYC has no usable reference selfie (real flow) | Re-run KYC with valid capture, or use sandbox mock         |
| 400  | `KYC_NOT_CONFIGURED`           | Org KYC credentials missing/disabled                    | Configure KYC in org settings                              |
| 400  | `ENTITY_NOT_FOUND`             | Invalid `entityId` or wrong org                         | Fix entity / `X-Organization-ID`                           |
| 400  | `INVALID_ENTITY_TYPE`          | Entity is not `person`                                  | Use a person entity                                        |
| 403  | `NOT_ENABLED`                  | `global_gueno_biometric_kyc` not active                 | Contact Gu1 to enable biometrics                           |
| 402  | `INSUFFICIENT_CREDITS_FOR_KYC` | Billing hold failed                                     | Top up credits / pack                                      |
| 409  | `ACTIVE_SESSION_EXISTS`        | Latest session is `pending` or `in_progress`            | Cancel `activeSessionId`, then create again                |
| 502  | `PROVIDER_ERROR`               | Gu1 could not start hosted capture                      | Retry later; use the `error` code and HTTP status          |
| 500  | `CREATION_FAILED`              | Unexpected persistence failure                          | Check logs; cancel any stuck session; retry once after fix |

Example **`409 ACTIVE_SESSION_EXISTS`** (additive — existing clients that ignore extra fields are unchanged):

```json theme={null}
{
  "error": "ACTIVE_SESSION_EXISTS",
  "message": "Cannot create new biometric session. There is already a pending session for this entity. Please cancel or complete the existing session first.",
  "activeSessionId": "7618e10c-b1b1-408b-b361-1901077ced73"
}
```

Repeated `POST /sessions` while a session is still open **always** returns this `409`. Gu1 does **not** return `201` with the same pending session.

## Prior Sessions and “Current” Biometric

An entity may have **many** biometric sessions over time (approved, rejected, cancelled, etc.).

| API                                                                                          | Meaning                                                                                            |
| -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `GET .../entities/:entityId/current`                                                         | **Latest created** session (any status) — use to recover a pending iframe or read the last attempt |
| `GET .../entities/by-tax-id/:taxId/current`                                                  | Same as above, resolved by `entityTaxId`                                                           |
| `GET .../entities/by-external-id/:externalId/current`                                        | Same as above, resolved by `entityExternalId`                                                      |
| `GET .../sessions?entityId=...` (or `entityTaxId` / `entityExternalId`) → `currentSessionId` | Latest **`approved`** session — active step-up verification for the entity                         |
| List `data[]`                                                                                | Full history ordered by `createdAt`                                                                |

A **previous approved** session does **not** block creating a new one. Only a **non-terminal** latest session (`pending` / `in_progress`) triggers `409 ACTIVE_SESSION_EXISTS`.

## `hostedSessionId`, Cancel, and Retries

`hostedSessionId` is the **hosted capture session reference** returned at create. Gu1 stores it uniquely and uses it for webhooks and sync.

**Correct recovery:**

```
409 → POST .../sessions/{activeSessionId}/cancel → POST .../sessions (once)
```

**After cancel:** you may create a new session. Idempotent persistence (reusing an existing **terminal** row when the same `hostedSessionId` is seen again) applies only after cancel or other terminal outcomes — **not** while a session is still `pending` or `in_progress`.

**Sandbox mock:** `hostedSessionId` is prefixed with `sandbox-mock-bio-` and each mock create gets a new ID.

## Embed in Your App

```html theme={null}
<iframe
  src="{{ sessionUrl }}"
  style="width: 100%; height: 700px; border: none;"
  allow="{{ iframeAllow }}"
></iframe>
```

Alternatives: full-page redirect to `sessionUrl`, or a compatible Web SDK initialized with the same URL.

## Poll or Sync Status

* `GET /api/kyc/biometric/sessions/:id` — one session by ID
* `GET /api/kyc/biometric/entities/:entityId/current` — **current session** = most recent by `createdAt` (any status). Returns `200` with `null` if none.
* `GET /api/kyc/biometric/entities/by-tax-id/:taxId/current` — same, resolved by tax ID
* `GET /api/kyc/biometric/entities/by-external-id/:externalId/current` — same, resolved by external ID
* `GET /api/kyc/biometric/sessions?entityId=...` — list; also accepts `entityTaxId` or `entityExternalId` (one at a time). `currentSessionId` is the latest **`approved`** session.
* `POST /api/kyc/biometric/sessions/:id/sync` — refresh session state from Gu1 if webhooks are delayed.
* `POST /api/kyc/biometric/sessions/:id/cancel` — manually cancel `pending` or `in_progress` sessions (marks `cancelled` in Gu1).

If `POST /sessions` returns `409 ACTIVE_SESSION_EXISTS`, the body includes `activeSessionId` so you can cancel without calling `/current`.

<Note>
  The platform may report **In Review** while capture is reviewed; Gu1 maps that to `in_progress` until the final verdict (`approved` or `rejected`). Only terminal outcome states define the entity current session.
</Note>

## Webhooks

Two channels:

1. **Per-request `webhookUrl`** — only if you sent it in `POST /sessions`; fires on terminal status.
2. **Organization webhooks** — subscribe to `biometric.session_*` events in [Webhook configuration](/en/webhooks/configuration). See [Biometric webhook events](/en/webhooks/events/biometric-events).

## Comparison with Synchronous Biometric

|                          | `POST /api/kyc/biometric` | `POST /api/kyc/biometric/sessions` |
| ------------------------ | ------------------------- | ---------------------------------- |
| Capture                  | Your app sends image      | Hosted UI (iframe)                 |
| Fraud resistance         | Lower                     | Higher (liveness in hosted UI)     |
| Async                    | No                        | Yes                                |
| `webhookUrl` per request | No                        | Yes                                |

For step-up authentication and login flows, prefer **embedded sessions**.

## Sandbox Mock Sessions

In **sandbox**, when the entity has an **approved KYC mock** (test `taxId` such as `99990001`, or KYC with `metadata.sandboxMock: true`), `POST /api/kyc/biometric/sessions` can return an **immediate mock result** (`approved` or `rejected`) without hosted capture.

* Default: `99990001` → biometric **approved**; `99990011` → biometric **rejected** (KYC still approved).
* Override on any eligible entity: `"metadata": { "sandboxMockOutcome": "rejected" }`.
* Mock responses use `hostedSessionId` prefixed with `sandbox-mock-bio-` and may return `status: approved` or `rejected` in the create response (not only `pending`).

Full tables and examples: [Sandbox mock data — Embedded biometric](/en/use-cases/kyc/sandbox-mock-data#embedded-biometric-sandbox-mock).
