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

# KYC Webhook Events

> KYC webhook event payloads from gu1 for verification completion, status changes, identity matches, document checks, and alert lifecycle updates.

## Overview

KYC (Know Your Customer) webhook events allow you to receive real-time notifications when a KYC verification status changes. Gu1 automatically sends HTTP POST requests to your configured webhook endpoint whenever a validation status updates, enabling you to automate customer onboarding workflows and maintain compliance.

## Why Use KYC Webhooks?

<CardGroup cols={2}>
  <Card title="Real-Time Updates" icon="bolt">
    Get instant notifications when verification status changes
  </Card>

  <Card title="Efficient" icon="gauge-high">
    No need to poll the API repeatedly
  </Card>

  <Card title="Automated Workflows" icon="robot">
    Automatically update user accounts based on verification results
  </Card>

  <Card title="Better UX" icon="face-smile">
    Notify customers immediately after verification
  </Card>
</CardGroup>

## Available Events

Gu1 sends webhooks for the following KYC validation events:

| Event Type                   | Description                   | When Triggered                                                      |
| ---------------------------- | ----------------------------- | ------------------------------------------------------------------- |
| `kyc.validation_created`     | Validation session created    | When you create a new KYC validation                                |
| `kyc.validation_in_progress` | Customer started verification | Customer is actively completing verification (filling out form)     |
| `kyc.validation_in_review`   | Verification under review     | Verification completed, requires manual review from compliance team |
| `kyc.validation_approved`    | Verification approved         | Identity successfully verified                                      |
| `kyc.validation_rejected`    | Verification rejected         | Identity verification failed                                        |
| `kyc.validation_abandoned`   | Customer abandoned process    | Customer left without completing                                    |
| `kyc.validation_expired`     | Validation session expired    | Session expired (typically after 7 days)                            |
| `kyc.validation_cancelled`   | Validation cancelled          | Validation manually cancelled by organization                       |

## Event Payload Structure

Every KYC webhook uses the same **outer envelope**. The inner `payload` object is the **KYC validation row** as stored in Gu1 (same field names as in the database/API: `id` is the validation UUID, plus `entityId`, `organizationId`, `status`, `decision`, `extractedData`, `verifiedFields`, `warnings`, `metadata`, timestamps, etc.).

<Note>
  **`entity`** (full Gu1 entity row) is sent **only** for terminal outcomes: `kyc.validation_approved` and `kyc.validation_rejected`. It is **not** included for `kyc.validation_created`, `in_progress`, `in_review`, `abandoned`, `expired`, or `cancelled`. This is an **additive** field: all previous payload fields are unchanged. For `kyc.validation_approved`, optional person auto-population from KYC still runs **after** the webhook (same order as before); `payload.entity` is a snapshot **at webhook send time** (before that step). To read the entity after auto-population, call the Entities API.
</Note>

```json theme={null}
{
  "event": "kyc.validation_approved",
  "timestamp": "2025-01-07T11:00:00Z",
  "organizationId": "org-uuid",
  "payload": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "entityId": "123e4567-e89b-12d3-a456-426614174000",
    "organizationId": "org-uuid",
    "status": "approved",
    "decision": {},
    "extractedData": {},
    "verifiedFields": [],
    "warnings": [],
    "entity": {
      "id": "123e4567-e89b-12d3-a456-426614174000",
      "externalId": "customer_xyz789",
      "name": "John Doe",
      "type": "person",
      "taxId": "…",
      "countryCode": "US",
      "status": "active",
      "entityData": {},
      "attributes": {}
    }
  }
}
```

When present, `payload.entity` includes **all entity columns**, JSON-serializable, with dates as ISO strings (`entityData`, `attributes`, `email`, `phone`, etc.).

### Common Payload Fields

<ResponseField name="event" type="string">
  The event type (e.g., `kyc.validation_approved`)
</ResponseField>

<ResponseField name="timestamp" type="string">
  ISO 8601 timestamp when the event occurred
</ResponseField>

<ResponseField name="organizationId" type="string">
  Your organization ID
</ResponseField>

<ResponseField name="payload.id" type="string">
  The KYC validation ID in Gu1 (primary key of the validation row)
</ResponseField>

<ResponseField name="payload.entityId" type="string">
  The entity (person) ID being verified
</ResponseField>

<ResponseField name="payload.entity" type="object">
  Present **only** for `kyc.validation_approved` and `kyc.validation_rejected`. Snapshot of the entity row in Gu1 at webhook send time (on approve, before optional auto-population runs).
</ResponseField>

<ResponseField name="payload.status" type="string">
  Current validation status: `pending`, `in_progress`, `in_review`, `approved`, `rejected`, `abandoned`, `expired`, `cancelled`
</ResponseField>

### Decision object (`payload.decision`)

When a validation reaches a terminal or in-review state with provider results, `payload.decision` contains the full verification outcome from the KYC flow. Gu1 **always stores and returns both shapes** for each feature: a singular object (legacy) **and** a one-element array (current). You can read either `id_verification` or `id_verifications[0]`; they are kept in sync. The same applies to `liveness` / `liveness_checks`, `face_match` / `face_matches`, `aml_screening` / `aml_screenings`, and `ip_analysis` / `ip_analyses`.

