# API Documentation

REST API reference for workflow-based identity verification: authentication, verifications, bans, workflows, billing and signed webhooks.

_API Reference · v1_

REST API for workflow-based verification management. Every request authenticates with a server-side API key sent in the `X-Authorization` header, next to the ID the key belongs to.

**On This Page**

- [Base URL & Authentication](#base-url--authentication)
- [Conventions](#conventions)
  - [HTTP Status Codes](#http-status-codes)
  - [Resource Model](#resource-model)
- [Action Mapping](#action-mapping)
- [Verifications](#verifications)
  - [Create Verification](#create-verification) — `POST /verifications`
  - [Retrieve Verification](#retrieve-verification) — `GET /verifications/{id}`
  - [List Verifications](#list-verifications) — `GET /verifications`
  - [Update Verification Status](#update-verification-status) — `PATCH /verifications/{id}/status`
  - [Update Verification Data](#update-verification-data) — `PATCH /verifications/{id}/data`
  - [Delete Verification](#delete-verification) — `DELETE /verifications/{id}`
  - [Create Verification PDF](#create-verification-pdf) — `POST /verifications/{id}/pdf`
- [Bans](#bans)
  - [List Bans](#list-bans) — `GET /bans`
  - [Create Ban](#create-ban) — `POST /bans`
  - [Delete Ban](#delete-ban) — `DELETE /bans/{id}`
- [Workflows](#workflows)
  - [List Workflows](#list-workflows) — `GET /workflows`
  - [Create Workflow](#create-workflow) — `POST /workflows`
  - [Retrieve Workflow](#retrieve-workflow) — `GET /workflows/{id}`
  - [Update Workflow](#update-workflow) — `PATCH /workflows/{id}`
  - [Deleting Workflows](#deleting-workflows)
- [Billing](#billing)
  - [Get Balance](#get-balance) — `GET /billing/balance`
  - [Balance Top-Up](#balance-top-up) — `POST /billing/top-up`
- [Webhooks](#webhooks)
  - [`verification.status`](#verificationstatus)
  - [Verifying Deliveries](#verifying-deliveries)
  - [`verification.status_update`](#verificationstatus_update)
- [Security & Operations](#security--operations)

## Base URL & Authentication

Send every request over HTTPS to the base URL, with two headers: an API key and the ID it belongs to. Verification endpoints take a workflow’s key; ban and workflow endpoints take your organization’s key.

### Base URL

```text
https://api.osvoy.com/api/v1
```

### Headers

| Endpoints                                | Send These Headers                                                                  |
| ---------------------------------------- | ----------------------------------------------------------------------------------- |
| **Verifications**<br>All seven endpoints | `X-Authorization: <workflow API key>`<br>`X-Workflow-ID: <workflow ID>`             |
| **Bans, Workflows**                      | `X-Authorization: <organization API key>`<br>`X-Organization-ID: <organization ID>` |
| **Requests with a body**                 | `Content-Type: application/json`                                                    |

### Example Request

```bash
curl -X POST https://api.osvoy.com/api/v1/verifications \
  -H "X-Authorization: $OSVOY_WORKFLOW_KEY" \
  -H "X-Workflow-ID: <workflow-id>" \
  -H "Content-Type: application/json" \
  -d '{"vendor_data": "cust_20931847"}'
```

> **Note:** Keys are shown once — when they’re generated or rotated. Keep them on your server; never ship one in a browser or a mobile app.

## Conventions

Shared rules across every endpoint — base URL, auth, IDs, pagination, rate limits and error format.

| Convention         | Value                                                                                                                      |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------- |
| **Base URL**       | `https://api.osvoy.com/api/v1`                                                                                                    |
| **Authentication** | `X-Authorization` plus `X-Workflow-ID` or `X-Organization-ID` — see [Base URL & Authentication](#base-url--authentication) |
| **Content Type**   | `application/json`                                                                                                         |
| **ID Format**      | UUID v4                                                                                                                    |
| **Timestamps**     | ISO 8601, UTC — `2026-04-23T14:30:00Z`                                                                                     |
| **Pagination**     | `?page=<number>`                                                                                                           |
| **Filtering**      | `?status=passed&workflow_id=…`                                                                                             |
| **Idempotency**    | The last request sent is the one applied.                                                                                  |
| **Rate Limit**     | 100 requests per minute per organization (burst 200). `X-RateLimit-*` headers are returned.                                |
| **Errors**         | RFC 7807 Problem Details with `code` and `message`.                                                                        |

### HTTP Status Codes

| Code                           | Meaning                                     |
| ------------------------------ | ------------------------------------------- |
| **`200 OK`**                   | Successful read / update / resource created |
| **`202 Accepted`**             | PDF generation queued                       |
| **`204 No Content`**           | Successful delete                           |
| **`400 Bad Request`**          | Validation error                            |
| **`401 Unauthorized`**         | Missing or invalid API key                  |
| **`402 Payment Required`**     | Insufficient credit balance                 |
| **`403 Forbidden`**            | Key lacks scope for action                  |
| **`404 Not Found`**            | Resource does not exist                     |
| **`409 Conflict`**             | Invalid state transition or duplicate       |
| **`422 Unprocessable Entity`** | Semantic validation failed                  |
| **`429 Too Many Requests`**    | Rate limit exceeded                         |
| **`5xx`**                      | Server error — retry with backoff           |

### Resource Model

```text
Verification  ── produced by ──>  Workflow
     │
     ├── status transitions
     │     (created → in_progress → manual_review → passed/failed/expired/cancelled)
     ├── verification data (document data, liveness results, checks, etc.)
     └── can be exported as PDF report

Ban           ── scoped by organization
Workflow      ── reusable, customizable template for hosted KYC flows
Balance       ── prepaid credits consumed per verification
```

## Action Mapping

All 17 actions mapped to REST endpoints across four groups: Verifications, Bans, Workflows and Billing.

| #   | Action                                                      | Method   | Path                         |
| --- | ----------------------------------------------------------- | -------- | ---------------------------- |
| 1   | [`create-verification`](#create-verification)               | `POST`   | `/verifications`             |
| 2   | [`retrieve-verification`](#retrieve-verification)           | `GET`    | `/verifications/{id}`        |
| 3   | [`list-verification`](#list-verifications)                  | `GET`    | `/verifications`             |
| 4   | [`update-verification-status`](#update-verification-status) | `PATCH`  | `/verifications/{id}/status` |
| 5   | [`update-verification-data`](#update-verification-data)     | `PATCH`  | `/verifications/{id}/data`   |
| 6   | [`delete-verification`](#delete-verification)               | `DELETE` | `/verifications/{id}`        |
| 7   | [`create-verification-pdf`](#create-verification-pdf)       | `POST`   | `/verifications/{id}/pdf`    |
| 8   | [`list-bans`](#list-bans)                                   | `GET`    | `/bans`                      |
| 9   | [`create-ban`](#create-ban)                                 | `POST`   | `/bans`                      |
| 10  | [`delete-ban`](#delete-ban)                                 | `DELETE` | `/bans/{id}`                 |
| 11  | [`list-workflows`](#list-workflows)                         | `GET`    | `/workflows`                 |
| 12  | [`create-workflow`](#create-workflow)                       | `POST`   | `/workflows`                 |
| 13  | [`retrieve-workflow`](#retrieve-workflow)                   | `GET`    | `/workflows/{id}`            |
| 14  | [`update-workflow`](#update-workflow)                       | `PATCH`  | `/workflows/{id}`            |
| 15  | [`delete-workflow`](#deleting-workflows)                    | —        | —                            |
| 16  | [`get-balance`](#get-balance)                               | `GET`    | `/billing/balance`           |
| 17  | [`balance-top-up`](#balance-top-up)                         | `POST`   | `/billing/top-up`            |

## Verifications

A verification is an instance of a workflow applied to one end user. Creating one returns a hosted `verification_url` you redirect the user to.

### Create Verification

```http
POST /verifications
```

Action: `create-verification` · [Workflow Key](#base-url--authentication)

Creates a new verification and returns a short-lived hosted `verification_url` — it lives for 1 hour.

**Request Body**

```json
{
  "workflow_id": "8b2f3c4d-0a1e-4b2f-9c3d-1e2f3a4b5c6d",
  "vendor_data": "user-42"
}
```

**Response** · `200 OK`

```json
{
  "id": "bbd23629-1a24-4d26-bb12-a905b9ae85b8",
  "workflow_id": "8b2f3c4d-0a1e-4b2f-9c3d-1e2f3a4b5c6d",
  "vendor_data": "user-42",
  "status": "created",
  "verification_url": "https://verify.yourdomain.com/bbd23629-1a24-4d26-bb12-a905b9ae85b8",
  "created_at": "2026-04-23T13:11:13.625Z",
  "expires_at": "2026-04-23T14:11:13.625Z"
}
```

> **Errors:** `400` invalid workflow · `402` insufficient balance.

### Retrieve Verification

```http
GET /verifications/{id}
```

Action: `retrieve-verification` · [Workflow Key](#base-url--authentication)

Returns the full verification record, including extracted data once the verification is complete.

**Response** · `200 OK`

```json
{
  "id": "84f56ba9-fcef-47b0-87f5-1a07ae70396e",
  "workflow_id": "8b2f3c4d-0a1e-4b2f-9c3d-1e2f3a4b5c6d",
  "vendor_data": null,
  "status": "passed",
  "created_by_api": false,
  "warnings": [],
  "files": [],
  "verification_data": {
    "document": {
      "document_type": "id_card",
      "document_number": null,
      "personal_number": null,
      "issuing_state": "TUR",
      "first_name": null,
      "last_name": null,
      "date_of_birth": null,
      "expiration_date": null,
      "gender": null,
      "is_document_verified": true
    },
    "liveness": {
      "active_score": null,
      "passive_score": null,
      "is_live": true
    },
    "face_match": {
      "face_similarity": null,
      "is_face_verified": true
    },
    "ip_analysis": {
      "time_zone": null,
      "isp": null,
      "organization": null,
      "is_datacenter": false,
      "is_vpn_tor": false
    }
  },
  "created_at": "2026-04-20T12:41:35.787Z",
  "finished_at": "2026-04-21T10:18:00.167Z",
  "webhook_sent_successfully": false
}
```

### List Verifications

```http
GET /verifications
```

Action: `list-verification` · [Workflow Key](#base-url--authentication)

Paginated, filterable list of verifications.

**Query Parameters**

| Name                 | Type       | Required | Description                                                                           |
| -------------------- | ---------- | -------- | ------------------------------------------------------------------------------------- |
| **`page`**           | `integer`  | Optional | Page number                                                                           |
| **`status`**         | `enum`     | Optional | `created`, `in_progress`, `manual_review`, `passed`, `failed`, `expired`, `cancelled` |
| **`workflow_id`**    | `uuid`     | Optional | Filter by workflow                                                                    |
| **`vendor_data`**    | `string`   | Optional | Filter by your user identifier                                                        |
| **`issuing_state`**  | `string`   | Optional | 3-letter country code (e.g. `TUR` for Türkiye)                                        |
| **`created_after`**  | `datetime` | Optional | ISO 8601                                                                              |
| **`created_before`** | `datetime` | Optional | ISO 8601                                                                              |

**Response** · `200 OK`

```json
{
  "count": 22,
  "prevPage": 1,
  "nextPage": 3,
  "results": [
    {
      "id": "84f56ba9-fcef-47b0-87f5-1a07ae70396e",
      "status": "passed",
      "workflow_id": "8b2f3c4d-0a1e-4b2f-9c3d-1e2f3a4b5c6d",
      "vendor_data": "user-42",
      "created_at": "2026-04-20T12:41:35.787Z"
    }
  ]
}
```

> **Note:** `prevPage` is `null` on the first page; `nextPage` is `null` on the last.

### Update Verification Status

```http
PATCH /verifications/{id}/status
```

Action: `update-verification-status` · [Workflow Key](#base-url--authentication)

Manual review override. Only these transitions are allowed: `manual_review → passed | failed`, `passed → failed`, `failed → passed`. Every transition is audit logged.

**Body Parameters**

| Name          | Type     | Required     | Description                                                        |
| ------------- | -------- | ------------ | ------------------------------------------------------------------ |
| **`status`**  | `enum`   | **Required** | `passed` or `failed`                                               |
| **`comment`** | `string` | Optional     | Why you decided. Kept as the session’s note, up to 100 characters. |

**Request Body**

```json
{
  "status": "passed",
  "comment": "Verified against secondary documentation by agent."
}
```

**Response** · `200 OK`

```json
{
  "id": "84f56ba9-fcef-47b0-87f5-1a07ae70396e",
  "status": "passed",
  "updated_at": "2026-04-23T15:00:00Z"
}
```

> **Errors:** `409` on an invalid transition (e.g. `created → passed`).

### Update Verification Data

```http
PATCH /verifications/{id}/data
```

Action: `update-verification-data` · [Workflow Key](#base-url--authentication)

Updates writable fields on a verification — typically `vendor_data` and manual corrections to extracted fields. Core biometric and document results are immutable.

**Request Body**

```json
{
  "vendor_data": "user-42-migrated",
  "document_data": {
    "document_type": "id_card",
    "document_number": "X12345678",
    "personal_number": "12345678901",
    "issuing_state": "TUR",
    "first_name": "Mehmet Ali",
    "last_name": "Yılmaz",
    "date_of_birth": "1990-06-15",
    "expiration_date": "2030-06-14",
    "gender": "M"
  }
}
```

**Response** · `200 OK`

```json
{
  "id": "84f56ba9-fcef-47b0-87f5-1a07ae70396e",
  "vendor_data": "user-42-migrated",
  "updated_at": "2026-04-23T15:05:00Z"
}
```

> **Errors:** `422` if a field is immutable or has an invalid value.

### Delete Verification

```http
DELETE /verifications/{id}
```

Action: `delete-verification` · [Workflow Key](#base-url--authentication)

GDPR-style hard delete with no recovery. Removes the record and its artefacts (images, videos, PDFs).

> **Errors:** `404` not found · `409` if the verification is currently `in_progress`.

### Create Verification PDF

```http
POST /verifications/{id}/pdf
```

Action: `create-verification-pdf` · [Workflow Key](#base-url--authentication)

Generates a signed PDF report of the verification. The PDF is deleted 3 hours after creation.

**Body Parameters**

| Name           | Type   | Required     | Description  |
| -------------- | ------ | ------------ | ------------ |
| **`language`** | `enum` | **Required** | `en` or `tr` |

**Request Body**

```json
{
  "language": "en"
}
```

**Response** · `200 OK`

```json
{
  "pdf_url": "https://cdn.osvoy.com/pdf/pdf_01HX....pdf",
  "expires_at": "2026-04-24T14:30:00Z"
}
```

> **Note:** `language` accepts `en` or `tr`.

## Bans

A ban blocks a subject — by `vendor_data`, `document_number`, `personal_number` or `ip` — from being approved in future verifications. Any matching session auto-flips to `failed`, with an audit log entry.

### List Bans

```http
GET /bans
```

Action: `list-bans` · [Organization Key](#base-url--authentication)

Lists ban entries, optionally filtered by data type and an exact-match lookup.

**Query Parameters**

| Name            | Type      | Required | Description                                               |
| --------------- | --------- | -------- | --------------------------------------------------------- |
| **`page`**      | `integer` | Optional | Pagination                                                |
| **`data_type`** | `enum`    | Optional | `vendor_data`, `personal_number`, `document_number`, `ip` |
| **`data`**      | `string`  | Optional | Exact-match lookup                                        |

**Response** · `200 OK`

```json
{
  "count": 22,
  "prevPage": 1,
  "nextPage": 3,
  "results": [
    {
      "id": 1,
      "data_type": "document_number",
      "data": "X12345678",
      "reason": "Confirmed fraudulent document",
      "created_at": "2026-04-20T09:10:00Z"
    }
  ]
}
```

### Create Ban

```http
POST /bans
```

Action: `create-ban` · [Organization Key](#base-url--authentication)

Blocks a subject by `vendor_data`, `document_number`, `personal_number` or `ip`. Any matching session auto-flips to `failed`, with an audit log entry.

**Body Parameters**

| Name            | Type     | Required     | Description                                               |
| --------------- | -------- | ------------ | --------------------------------------------------------- |
| **`data_type`** | `enum`   | **Required** | `vendor_data`, `personal_number`, `document_number`, `ip` |
| **`data`**      | `string` | **Required** | The value to block, up to 500 characters.                 |
| **`reason`**    | `string` | Optional     | Why it’s blocked, up to 500 characters.                   |

**Request Body**

```json
{
  "data_type": "document_number",
  "data": "X12345678",
  "reason": "Confirmed fraudulent document"
}
```

**Response** · `200 OK`

```json
{
  "id": 1,
  "data_type": "document_number",
  "data": "X12345678",
  "reason": "Confirmed fraudulent document",
  "created_at": "2026-04-20T09:10:00Z"
}
```

> **Errors:** `409` if the same subject is already banned.

### Delete Ban

```http
DELETE /bans/{id}
```

Action: `delete-ban` · [Organization Key](#base-url--authentication)

Removes a ban. Verifications it already declined stay declined.

## Workflows

A workflow is a reusable template that defines which verification modules run, in what order, and with what decision rules.

### List Workflows

```http
GET /workflows
```

Action: `list-workflows` · [Organization Key](#base-url--authentication)

**Query Parameters**

| Name        | Type      | Required | Description      |
| ----------- | --------- | -------- | ---------------- |
| **`page`**  | `integer` | Optional | Page number      |
| **`limit`** | `integer` | Optional | Results per page |

**Response** · `200 OK`

```json
{
  "results": [
    {
      "id": "8b2f3c4d-0a1e-4b2f-9c3d-1e2f3a4b5c6d",
      "name": "Standard KYC",
      "created_at": "2026-01-10T10:00:00Z",
      "updated_at": "2026-04-01T12:00:00Z"
    }
  ]
}
```

### Create Workflow

```http
POST /workflows
```

Action: `create-workflow` · [Organization Key](#base-url--authentication)

Workflows are reusable templates defining which verification modules run, in what order, and with what decision rules. `id_verification.maximum_age` is a year of birth: anyone born earlier is rejected.

**Request Body**

```json
{
  "name": "Standard KYC",
  "primary_color": "#2563EB",
  "font": "inter",
  "redirection_url": "https://example.com",
  "redirection_enabled": true,
  "id_verification": {
    "minimum_age": 18,
    "maximum_age": 1961
  },
  "liveness": {
    "method": "3d_action",
    "liveness_3d_action_order": ["left", "right", "up"],
    "active_decline": 30,
    "active_review": 60,
    "passive_decline": 30,
    "passive_review": 60
  },
  "face_match": {
    "decline_max": 30,
    "review_max": 60
  },
  "ip_analysis": {
    "ip_mismatch": "review",
    "tor_vpn": "decline",
    "expected_ip": "review"
  }
}
```

**Response** · `200 OK`

```json
{
  "id": "8b2f3c4d-0a1e-4b2f-9c3d-1e2f3a4b5c6d",
  "name": "Standard KYC",
  "created_at": "2026-04-23T13:11:13.625Z"
}
```

### Retrieve Workflow

```http
GET /workflows/{id}
```

Action: `retrieve-workflow` · [Organization Key](#base-url--authentication)

Returns the full workflow, including modules, configs and decision rules. Same shape as the create response.

### Update Workflow

```http
PATCH /workflows/{id}
```

Action: `update-workflow` · [Organization Key](#base-url--authentication)

Partial update — only the supplied fields change, and a settings object you send is merged into the stored one.

**Request Body**

```json
{
  "name": "Standard KYC (v2)",
  "id_verification": {
    "minimum_age": 21
  }
}
```

**Response** · `200 OK`

```json
{
  "id": "8b2f3c4d-0a1e-4b2f-9c3d-1e2f3a4b5c6d",
  "name": "Standard KYC (v2)",
  "updated_at": "2026-04-23T15:10:00Z"
}
```

> **Errors:** `409` on a conflicting update.

### Deleting Workflows

> **Warning:** `delete-workflow` — deleting a workflow through the API isn’t allowed, to prevent accidental data loss. Delete one from the Workflows page instead.

## Billing

Prepaid credits are consumed per verification. Top up with a saved payment method or a one-time payment token.

### Get Balance

```http
GET /billing/balance
```

Action: `get-balance`

Each verification module has a credit cost (`id_verification` = 10, `liveness` = 5, `aml` = 3). The cost is deducted when a verification is created and refunded if it expires unused.

**Response** · `200 OK`

```json
{
  "balance": 4275,
  "currency": "credits",
  "last_updated": "2026-04-23T14:25:00Z"
}
```

### Balance Top-Up

```http
POST /billing/top-up
```

Action: `balance-top-up`

Purchases credits, charged to a saved payment method on the account or a one-time payment token.

**Request Body**

```json
{
  "amount": 10000
}
```

**Response** · `200 OK`

```json
{
  "transaction_url": "https://api.osvoy.com/txn_01HX..."
}
```

> **Errors:** `402` if the payment method fails · `409` on a duplicate `idempotency_key`.

## Webhooks

Register destination URLs out of band (console or a separate endpoint) and receive signed POSTs. Each request is signed with HMAC-SHA256 in `X-Signature`.

### `verification.status`

_Event_

```http
POST <your-webhook-url>
```

Sent whenever a verification transitions to a terminal state.

**Payload**

```json
{
  "session_id": "sess_8f3a2c1d-4b5e-6f7a-8b9c-0d1e2f3a4b5c",
  "status": "passed",
  "timestamp": "2026-05-15T14:32:18Z",
  "vendor_data": "customer_ref_98765",
  "webhook_type": "status.updated",
  "workflow_id": 3,
  "created_at": "2026-05-15T14:28:42Z",
  "webhook_url": "https://api.example.com/webhooks/kyc",
  "redirect_url": "https://app.example.com/verification/complete",
  "face_match": {
    "similarity_score": 0.82,
    "verified": true,
    "selfie_image_url": "https://cdn.example.com/sessions/sess_8f3a2c1d/selfie.jpg",
    "warnings": [
      {
        "label": "face-features-match-reference-image",
        "type": "danger"
      }
    ]
  },
  "document": {
    "front_side_image_url": "https://cdn.example.com/sessions/sess_8f3a2c1d/doc_front.jpg",
    "front_side_video_url": "https://cdn.example.com/sessions/sess_8f3a2c1d/doc_front.mp4",
    "back_side_image_url": "https://cdn.example.com/sessions/sess_8f3a2c1d/doc_back.jpg",
    "back_side_video_url": "https://cdn.example.com/sessions/sess_8f3a2c1d/doc_back.mp4",
    "face_image_url": "https://cdn.example.com/sessions/sess_8f3a2c1d/doc_face.jpg",
    "issuing_state": "TR",
    "nationality": "TR",
    "document_type": "id_card",
    "document_number": "A12B34567",
    "personal_number": "12345678901",
    "first_name": "Mehmet Ali",
    "last_name": "Yılmaz",
    "full_name": "Mehmet Ali Yılmaz",
    "date_of_birth": "1990-06-15",
    "expiration_date": "2030-06-14",
    "gender": "M",
    "warnings": [
      {
        "label": "user-meets-minimum-age-requirement",
        "type": "danger"
      }
    ],
    "verified_by_gov_db": true
  },
  "ip_analysis": {
    "location": "Istanbul, Turkey",
    "time_zone": "Europe/Istanbul",
    "isp": "Turk Telekom",
    "organization": "TTNet A.S.",
    "is_vpn_tor": false,
    "is_datacenter": false,
    "device": "iPhone 14 Pro",
    "browser": "Safari 17.4",
    "os_name": "iOS",
    "platform": "mobile",
    "verified": true,
    "warnings": []
  },
  "liveness": {
    "verified": true,
    "selfie_image_url": "https://cdn.example.com/sessions/sess_8f3a2c1d/liveness_selfie.jpg",
    "selfie_video_url": "https://cdn.example.com/sessions/sess_8f3a2c1d/liveness_selfie.mp4",
    "warnings": [],
    "active_liveness": {
      "mode": "active",
      "actions_matched": ["left", "right"],
      "actions_requested": ["left", "right"],
      "liveness_score": 0.78
    },
    "passive_liveness": {
      "verified": true,
      "liveness_score": 0.71
    }
  }
}
```

> **Note:** Set the destination URL on each workflow, under Customization in the workflow editor. Deliveries go to that workflow’s URL.

### Verifying Deliveries

Every delivery carries three headers. Recompute the signature over the raw body with the workflow’s secret key and compare before you trust the payload.

**Delivery Headers**

| Header             | Description                                                                                       |
| ------------------ | ------------------------------------------------------------------------------------------------- |
| **`content-type`** | `application/json; charset=utf-8`                                                                 |
| **`x-timestamp`**  | Unix time, in seconds, when the delivery was sent. It isn’t part of the signature.                |
| **`x-signature`**  | Hex-encoded HMAC-SHA256 of the raw request body, keyed with the workflow’s secret key. No prefix. |

**Example · Node.js**

```js
import crypto from "node:crypto";

// rawBody: the exact bytes you received — check them before JSON.parse.
// secretKey: the workflow's secret key (sk_…), kept on your server.
export function isGenuineDelivery(rawBody, signatureHeader, secretKey) {
  const expected = crypto
    .createHmac("sha256", secretKey)
    .update(rawBody)
    .digest("hex");
  const received = String(signatureHeader || "");
  return (
    received.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected))
  );
}
```

> **Note:** A delivery that fails or doesn’t answer 2xx is retried up to 3 times, 60 seconds apart. Answer quickly and do slow work afterwards.

### `verification.status_update`

_Event · Opt-In_

```http
POST <your-webhook-url>
```

Sent on every status change — `created → in_progress → manual_review → passed` — so you can follow a session without polling. It carries what changed, not the results; those still arrive once in `verification.status`. Turned on per workflow: ask Support.

**Payload**

```json
{
  "session_id": "84f56ba9-fcef-47b0-87f5-1a07ae70396e",
  "webhook_type": "verification.status_update",
  "status": "manual_review",
  "previous_status": "in_progress",
  "is_final": false,
  "current_step": "liveness",
  "fail_reason": null,
  "reject_reason": null,
  "vendor_data": "customer_ref_98765",
  "workflow_id": 3,
  "webhook_url": "https://api.example.com/webhooks/kyc",
  "redirect_url": "https://app.example.com/verification/complete",
  "created_at": "2026-05-15T14:28:42Z",
  "finished_at": null,
  "changed_at": "2026-05-15T14:31:05Z",
  "timestamp": 1778855465
}
```

## Security & Operations

**API Keys**  
API keys are scoped per organization and manage all organization data. For testing or playground access, contact Support.

**PII at Rest**  
Encrypted with per-tenant keys. The `DELETE /verifications/{id}` action guarantees physical removal at the same time.

**Audit Log**  
Maintained for every POST, PATCH and DELETE — retrievable separately for a compliance review.

**Consent & Disclosure**  
The caller’s responsibility. Show the end user who is verifying them and link to your privacy policy before redirecting to the hosted `verification_url`.
