> ## 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.

# Biometric Session Webhook Events

> Webhook event payloads for embedded biometric re-authentication sessions, covering organization-level events and per-request webhookUrl callbacks.

## Overview

Embedded biometric sessions are **managed end-to-end by Gu1**. When the user completes capture in the hosted UI, Gu1 persists the session, applies your organization policies (face-match score thresholds, cross-entity checks, billing), and emits webhooks with the **session result**.

Your integration should treat **`payload.status`** as the biometric outcome — the same field returned by `GET /api/kyc/biometric/sessions/:id`.

Gu1 exposes two delivery paths:

1. **Organization webhooks** — configure in [Webhook configuration](/en/webhooks/configuration) and subscribe to `biometric.session_*` events (all status transitions).
2. **Per-request `webhookUrl`** — optional HTTPS URL passed in `POST /api/kyc/biometric/sessions`; Gu1 POSTs on **terminal capture outcomes** (`approved`, `rejected`, `abandoned`, `expired`). Manual `cancel` via API triggers organization webhooks only.

## Event Types

| Event                           | When                                                      |
| ------------------------------- | --------------------------------------------------------- |
| `biometric.session_created`     | Session created (`status: pending`)                       |
| `biometric.session_in_progress` | User started capture in the hosted UI                     |
| `biometric.session_approved`    | Biometric session **approved** by Gu1                     |
| `biometric.session_rejected`    | Biometric session **rejected** by Gu1                     |
| `biometric.session_abandoned`   | User left the hosted flow without finishing               |
| `biometric.session_cancelled`   | Session cancelled in Gu1 (`pending` / `in_progress` only) |
| `biometric.session_expired`     | Session expired                                           |

## Payload Envelope

Same outer structure as [KYC events](/en/webhooks/events/kyc-events):

```json theme={null}
{
  "event": "biometric.session_approved",
  "timestamp": "2026-06-11T15:00:00Z",
  "organizationId": "org-uuid",
  "payload": { }
}
```

The inner **`payload`** is the biometric session as stored in Gu1 (same shape as the REST API), plus an `entity` snapshot when available.

### Payload Fields

<ResponseField name="payload.status" type="string" required>
  Biometric session status: `pending`, `in_progress`, `approved`, `rejected`, `abandoned`, `cancelled`, `expired`.
</ResponseField>

<ResponseField name="payload.rejectionCode" type="string | null">
  Present when `status` is `rejected` (e.g. `FACE_MATCH_SCORE_LOW`, `CROSS_ENTITY`, `CAPTURE_DECLINED`).
</ResponseField>

<ResponseField name="payload.rejectionMessage" type="string | null">
  Human-readable detail for `rejectionCode` when available.
</ResponseField>

<ResponseField name="payload.mode" type="string">
  Always `face_match` for new sessions (hosted liveness + KYC portrait comparison).
</ResponseField>

<ResponseField name="payload.sessionUrl" type="string | null">
  Hosted capture URL while the session is active (`pending` / `in_progress`).
</ResponseField>

<ResponseField name="payload.hostedSessionId" type="string">
  Opaque ID of the session in the hosted capture environment (support/correlation only; use `id` as your primary Gu1 session key).
</ResponseField>

<ResponseField name="payload.decision" type="object">
  Provider capture payload after completion (liveness, face match scores, media keys). Fetch images via `GET /api/kyc/biometric/sessions/:id/media?key=`.
</ResponseField>

<ResponseField name="payload.entity" type="object">
  Snapshot of the person entity (`id`, `externalId`, `name`, `type`) when available.
</ResponseField>

## Example: Session Created