Media fields (`front_image`, `reference_image`, `images.*`, etc.) are **Gu1 storage keys** (`kyc/...`) after ingest. Fetch them via the [validation media API](/en/use-cases/kyc/validation-media). Older rows may still contain short-lived HTTPS URLs until synced.

**Approved example (complete):**

```json theme={null}
{
  "status": "Approved",
  "workflow_type": "standard",
  "session_id": "7c0fa22d-0cfa-4a78-8090-7c842397e788",
  "session_number": 921,
  "features": ["ID_VERIFICATION", "LIVENESS", "FACE_MATCH", "IP_ANALYSIS"],
  "images": {
    "documentFront": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-front.jpg",
    "documentBack": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-back.jpg",
    "selfie": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/selfie.jpg"
  },
  "id_verification": {
    "status": "Approved",
    "node_id": "feature_ocr",
    "document_type": "Passport",
    "document_number": "AB123456",
    "first_name": "John",
    "last_name": "Doe",
    "full_name": "John Doe",
    "date_of_birth": "1990-05-20",
    "nationality": "US",
    "gender": "M",
    "age": 35,
    "issuing_state": "US",
    "expiration_date": "2030-05-20",
    "date_of_issue": "2020-05-20",
    "front_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-front.jpg",
    "back_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-back.jpg",
    "portrait_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/selfie.jpg",
    "warnings": [],
    "matches": []
  },
  "id_verifications": [
    {
      "status": "Approved",
      "node_id": "feature_ocr",
      "document_type": "Passport",
      "document_number": "AB123456",
      "first_name": "John",
      "last_name": "Doe",
      "full_name": "John Doe",
      "date_of_birth": "1990-05-20",
      "nationality": "US",
      "gender": "M",
      "age": 35,
      "issuing_state": "US",
      "expiration_date": "2030-05-20",
      "date_of_issue": "2020-05-20",
      "front_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-front.jpg",
      "back_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-back.jpg",
      "portrait_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/selfie.jpg",
      "warnings": [],
      "matches": []
    }
  ],
  "liveness": {
    "status": "Approved",
    "node_id": "feature_liveness",
    "score": 98,
    "method": "PASSIVE",
    "reference_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/liveness-reference.jpg",
    "video_url": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/liveness-video.webm",
    "face_quality": 92.5,
    "warnings": [],
    "matches": []
  },
  "liveness_checks": [
    {
      "status": "Approved",
      "node_id": "feature_liveness",
      "score": 98,
      "method": "PASSIVE",
      "reference_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/liveness-reference.jpg",
      "video_url": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/liveness-video.webm",
      "face_quality": 92.5,
      "warnings": [],
      "matches": []
    }
  ],
  "face_match": {
    "status": "Approved",
    "node_id": "feature_face_match",
    "score": 95,
    "source_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-front.jpg",
    "target_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/selfie.jpg",
    "warnings": []
  },
  "face_matches": [
    {
      "status": "Approved",
      "node_id": "feature_face_match",
      "score": 95,
      "source_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-front.jpg",
      "target_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/selfie.jpg",
      "warnings": []
    }
  ],
  "aml_screening": {
    "status": "Approved",
    "node_id": "feature_aml",
    "warnings": []
  },
  "aml_screenings": [
    {
      "status": "Approved",
      "node_id": "feature_aml",
      "warnings": []
    }
  ],
  "ip_analysis": {
    "status": "Approved",
    "node_id": "feature_ip_analysis",
    "ip_address": "203.0.113.10",
    "country": "US",
    "region": "New York",
    "city": "New York",
    "is_vpn": false,
    "is_proxy": false,
    "warnings": []
  },
  "ip_analyses": [
    {
      "status": "Approved",
      "node_id": "feature_ip_analysis",
      "ip_address": "203.0.113.10",
      "country": "US",
      "region": "New York",
      "city": "New York",
      "is_vpn": false,
      "is_proxy": false,
      "warnings": []
    }
  ]
}
```

**Rejected example (complete):**

```json theme={null}
{
  "status": "Declined",
  "workflow_type": "standard",
  "session_id": "7c0fa22d-0cfa-4a78-8090-7c842397e788",
  "session_number": 921,
  "features": ["ID_VERIFICATION", "LIVENESS", "FACE_MATCH"],
  "images": {
    "documentFront": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-front.jpg",
    "selfie": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/selfie.jpg"
  },
  "id_verification": {
    "status": "Declined",
    "node_id": "feature_ocr",
    "document_type": "Passport",
    "document_number": "AB123456",
    "first_name": "John",
    "last_name": "Doe",
    "full_name": "John Doe",
    "front_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-front.jpg",
    "portrait_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/selfie.jpg",
    "warnings": [
      {
        "risk": "DOCUMENT_AUTHENTICITY_FAILED",
        "feature": "ID_VERIFICATION",
        "short_description": "Document authenticity could not be verified",
        "long_description": "The document failed authenticity checks.",
        "log_type": "error"
      }
    ],
    "matches": []
  },
  "id_verifications": [
    {
      "status": "Declined",
      "node_id": "feature_ocr",
      "document_type": "Passport",
      "document_number": "AB123456",
      "first_name": "John",
      "last_name": "Doe",
      "full_name": "John Doe",
      "front_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-front.jpg",
      "portrait_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/selfie.jpg",
      "warnings": [
        {
          "risk": "DOCUMENT_AUTHENTICITY_FAILED",
          "feature": "ID_VERIFICATION",
          "short_description": "Document authenticity could not be verified",
          "long_description": "The document failed authenticity checks.",
          "log_type": "error"
        }
      ],
      "matches": []
    }
  ],
  "liveness": {
    "status": "Declined",
    "node_id": "feature_liveness",
    "score": 42,
    "method": "PASSIVE",
    "reference_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/liveness-reference.jpg",
    "warnings": [
      {
        "risk": "LIVENESS_FAILED",
        "feature": "LIVENESS",
        "short_description": "Liveness detection failed",
        "long_description": "The liveness check did not pass the required threshold.",
        "log_type": "error"
      }
    ],
    "matches": []
  },
  "liveness_checks": [
    {
      "status": "Declined",
      "node_id": "feature_liveness",
      "score": 42,
      "method": "PASSIVE",
      "reference_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/liveness-reference.jpg",
      "warnings": [
        {
          "risk": "LIVENESS_FAILED",
          "feature": "LIVENESS",
          "short_description": "Liveness detection failed",
          "long_description": "The liveness check did not pass the required threshold.",
          "log_type": "error"
        }
      ],
      "matches": []
    }
  ],
  "face_match": {
    "status": "Declined",
    "node_id": "feature_face_match",
    "score": 38,
    "source_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-front.jpg",
    "target_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/selfie.jpg",
    "warnings": [
      {
        "risk": "FACE_MATCH_LOW_CONFIDENCE",
        "feature": "FACE_MATCH",
        "short_description": "Face match confidence below threshold",
        "long_description": "The selfie did not match the document portrait with sufficient confidence.",
        "log_type": "error"
      }
    ]
  },
  "face_matches": [
    {
      "status": "Declined",
      "node_id": "feature_face_match",
      "score": 38,
      "source_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-front.jpg",
      "target_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/selfie.jpg",
      "warnings": [
        {
          "risk": "FACE_MATCH_LOW_CONFIDENCE",
          "feature": "FACE_MATCH",
          "short_description": "Face match confidence below threshold",
          "long_description": "The selfie did not match the document portrait with sufficient confidence.",
          "log_type": "error"
        }
      ]
    }
  ]
}
```

## Event-Specific Payloads

### kyc.validation\_created

Sent when a new KYC validation is created. The `payload` is the validation row; **`entity` is not included** (non-terminal event).

```json theme={null}
{
  "event": "kyc.validation_created",
  "timestamp": "2025-01-07T10:30:00Z",
  "organizationId": "org-123",
  "payload": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "entityId": "123e4567-e89b-12d3-a456-426614174000",
    "status": "pending",
    "providerSessionUrl": "https://kyc.gu1.io/validate/abc123",
    "expiresAt": "2025-01-14T10:30:00Z"
  }
}
```

**Use case**: Send the validation URL to your customer via email or SMS.

### kyc.validation\_in\_progress

Sent when a customer starts the verification process. **`entity` is not included.**

```json theme={null}
{
  "event": "kyc.validation_in_progress",
  "timestamp": "2025-01-07T10:35:00Z",
  "organizationId": "org-123",
  "payload": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "entityId": "123e4567-e89b-12d3-a456-426614174000",
    "status": "in_progress"
  }
}
```

**Use case**: Update UI to show "Verification in progress" status.

### kyc.validation\_in\_review

Sent when a customer completes verification and requires manual review from the compliance team. **`entity` is not included.**

```json theme={null}
{
  "event": "kyc.validation_in_review",
  "timestamp": "2025-01-07T10:50:00Z",
  "organizationId": "org-123",
  "payload": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "entityId": "123e4567-e89b-12d3-a456-426614174000",
    "status": "in_review"
  }
}
```

**Use case**: Notify compliance team for manual review. Update UI to show "Under review by compliance team".

### kyc.validation\_approved

Sent when verification is successfully completed. **`entity` is included** (full row at send time; optional auto-population may run afterward, same as before).