```json theme={null}
{
  "event": "biometric.session_created",
  "timestamp": "2026-06-11T14:00:00Z",
  "organizationId": "org-uuid",
  "payload": {
    "id": "bio-session-uuid",
    "entityId": "entity-uuid",
    "organizationId": "org-uuid",
    "kycValidationId": "kyc-validation-uuid",
    "status": "pending",
    "mode": "face_match",
    "sessionUrl": "https://verify.example.com/session/abc",
    "hostedSessionId": "hosted-session-id",
    "iframeAllow": "camera; microphone; fullscreen; autoplay; encrypted-media",
    "rejectionCode": null,
    "rejectionMessage": null,
    "createdAt": "2026-06-11T14:00:00.000Z",
    "updatedAt": "2026-06-11T14:00:00.000Z",
    "completedAt": null,
    "entity": {
      "id": "entity-uuid",
      "externalId": "customer-123",
      "name": "Jane Doe",
      "type": "person"
    }
  }
}
```

## Example: In Progress

```json theme={null}
{
  "event": "biometric.session_in_progress",
  "timestamp": "2026-06-11T14:05:00Z",
  "organizationId": "org-uuid",
  "payload": {
    "id": "bio-session-uuid",
    "entityId": "entity-uuid",
    "status": "in_progress",
    "mode": "face_match",
    "sessionUrl": "https://verify.example.com/session/abc",
    "rejectionCode": null,
    "rejectionMessage": null
  }
}
```

## Example: Approved

```json theme={null}
{
  "event": "biometric.session_approved",
  "timestamp": "2026-06-11T14:10:00Z",
  "organizationId": "org-uuid",
  "payload": {
    "id": "bio-session-uuid",
    "entityId": "entity-uuid",
    "status": "approved",
    "mode": "face_match",
    "sessionUrl": "https://verify.example.com/session/abc",
    "rejectionCode": null,
    "rejectionMessage": null,
    "completedAt": "2026-06-11T14:10:00.000Z",
    "decision": {
      "face_match": {
        "status": "Approved",
        "score": 99.2
      },
      "liveness": {
        "status": "Approved",
        "score": 98.5
      }
    },
    "entity": {
      "id": "entity-uuid",
      "externalId": "customer-123",
      "name": "Jane Doe",
      "type": "person"
    }
  }
}
```

## Example: Rejected

```json theme={null}
{
  "event": "biometric.session_rejected",
  "timestamp": "2026-06-11T14:12:00Z",
  "organizationId": "org-uuid",
  "payload": {
    "id": "bio-session-uuid",
    "entityId": "entity-uuid",
    "status": "rejected",
    "mode": "face_match",
    "rejectionCode": "FACE_MATCH_SCORE_LOW",
    "rejectionMessage": "Face match score 28.5 is below organization threshold 30",
    "completedAt": "2026-06-11T14:12:00.000Z",
    "decision": {
      "face_match": {
        "status": "Approved",
        "score": 28.5
      }
    },
    "entity": {
      "id": "entity-uuid",
      "externalId": "customer-123",
      "name": "Jane Doe",
      "type": "person"
    }
  }
}
```

### Common `rejectionCode` values

| Code                   | Meaning                                                           |
| ---------------------- | ----------------------------------------------------------------- |
| `FACE_MATCH_SCORE_LOW` | Face match score below organization threshold (`face_match` mode) |
| `CROSS_ENTITY`         | Capture matches a different entity in your organization           |
| `CAPTURE_DECLINED`     | Capture declined in the hosted flow                               |

## Per-request `webhookUrl`

When you pass `webhookUrl` at session creation:

* Body uses the same `event` names and `payload` shape as organization webhooks.
* Requests are signed with HMAC-SHA256 in `X-Webhook-Signature` when your KYC webhook secret is configured.
* Headers also include `X-Webhook-Event`, `X-Webhook-ID`, and `X-Webhook-Timestamp`.

Verify signatures the same way as [outbound webhooks](/en/webhooks/configuration#signature-verification).

<Note>
  Manual cancel (`POST /api/kyc/biometric/sessions/:id/cancel`) emits `biometric.session_cancelled` on **organization** webhooks only; it does not POST to per-request `webhookUrl`.
</Note>