```json theme={null}
{
  "event": "kyc.validation_approved",
  "timestamp": "2025-01-07T11:00:00Z",
  "organizationId": "org-123",
  "payload": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "entityId": "123e4567-e89b-12d3-a456-426614174000",
    "entity": {
      "id": "123e4567-e89b-12d3-a456-426614174000",
      "externalId": "customer_xyz789",
      "name": "John Doe",
      "type": "person",
      "entityData": { "person": { "firstName": "John", "lastName": "Doe" } },
      "attributes": {}
    },
    "status": "approved",
    "verifiedAt": "2025-01-07T11:00:00Z",
    "extractedData": {
      "firstName": "John",
      "lastName": "Doe",
      "dateOfBirth": "1990-05-20",
      "nationality": "US",
      "documentNumber": "AB123456",
      "documentType": "passport",
      "documentExpiry": "2030-05-20",
      "address": {
        "street": "123 Main St",
        "city": "New York",
        "state": "NY",
        "postalCode": "10001",
        "country": "US"
      }
    },
    "verifiedFields": [
      "firstName",
      "lastName",
      "dateOfBirth",
      "nationality",
      "documentNumber"
    ],
    "warnings": [],
    "decision": {
      "status": "Approved",
      "workflow_type": "standard",
      "session_id": "7c0fa22d-0cfa-4a78-8090-7c842397e788",
      "session_number": 921,
      "features": ["ID_VERIFICATION", "LIVENESS", "FACE_MATCH", "IP_ANALYSIS"],
      "images": {
        "documentFront": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-front.jpg",
        "documentBack": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-back.jpg",
        "selfie": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/selfie.jpg"
      },
      "id_verification": {
        "status": "Approved",
        "node_id": "feature_ocr",
        "document_type": "Passport",
        "document_number": "AB123456",
        "first_name": "John",
        "last_name": "Doe",
        "full_name": "John Doe",
        "date_of_birth": "1990-05-20",
        "nationality": "US",
        "gender": "M",
        "age": 35,
        "issuing_state": "US",
        "expiration_date": "2030-05-20",
        "date_of_issue": "2020-05-20",
        "front_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-front.jpg",
        "back_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-back.jpg",
        "portrait_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/selfie.jpg",
        "warnings": [],
        "matches": []
      },
      "id_verifications": [
        {
          "status": "Approved",
          "node_id": "feature_ocr",
          "document_type": "Passport",
          "document_number": "AB123456",
          "first_name": "John",
          "last_name": "Doe",
          "full_name": "John Doe",
          "date_of_birth": "1990-05-20",
          "nationality": "US",
          "gender": "M",
          "age": 35,
          "issuing_state": "US",
          "expiration_date": "2030-05-20",
          "date_of_issue": "2020-05-20",
          "front_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-front.jpg",
          "back_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-back.jpg",
          "portrait_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/selfie.jpg",
          "warnings": [],
          "matches": []
        }
      ],
      "liveness": {
        "status": "Approved",
        "node_id": "feature_liveness",
        "score": 98,
        "method": "PASSIVE",
        "reference_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/liveness-reference.jpg",
        "video_url": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/liveness-video.webm",
        "face_quality": 92.5,
        "warnings": [],
        "matches": []
      },
      "liveness_checks": [
        {
          "status": "Approved",
          "node_id": "feature_liveness",
          "score": 98,
          "method": "PASSIVE",
          "reference_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/liveness-reference.jpg",
          "video_url": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/liveness-video.webm",
          "face_quality": 92.5,
          "warnings": [],
          "matches": []
        }
      ],
      "face_match": {
        "status": "Approved",
        "node_id": "feature_face_match",
        "score": 95,
        "source_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-front.jpg",
        "target_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/selfie.jpg",
        "warnings": []
      },
      "face_matches": [
        {
          "status": "Approved",
          "node_id": "feature_face_match",
          "score": 95,
          "source_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-front.jpg",
          "target_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/selfie.jpg",
          "warnings": []
        }
      ],
      "aml_screening": {
        "status": "Approved",
        "node_id": "feature_aml",
        "warnings": []
      },
      "aml_screenings": [
        {
          "status": "Approved",
          "node_id": "feature_aml",
          "warnings": []
        }
      ],
      "ip_analysis": {
        "status": "Approved",
        "node_id": "feature_ip_analysis",
        "ip_address": "203.0.113.10",
        "country": "US",
        "region": "New York",
        "city": "New York",
        "is_vpn": false,
        "is_proxy": false,
        "warnings": []
      },
      "ip_analyses": [
        {
          "status": "Approved",
          "node_id": "feature_ip_analysis",
          "ip_address": "203.0.113.10",
          "country": "US",
          "region": "New York",
          "city": "New York",
          "is_vpn": false,
          "is_proxy": false,
          "warnings": []
        }
      ]
    }
  }
}
```

**Additional Fields**:

* `entity`: Full Gu1 entity record at send time (all columns; dates as ISO strings)
* `verifiedAt`: Timestamp when verification was approved
* `extractedData`: Personal information extracted from the document
* `verifiedFields`: Array of fields that were successfully verified
* `warnings`: Array of any warnings detected during verification (empty if approved)
* `decision`: Verification results for each check

**Use case**: Activate customer account and grant access to services.

### kyc.validation\_rejected

Sent when verification fails. **`entity` is included** (current entity row in Gu1; typically unchanged by KYC on rejection).

```json theme={null}
{
  "event": "kyc.validation_rejected",
  "timestamp": "2025-01-07T11:00:00Z",
  "organizationId": "org-123",
  "payload": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "entityId": "123e4567-e89b-12d3-a456-426614174000",
    "entity": {
      "id": "123e4567-e89b-12d3-a456-426614174000",
      "externalId": "customer_xyz789",
      "name": "John Doe",
      "type": "person",
      "entityData": {},
      "attributes": {}
    },
    "status": "rejected",
    "verifiedAt": "2025-01-07T11:00:00Z",
    "extractedData": {},
    "verifiedFields": [],
    "warnings": [
      "Document authenticity check failed",
      "Face match confidence low",
      "Liveness detection failed"
    ],
    "decision": {
      "status": "Declined",
      "workflow_type": "standard",
      "session_id": "7c0fa22d-0cfa-4a78-8090-7c842397e788",
      "session_number": 921,
      "features": ["ID_VERIFICATION", "LIVENESS", "FACE_MATCH"],
      "images": {
        "documentFront": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-front.jpg",
        "selfie": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/selfie.jpg"
      },
      "id_verification": {
        "status": "Declined",
        "node_id": "feature_ocr",
        "document_type": "Passport",
        "document_number": "AB123456",
        "first_name": "John",
        "last_name": "Doe",
        "full_name": "John Doe",
        "front_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-front.jpg",
        "portrait_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/selfie.jpg",
        "warnings": [
          {
            "risk": "DOCUMENT_AUTHENTICITY_FAILED",
            "feature": "ID_VERIFICATION",
            "short_description": "Document authenticity could not be verified",
            "long_description": "The document failed authenticity checks.",
            "log_type": "error"
          }
        ],
        "matches": []
      },
      "id_verifications": [
        {
          "status": "Declined",
          "node_id": "feature_ocr",
          "document_type": "Passport",
          "document_number": "AB123456",
          "first_name": "John",
          "last_name": "Doe",
          "full_name": "John Doe",
          "front_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-front.jpg",
          "portrait_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/selfie.jpg",
          "warnings": [
            {
              "risk": "DOCUMENT_AUTHENTICITY_FAILED",
              "feature": "ID_VERIFICATION",
              "short_description": "Document authenticity could not be verified",
              "long_description": "The document failed authenticity checks.",
              "log_type": "error"
            }
          ],
          "matches": []
        }
      ],
      "liveness": {
        "status": "Declined",
        "node_id": "feature_liveness",
        "score": 42,
        "method": "PASSIVE",
        "reference_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/liveness-reference.jpg",
        "warnings": [
          {
            "risk": "LIVENESS_FAILED",
            "feature": "LIVENESS",
            "short_description": "Liveness detection failed",
            "long_description": "The liveness check did not pass the required threshold.",
            "log_type": "error"
          }
        ],
        "matches": []
      },
      "liveness_checks": [
        {
          "status": "Declined",
          "node_id": "feature_liveness",
          "score": 42,
          "method": "PASSIVE",
          "reference_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/liveness-reference.jpg",
          "warnings": [
            {
              "risk": "LIVENESS_FAILED",
              "feature": "LIVENESS",
              "short_description": "Liveness detection failed",
              "long_description": "The liveness check did not pass the required threshold.",
              "log_type": "error"
            }
          ],
          "matches": []
        }
      ],
      "face_match": {
        "status": "Declined",
        "node_id": "feature_face_match",
        "score": 38,
        "source_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-front.jpg",
        "target_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/selfie.jpg",
        "warnings": [
          {
            "risk": "FACE_MATCH_LOW_CONFIDENCE",
            "feature": "FACE_MATCH",
            "short_description": "Face match confidence below threshold",
            "long_description": "The selfie did not match the document portrait with sufficient confidence.",
            "log_type": "error"
          }
        ]
      },
      "face_matches": [
        {
          "status": "Declined",
          "node_id": "feature_face_match",
          "score": 38,
          "source_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-front.jpg",
          "target_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/selfie.jpg",
          "warnings": [
            {
              "risk": "FACE_MATCH_LOW_CONFIDENCE",
              "feature": "FACE_MATCH",
              "short_description": "Face match confidence below threshold",
              "long_description": "The selfie did not match the document portrait with sufficient confidence.",
              "log_type": "error"
            }
          ]
        }
      ]
    },
    "rejectionReason": "Document authenticity could not be verified"
  }
}
```

**Additional Fields**:

* `entity`: Full Gu1 entity record at send time
* `warnings`: Array of reasons why verification failed
* `rejectionReason`: Primary reason for rejection

**Use case**: Notify customer that verification failed and provide guidance on next steps.

### kyc.validation\_abandoned

Sent when a customer starts but doesn't complete the verification. **`entity` is not included.**

```json theme={null}
{
  "event": "kyc.validation_abandoned",
  "timestamp": "2025-01-07T10:45:00Z",
  "organizationId": "org-123",
  "payload": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "entityId": "123e4567-e89b-12d3-a456-426614174000",
    "status": "abandoned",
    "abandonedAt": "2025-01-07T10:45:00Z",
    "lastStep": "document_upload"
  }
}
```

**Additional Fields**:

* `lastStep`: The last step the customer completed before abandoning

**Use case**: Send a reminder email to complete verification.

### kyc.validation\_expired

Sent when a validation session expires without completion. **`entity` is not included.**

```json theme={null}
{
  "event": "kyc.validation_expired",
  "timestamp": "2025-01-14T10:30:00Z",
  "organizationId": "org-123",
  "payload": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "entityId": "123e4567-e89b-12d3-a456-426614174000",
    "status": "expired",
    "expiresAt": "2025-01-14T10:30:00Z",
    "createdAt": "2025-01-07T10:30:00Z"
  }
}
```

**Use case**: Clean up pending validations and notify customer to restart verification.

### kyc.validation\_cancelled

Sent when a validation is manually cancelled by the organization. **`entity` is not included.**

```json theme={null}
{
  "event": "kyc.validation_cancelled",
  "timestamp": "2025-01-07T12:00:00Z",
  "organizationId": "org-123",
  "payload": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "entityId": "123e4567-e89b-12d3-a456-426614174000",
    "status": "cancelled",
    "cancelledAt": "2025-01-07T12:00:00Z",
    "cancelledBy": "user_123"
  }
}
```

**Use case**: Notify customer that validation was cancelled. Clean up associated resources and update status in your system.

## Code Examples

### Node.js - Handling KYC Events

```javascript theme={null}
const express = require('express');
const crypto = require('crypto');

const app = express();

app.use(express.json({
  verify: (req, res, buf) => {
    req.rawBody = buf.toString('utf8');
  }
}));

app.post('/webhooks/kyc', async (req, res) => {
  try {
    // Verify webhook signature (see security guide)
    const signature = req.headers['x-webhook-signature'];
    const webhookSecret = process.env.GU1_WEBHOOK_SECRET;

    if (!verifySignature(req.rawBody, signature, webhookSecret)) {
      console.error('Invalid webhook signature');
      return res.status(401).json({ error: 'Invalid signature' });
    }

    // Extract webhook data
    const { event, timestamp, organizationId, payload } = req.body;

    console.log('Received KYC webhook:', {
      event,
      validationId: payload.id,
      status: payload.status
    });

    // Process the webhook based on event type
    await handleKycWebhook(event, payload);

    // Return 200 to acknowledge receipt
    res.status(200).json({
      success: true,
      message: 'Webhook received'
    });
  } catch (error) {
    console.error('Webhook error:', error);
    res.status(500).json({
      error: error.message
    });
  }
});

async function handleKycWebhook(event, data) {
  const validationId = data.id;
  const { entityId, entity, status } = data;

  // `entity` is only present for kyc.validation_approved / kyc.validation_rejected

  switch (event) {
    case 'kyc.validation_created':
      console.log('KYC validation created:', validationId, entityId);
      await sendSessionLinkToCustomer(entityId, data.providerSessionUrl);
      break;

    case 'kyc.validation_in_progress':
      await notifyCustomerByEntityId(entityId, 'verification-started');
      break;

    case 'kyc.validation_in_review':
      await notifyComplianceTeam(entityId, validationId);
      await notifyCustomerByEntityId(entityId, 'verification-in-review');
      break;

    case 'kyc.validation_approved': {
      if (!entity) break;
      const { extractedData, verifiedFields } = data;
      await db.updateEntity(entity.externalId, {
        kycValidationId: validationId,
        kycStatus: status,
        verifiedData: extractedData,
        verifiedFields,
        verifiedAt: data.verifiedAt,
        isVerified: true,
        lastUpdated: new Date()
      });
      await activateCustomerAccount(entity.externalId);
      await notifyCustomer(entity.externalId, 'verification-approved');
      break;
    }

    case 'kyc.validation_rejected': {
      if (!entity) break;
      await db.updateEntity(entity.externalId, {
        kycValidationId: validationId,
        kycStatus: status,
        isVerified: false,
        rejectionReasons: data.warnings,
        rejectionReason: data.rejectionReason,
        lastUpdated: new Date()
      });
      await notifyCustomer(entity.externalId, 'verification-rejected', {
        reasons: data.warnings
      });
      break;
    }

    case 'kyc.validation_abandoned':
      await notifyCustomerByEntityId(entityId, 'verification-incomplete', {
        lastStep: data.lastStep
      });
      break;

    case 'kyc.validation_expired':
      await notifyCustomerByEntityId(entityId, 'verification-expired');
      await db.deleteValidation(validationId);
      break;

    case 'kyc.validation_cancelled':
      await notifyCustomerByEntityId(entityId, 'verification-cancelled');
      await db.deleteValidation(validationId);
      break;
  }
}

function verifySignature(rawBody, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  return signature === expectedSignature;
}

app.listen(3000);
```

### Python - Handling KYC Events

```python theme={null}
from flask import Flask, request, jsonify
import hmac
import hashlib
import logging

app = Flask(__name__)

@app.route('/webhooks/kyc', methods=['POST'])
def kyc_webhook():
    try:
        # Get signature from header
        signature = request.headers.get('X-Webhook-Signature')
        webhook_secret = os.getenv('GU1_WEBHOOK_SECRET')

        # Get raw body for signature verification
        raw_body = request.get_data(as_text=True)

        # Verify signature
        if not verify_signature(raw_body, signature, webhook_secret):
            logging.error('Invalid webhook signature')
            return jsonify({'error': 'Invalid signature'}), 401

        # Parse payload
        payload = request.json
        event = payload.get('event')
        data = payload.get('payload')

        logging.info(f'Received KYC webhook: {event}')

        # Process the webhook
        handle_kyc_webhook(event, data)

        return jsonify({
            'success': True,
            'message': 'Webhook received'
        }), 200

    except Exception as e:
        logging.error(f'Webhook error: {e}')
        return jsonify({
            'error': str(e)
        }), 500

def verify_signature(raw_body, signature, secret):
    """Verify HMAC SHA-256 signature"""
    expected_signature = hmac.new(
        secret.encode('utf-8'),
        raw_body.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature, expected_signature)

def handle_kyc_webhook(event, data):
    validation_id = data['id']
    entity_id = data['entityId']
    entity = data.get('entity')  # only for approved / rejected
    status = data.get('status')

    if event == 'kyc.validation_created':
        send_session_link_to_customer(entity_id, data.get('providerSessionUrl'))

    elif event == 'kyc.validation_in_review':
        notify_compliance_team(entity_id, validation_id)
        notify_customer_by_entity_id(entity_id, 'verification-in-review')

    elif event == 'kyc.validation_approved':
        if not entity:
            return
        db.update_entity(
            external_id=entity['externalId'],
            kyc_validation_id=validation_id,
            kyc_status=status,
            verified_data=data.get('extractedData'),
            verified_fields=data.get('verifiedFields'),
            verified_at=data.get('verifiedAt'),
            is_verified=True,
            last_updated=datetime.now()
        )
        activate_customer_account(entity['externalId'])
        notify_customer(entity['externalId'], 'verification-approved')

    elif event == 'kyc.validation_rejected':
        if not entity:
            return
        db.update_entity(
            external_id=entity['externalId'],
            kyc_validation_id=validation_id,
            kyc_status=status,
            is_verified=False,
            rejection_reasons=data.get('warnings'),
            last_updated=datetime.now()
        )
        notify_customer(entity['externalId'], 'verification-rejected')

    elif event == 'kyc.validation_abandoned':
        notify_customer_by_entity_id(entity_id, 'verification-incomplete')

    elif event == 'kyc.validation_expired':
        notify_customer_by_entity_id(entity_id, 'verification-expired')
        db.delete_validation(validation_id)

    elif event == 'kyc.validation_cancelled':
        notify_customer_by_entity_id(entity_id, 'verification-cancelled')
        db.delete_validation(validation_id)

if __name__ == '__main__':
    app.run(port=3000)
```

## Use Cases

### Use Case 1: Automatic Account Activation

Automatically activate customer accounts when KYC is approved:

```javascript theme={null}
async function handleKycApproved(data) {
  const { entity, extractedData, verifiedFields } = data;

  // Update customer record
  await db.customers.update({
    where: { externalId: entity.externalId },
    data: {
      kycStatus: 'approved',
      verifiedAt: new Date(),
      verifiedData: extractedData,
      isActive: true
    }
  });

  // Enable login
  await auth.enableUser(entity.externalId);

  // Grant access to services
  await services.grantAccess(entity.externalId, ['trading', 'withdrawal']);

  // Send welcome email
  await email.send({
    to: extractedData.email || entity.attributes?.email,
    subject: 'Welcome! Your account is verified',
    template: 'kyc-approved',
    data: {
      name: entity.name,
      verifiedAt: new Date().toISOString()
    }
  });

  // Log event
  await audit.log({
    action: 'kyc_approved',
    entityId: entity.id,
    timestamp: new Date()
  });
}
```

### Use Case 2: Compliance Monitoring

Track KYC rejections for compliance review:

```javascript theme={null}
async function handleKycRejected(data) {
  const { entity, warnings, rejectionReason } = data;

  // Log rejection
  await compliance.logRejection({
    entityId: entity.id,
    externalId: entity.externalId,
    reasons: warnings,
    primaryReason: rejectionReason,
    timestamp: new Date()
  });

  // Alert compliance team if multiple rejections
  const rejectCount = await db.rejections.count({
    where: { entityId: entity.id }
  });

  if (rejectCount >= 3) {
    await slack.send({
      channel: '#compliance-alerts',
      message: `🚨 Multiple KYC rejections for ${entity.name} (${entity.externalId})`,
      fields: {
        'Entity ID': entity.id,
        'Rejection Count': rejectCount,
        'Latest Reason': rejectionReason
      }
    });
  }

  // Notify customer with guidance
  await email.send({
    to: entity.attributes?.email,
    subject: 'Verification unsuccessful',
    template: 'kyc-rejected',
    data: {
      name: entity.name,
      reasons: warnings,
      supportUrl: 'https://support.example.com/kyc'
    }
  });
}
```

### Use Case 3: Abandoned Verification Recovery

Send reminders to customers who abandon verification:

```javascript theme={null}
async function handleKycAbandoned(data) {
  const { entityId, id: validationId, lastStep, providerSessionUrl } = data;

  // Schedule reminder email (resolve contact via entityId in your DB)
  await queue.scheduleEmail({
    to: await resolveCustomerEmail(entityId),
    subject: 'Complete your verification',
    template: 'kyc-reminder',
    data: {
      lastStep,
      validationUrl: providerSessionUrl
    },
    sendAt: new Date(Date.now() + 24 * 60 * 60 * 1000) // Send in 24 hours
  });

  await analytics.track({
    event: 'kyc_abandoned',
    entityId,
    validationId,
    lastStep,
    timestamp: new Date()
  });
}
```

## Best Practices

<AccordionGroup>
  <Accordion title="Use entity.externalId for Lookup (terminal events)">
    For `kyc.validation_approved` and `kyc.validation_rejected`, `payload.entity` includes the full entity row—use `entity.externalId` to match the record you created in Gu1.

    ```javascript theme={null}
    const customer = await db.findCustomer({
      externalId: webhook.payload.entity.externalId
    });
    ```

    For other KYC events, use `payload.entityId` (and your own mapping) until you receive a terminal event.
  </Accordion>

  <Accordion title="Store Validation IDs">
    Store the validation UUID from Gu1 (`payload.id`) in your database so you can query validation details later.

    ```javascript theme={null}
    await db.updateCustomer(customer.id, {
      kycValidationId: payload.id,
      kycStatus: payload.status
    });
    ```
  </Accordion>

  <Accordion title="Handle Idempotency">
    You might receive the same webhook multiple times. Use `payload.id` and the `event` type to deduplicate.

    ```javascript theme={null}
    async function handleWebhook(webhook) {
      const alreadyProcessed = await db.checkWebhookProcessed(
        webhook.payload.id,
        webhook.event
      );

      if (alreadyProcessed) {
        return; // Skip duplicate
      }

      await processValidation(webhook.payload);

      await db.markWebhookProcessed(
        webhook.payload.id,
        webhook.event
      );
    }
    ```
  </Accordion>

  <Accordion title="Return 200 Quickly">
    Always return a 200 status code as quickly as possible to acknowledge receipt. Process the webhook asynchronously if needed.

    ```javascript theme={null}
    app.post('/webhooks/kyc', async (req, res) => {
      // Acknowledge immediately
      res.status(200).send('OK');

      // Process asynchronously
      processWebhook(req.body).catch(console.error);
    });
    ```
  </Accordion>

  <Accordion title="Verify Signatures">
    Always verify the `X-Webhook-Signature` header to ensure the webhook is authentic. See the [security guide](/en/webhooks/security) for details.

    ```javascript theme={null}
    const signature = req.headers['x-webhook-signature'];
    if (!verifySignature(req.rawBody, signature, secret)) {
      return res.status(401).json({ error: 'Invalid signature' });
    }
    ```
  </Accordion>

  <Accordion title="Handle Errors Gracefully">
    If processing fails, log the error but still return 200 to prevent retries. Store failed webhooks for manual review.

    ```javascript theme={null}
    try {
      await processWebhook(payload);
    } catch (error) {
      await db.saveFailedWebhook({
        payload,
        error: error.message,
        receivedAt: new Date()
      });

      // Still return 200
      res.status(200).json({ success: false });
    }
    ```
  </Accordion>
</AccordionGroup>

## Troubleshooting

<AccordionGroup>
  <Accordion title="Not Receiving Webhooks">
    **Check these items:**

    * Webhook URL is publicly accessible via HTTPS
    * Webhook is configured and **enabled** in dashboard
    * Subscribed to correct KYC event types
    * Endpoint returns 200 status code within 30 seconds
    * Check server logs for incoming requests
  </Accordion>

  <Accordion title="Missing extractedData">
    `extractedData` and `verifiedFields` are only included in:

    * `kyc.validation_approved`
    * `kyc.validation_rejected`

    They are not present in other event types like `validation_created` or `validation_in_progress`.
  </Accordion>

  <Accordion title="Signature Verification Failing">
    **Common causes:**

    * Using wrong secret (check dashboard for current secret)
    * Verifying signature on parsed JSON instead of raw body
    * Re-verifying from **Webhook Monitor** JSON (pretty-print / `JSON.stringify` ≠ signed bytes)
    * Middleware that mutates the body before verification (some payloads fail, others pass)
    * Secret not properly saved after webhook creation
    * Encoding issues (ensure UTF-8)

    See [Webhook Security](/en/webhooks/security) — especially *Raw body vs parsed JSON*, *Webhook history in the dashboard*, and *Intermittent signature failures*.
  </Accordion>

  <Accordion title="Receiving Duplicate Webhooks">
    This is normal behavior. Webhooks may be sent multiple times due to network issues, timeouts, or retries.

    **Always implement idempotency** using the webhook `payload.id` (validation UUID) and `event` type.
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Entity Events" icon="user" href="/en/webhooks/events/entity-events">
    Handle entity lifecycle events
  </Card>

  <Card title="Rule Events" icon="gavel" href="/en/webhooks/events/rule-events">
    Process compliance rule triggers
  </Card>

  <Card title="Webhook Security" icon="shield" href="/en/webhooks/security">
    Secure your webhook endpoints
  </Card>

  <Card title="Configuration" icon="gear" href="/en/webhooks/configuration">
    Configure webhook settings
  </Card>
</CardGroup>
