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

# API Reference

> Integrate real-time AML/CFT transaction screening, KYC/KYB verification, and compliance monitoring into your core banking application.

## Introduction

Verifow is an enterprise-grade Anti-Money Laundering (AML) and Combating the Financing of Terrorism (CFT) platform built for African financial institutions. Our API enables you to:

* **Screen transactions** through a multi-layer compliance pipeline in under 200ms
* **Verify identities** via BVN, NIN, and biometric liveness checks
* **Onboard businesses** with CAC lookup, director verification, and beneficial owner screening
* **Monitor compliance** via real-time dashboards and webhook notifications

This guide covers everything you need to integrate:

1. [Authentication](#authentication)
2. [Transaction Screening](#screen-transaction)
3. [KYC Verification](#kyc-verification)
4. [KYB Verification](#kyb-verification)
5. [Liveness Check](#liveness-check)
6. [How Screening Works](#how-screening-works)
7. [Webhooks](#webhooks)
8. [Error Handling](#error-handling)

***

## Authentication

The API supports two authentication methods. All endpoints (except `/health`) require authentication.

### Method 1: Bearer Token (Dashboard Sessions)

Used when calling the API from the dashboard or on behalf of a logged-in user.

```
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
```

Obtain a token by calling `POST /api/v1/auth/login` with email and password.

### Method 2: API Key (Backend Integrations)

Used for server-to-server integrations from your core banking system.

```
X-API-Key: rk_live_a1b2c3d4e5f6g7h8i9j0
```

Generate API keys from **Dashboard → API Keys**. API keys are tenant-scoped and carry the same permissions as the user who created them.

<Warning>
  **Keep your credentials secret.** Never expose tokens or API keys in
  client-side code (mobile apps, browser JavaScript). All API calls must
  originate from your backend servers.
</Warning>

***

## Transaction Screening

### Screen Transaction

`POST /api/v1/transactions/screen`

Submit a single transaction for real-time AML/CFT screening. The response includes the final outcome, risk breakdown per engine, triggered rules, and recommended actions.

**Request Body**

| Field                    | Type     | Required | Description                                                                                                                                  |
| ------------------------ | -------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `externalId`             | `string` | ✅        | Your unique transaction reference. Used for **idempotency** -sending the same ID twice returns the cached verdict.                           |
| `type`                   | `string` | ✅        | `CASH_DEPOSIT`, `CASH_WITHDRAWAL`, `TRANSFER`, `INTERNATIONAL_TRANSFER`, `POS`, `ATM`, `MOBILE`, `USSD`, `INTERNET_BANKING`                  |
| `channel`                | `string` | ✅        | `BRANCH`, `MOBILE_APP`, `INTERNET_BANKING`, `POS_TERMINAL`, `ATM`, `USSD`, `AGENT`, `API`                                                    |
| `amount`                 | `number` | ✅        | Amount in base currency unit (e.g. `5000000` = ₦5M).                                                                                         |
| `currency`               | `string` |          | ISO 4217 code. Defaults to `NGN`.                                                                                                            |
| `senderAccountNumber`    | `string` | ✅        | Sender's bank account number (NUBAN, 10 digits).                                                                                             |
| `senderBvn`              | `string` |          | Sender's BVN. **Strongly recommended** -enables KYC Verification scoring.                                                                    |
| `senderKycStatus`        | `string` |          | `VERIFIED`, `PENDING`, `IN_PROGRESS`, `REJECTED`, `EXPIRED`, `NONE`. Overrides DB lookup in HYBRID/EXTERNAL modes.                           |
| `senderKycTier`          | `string` |          | `TIER_1`, `TIER_2`, `TIER_3`. Required in EXTERNAL mode; enables CBN tier-based limits in rule engines.                                      |
| `senderKycVerifiedAt`    | `string` |          | ISO 8601 timestamp of last KYC verification.                                                                                                 |
| `senderKycExternalRef`   | `string` |          | Internal KYC reference (e.g. `CORE-BANK-KYC-78432`).                                                                                         |
| `senderKybStatus`        | `string` |          | `VERIFIED`, `PENDING`, `IN_PROGRESS`, `REJECTED`, `EXPIRED`, `NONE`. For business transactions; bypasses Verifow KYB lookup when `VERIFIED`. |
| `senderName`             | `string` | ✅        | Sender's full name. **Screened against global sanctions & PEP lists.**                                                                       |
| `senderBankCode`         | `string` |          | Sender's bank CBN code (e.g. `"058"` for GTBank).                                                                                            |
| `receiverAccountNumber`  | `string` |          | Receiver's account number.                                                                                                                   |
| `receiverName`           | `string` |          | Receiver's full name. **Also screened if provided.**                                                                                         |
| `receiverBvn`            | `string` |          | Receiver's BVN.                                                                                                                              |
| `receiverKycStatus`      | `string` |          | `VERIFIED`, `PENDING`, `IN_PROGRESS`, `REJECTED`, `EXPIRED`, `NONE`.                                                                         |
| `receiverKycVerifiedAt`  | `string` |          | ISO 8601 timestamp of last KYC verification.                                                                                                 |
| `receiverKycExternalRef` | `string` |          | Internal KYC reference for the receiver.                                                                                                     |
| `receiverBankCode`       | `string` |          | Receiver's bank CBN code.                                                                                                                    |
| `receiverCountry`        | `string` |          | ISO 3166-1 alpha-2 code (e.g. `"NG"`, `"US"`). Triggers FTR if ≠ `NG`.                                                                       |
| `narration`              | `string` |          | Transaction description. Scanned for high-risk keywords.                                                                                     |
| `deviceId`               | `string` |          | Device fingerprint. Enables behavioral anomaly detection.                                                                                    |
| `ipAddress`              | `string` |          | Client IP address. Enables geo-anomaly detection.                                                                                            |
| `latitude`               | `number` |          | GPS latitude. Used for geo-anomaly detection.                                                                                                |
| `longitude`              | `number` |          | GPS longitude. Used for geo-anomaly detection.                                                                                               |
| `timestamp`              | `string` | ✅        | ISO 8601 (e.g. `"2026-05-08T12:00:00Z"`).                                                                                                    |
| `metadata`               | `object` |          | Optional metadata for business transactions (see [KYB Screening](#kyb-screening)).                                                           |

**Example Request — Individual Transaction**

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST /api/v1/transactions/screen \
    -H "Authorization: Bearer <access_token>" \
    -H "Content-Type: application/json" \
    -d '{
      "externalId": "TXN-2026-001",
      "type": "TRANSFER",
      "channel": "MOBILE_APP",
      "amount": 5000000,
      "currency": "NGN",
      "senderAccountNumber": "0123456789",
      "senderBvn": "22012345678",
      "senderKycStatus": "PENDING",
      "senderKycTier": "TIER_1",
      "senderKycVerifiedAt": "2026-05-01T10:00:00Z",
      "senderKycExternalRef": "CORE-BANK-KYC-78432",
      "senderName": "John Doe",
      "senderBankCode": "058",
      "receiverAccountNumber": "9876543210",
      "receiverBvn": "22087654321",
      "receiverName": "Jane Smith",
      "receiverBankCode": "033",
      "receiverKycStatus": "VERIFIED",
      "receiverKycVerifiedAt": "2026-04-15T10:00:00Z",
      "receiverKycExternalRef": "CORE-BANK-KYC-99123",
      "receiverCountry": "NG",
      "narration": "Payment for goods",
      "deviceId": "device_abc123",
      "ipAddress": "102.89.47.12",
      "latitude": 6.5244,
      "longitude": 3.3792,
      "timestamp": "2026-05-08T12:00:00Z",
      "metadata": {
        "entityType": "INDIVIDUAL"
      }
    }'
  ```

  ```javascript Node.js theme={null}
  const response = await fetch("/api/v1/transactions/screen", {
    method: "POST",
    headers: {
      Authorization: "Bearer <access_token>",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      externalId: "TXN-2026-001",
      type: "TRANSFER",
      channel: "MOBILE_APP",
      amount: 5000000,
      currency: "NGN",
      senderAccountNumber: "0123456789",
      senderBvn: "22012345678",
      senderKycStatus: "PENDING",
      senderKycTier: "TIER_1",
      senderKycVerifiedAt: "2026-05-01T10:00:00Z",
      senderKycExternalRef: "CORE-BANK-KYC-78432",
      senderName: "John Doe",
      senderBankCode: "058",
      receiverAccountNumber: "9876543210",
      receiverBvn: "22087654321",
      receiverName: "Jane Smith",
      receiverBankCode: "033",
      receiverKycStatus: "VERIFIED",
      receiverKycVerifiedAt: "2026-04-15T10:00:00Z",
      receiverKycExternalRef: "CORE-BANK-KYC-99123",
      receiverCountry: "NG",
      narration: "Payment for goods",
      deviceId: "device_abc123",
      ipAddress: "102.89.47.12",
      latitude: 6.5244,
      longitude: 3.3792,
      timestamp: "2026-05-08T12:00:00Z",
      metadata: {
        entityType: "INDIVIDUAL",
      },
    }),
  });

  const result = await response.json();
  console.log(result.data.outcome); // "APPROVE" | "REVIEW" | "ESCALATE" | "BLOCK"
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      "/api/v1/transactions/screen",
      headers={
          "Authorization": "Bearer <access_token>",
          "Content-Type": "application/json"
      },
      json={
          "externalId": "TXN-2026-001",
          "type": "TRANSFER",
          "channel": "MOBILE_APP",
          "amount": 5000000,
          "currency": "NGN",
          "senderAccountNumber": "0123456789",
          "senderBvn": "22012345678",
          "senderKycStatus": "PENDING",
          "senderKycTier": "TIER_1",
          "senderKycVerifiedAt": "2026-05-01T10:00:00Z",
          "senderKycExternalRef": "CORE-BANK-KYC-78432",
          "senderName": "John Doe",
          "senderBankCode": "058",
          "receiverAccountNumber": "9876543210",
          "receiverBvn": "22087654321",
          "receiverName": "Jane Smith",
          "receiverBankCode": "033",
          "receiverKycStatus": "VERIFIED",
          "receiverKycVerifiedAt": "2026-04-15T10:00:00Z",
          "receiverKycExternalRef": "CORE-BANK-KYC-99123",
          "receiverCountry": "NG",
          "narration": "Payment for goods",
          "deviceId": "device_abc123",
          "ipAddress": "102.89.47.12",
          "latitude": 6.5244,
          "longitude": 3.3792,
          "timestamp": "2026-05-08T12:00:00Z",
          "metadata": {
              "entityType": "INDIVIDUAL"
          }
      }
  )

  result = response.json()
  print(result["data"]["outcome"])  # "APPROVE" | "REVIEW" | "ESCALATE" | "BLOCK"
  ```
</CodeGroup>

### Business Transactions (KYB)

For corporate senders, set `metadata.entityType` to `"BUSINESS"` and provide the company's CAC registration number in `metadata.senderRcNumber`. The platform runs the transaction through the KYB identity-risk engine in addition to KYC and sanctions screening.

If your bank has already verified the business outside of Verifow, send `senderKybStatus: "VERIFIED"` to bypass the Verifow KYB lookup. You should still send the sender's KYC fields (`senderKycStatus`, `senderKycTier`, `senderKycVerifiedAt`, `senderKycExternalRef`) when available, because authorized signatories and directors may still be evaluated under individual KYC rules.

**Example Request — Business Transaction**

```json theme={null}
{
  "externalId": "TXN-2026-BIZ-001",
  "type": "TRANSFER",
  "channel": "INTERNET_BANKING",
  "amount": 25000000,
  "currency": "NGN",
  "senderAccountNumber": "0123456789",
  "senderBvn": "22012345678",
  "senderName": "Acme Nigeria Ltd",
  "senderBankCode": "058",
  "senderKycStatus": "VERIFIED",
  "senderKycTier": "TIER_3",
  "senderKycVerifiedAt": "2026-05-01T10:00:00Z",
  "senderKycExternalRef": "CORE-BANK-KYC-78432",
  "senderKybStatus": "VERIFIED",
  "receiverAccountNumber": "9876543210",
  "receiverName": "Jane Smith",
  "receiverBankCode": "033",
  "receiverCountry": "NG",
  "narration": "Invoice payment",
  "timestamp": "2026-05-08T12:00:00Z",
  "metadata": {
    "entityType": "BUSINESS",
    "senderRcNumber": "RC123456"
  }
}
```

**Business Screening Payload Fields**

| Field                     | Type     | Required | Description                                                                                                                                 |
| ------------------------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `senderKycStatus`         | `string` | ✅        | `VERIFIED`, `PENDING`, `IN_PROGRESS`, `REJECTED`, `EXPIRED`, `NONE`. Required in EXTERNAL mode; asserts the sender's individual KYC status. |
| `senderKycTier`           | `string` | ✅        | `TIER_1`, `TIER_2`, `TIER_3`. Required in EXTERNAL mode; enables CBN tier-based limits.                                                     |
| `senderKycVerifiedAt`     | `string` |          | ISO 8601 timestamp of last KYC verification.                                                                                                |
| `senderKycExternalRef`    | `string` |          | Internal KYC reference for audit trail.                                                                                                     |
| `senderKybStatus`         | `string` | ✅        | `VERIFIED`, `PENDING`, `IN_PROGRESS`, `REJECTED`, `EXPIRED`, `NONE`. For business transactions; asserts the corporate KYB status.           |
| `metadata.entityType`     | `string` | ✅        | Must be `"BUSINESS"` to trigger KYB screening.                                                                                              |
| `metadata.senderRcNumber` | `string` | ✅        | CAC registration number (e.g. `"RC123456"`).                                                                                                |

<Note>
  **Regulatory transparency:** When you assert `senderKycStatus` or `senderKybStatus` in the payload, you are attesting that your bank has independently verified the customer or business. Verifow will rely on these assertions instead of performing its own lookup when the tenant is configured for HYBRID or EXTERNAL mode.
</Note>

<Note>
  If `senderKybStatus` is omitted for a business transaction, Verifow will attempt a local KYB lookup using `metadata.senderRcNumber`. If no KYB record exists, the transaction will be scored as high risk.
</Note>

***

**Example Response — 200 OK (REVIEW)**

```json theme={null}
{
  "success": true,
  "data": {
    "transactionId": "fae50ecb-d997-4700-bae7-49650678bb06",
    "externalId": "TXN-2026-001",
    "outcome": "BLOCK",
    "riskLevel": "HIGH",
    "aggregateScore": 100,
    "actions": ["NOTIFY_OFFICER", "CREATE_CASE"],
    "riskBreakdown": [
      {
        "category": "Regulatory Compliance",
        "score": 0,
        "outcome": "APPROVE",
        "rulesTriggered": 0,
        "latencyMs": 3
      },
      {
        "category": "Global Sanctions Screening",
        "score": 0,
        "outcome": "APPROVE",
        "rulesTriggered": 0,
        "latencyMs": 97
      },
      {
        "category": "KYC Verification",
        "score": 100,
        "outcome": "BLOCK",
        "rulesTriggered": 1,
        "latencyMs": 0
      },
      {
        "category": "Custom Rules",
        "score": 0,
        "outcome": "APPROVE",
        "rulesTriggered": 0,
        "latencyMs": 178
      },
      {
        "category": "Decision Engine",
        "score": 0,
        "outcome": "APPROVE",
        "rulesTriggered": 0,
        "latencyMs": 9
      },
      {
        "category": "Behavioral Analysis",
        "score": 0,
        "outcome": "APPROVE",
        "rulesTriggered": 1,
        "latencyMs": 92
      }
    ],
    "triggeredRules": [
      {
        "name": "KYC Status Non-Verified",
        "category": "KYC Verification",
        "riskScore": 100,
        "details": "No KYC record found for sender (BVN: 22012345678)",
        "createdBy": null,
        "kycSource": "DATABASE",
        "kycExternalRef": null
      }
    ],
    "totalLatencyMs": 398
  }
}
```

**Example Response — 200 OK (BLOCK with Sanctions Hit)**

````json theme={null}
{
  "success": true,
  "data": {
    "transactionId": "20442e74-c039-4640-aadd-5ee5d9024b33",
    "externalId": "TXN-2026-002",
    "outcome": "BLOCK",
    "riskLevel": "CRITICAL",
    "aggregateScore": 95,
    "actions": [
      "CREATE_CASE",
      "NOTIFY_COMPLIANCE",
      "GENERATE_SAR",
      "ENHANCED_DUE_DILIGENCE"
    ],
    "riskBreakdown": [
      {
        "category": "Regulatory Compliance",
        "score": 100,
        "outcome": "BLOCK",
        "rulesTriggered": 2,
        "latencyMs": 1
      },
      {
        "category": "Global Sanctions Screening",
        "score": 95,
        "outcome": "BLOCK",
        "rulesTriggered": 2,
        "latencyMs": 97
      }
    ],
    "triggeredRules": [
      {
        "name": "Structuring Detection",
        "category": "Regulatory Compliance",
        "riskScore": 80,
        "details": "Potential structuring: 4 transactions totaling ₦11,000,000.00 in 24h",
        "createdBy": null
      },
      {
        "name": "Sender \"John Doe\" matches global sanctions list",
        "category": "Global Sanctions Screening",
        "riskScore": 95,
        "details": "Matched: \"LAM, John Top\" (95.0% confidence) | Source: US OFAC Sanctions | Programs: GLOMAG",
        "createdBy": null
      }
    ],
    "totalLatencyMs": 398
  }
}
```-

**Example Response — 400 Bad Request**

```json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed",
    "details": [
      { "field": "amount", "message": "amount must be a positive number" }
    ]
  }
}
````

**Example Response — 409 Conflict (Idempotent)**

```json theme={null}
{
  "success": false,
  "error": {
    "code": "DUPLICATE_EXTERNAL_ID",
    "message": "This externalId has already been processed",
    "data": {
      "transactionId": "fae50ecb-d997-4700-bae7-49650678bb06",
      "outcome": "BLOCK",
      "riskLevel": "HIGH",
      "aggregateScore": 100
    }
  }
}
```

<Note>
  **Idempotency:** Sending the same `externalId` twice returns the cached
  verdict without re-screening. This makes retries safe.
</Note>

***

### List Transactions

`GET /api/v1/transactions`

Retrieve a paginated list of all screened transactions in your tenant.

**Query Parameters**

| Parameter | Type      | Default | Description                                        |
| --------- | --------- | ------- | -------------------------------------------------- |
| `page`    | `integer` | `1`     | Page number                                        |
| `limit`   | `integer` | `20`    | Items per page (max 100)                           |
| `outcome` | `string`  | —       | Filter by `APPROVE`, `REVIEW`, `ESCALATE`, `BLOCK` |
| `type`    | `string`  | —       | Filter by transaction type                         |
| `from`    | `string`  | —       | ISO 8601 start date                                |
| `to`      | `string`  | —       | ISO 8601 end date                                  |

**Example Response — 200 OK**

```json theme={null}
{
  "success": true,
  "data": {
    "items": [
      {
        "id": "fae50ecb-d997-4700-bae7-49650678bb06",
        "externalId": "TXN-0791-389803",
        "type": "TRANSFER",
        "channel": "MOBILE_APP",
        "amount": "5000000",
        "currency": "NGN",
        "senderAccountNumber": "0123456789",
        "senderName": "John Doe",
        "receiverName": "Jane Smith",
        "receiverCountry": "NG",
        "transactionTimestamp": "2026-05-16T12:00:00Z",
        "verdict": {
          "outcome": "BLOCK",
          "riskLevel": "CRITICAL",
          "aggregateScore": 95,
          "totalLatencyMs": 145
        },
        "createdAt": "2026-05-16T12:00:00Z"
      }
    ],
    "total": 3,
    "page": 1,
    "limit": 10,
    "totalPages": 1
  }
}
```

***

### Get Transaction Detail

`GET /api/v1/transactions/:id`

Retrieve the full screening result for a single transaction, including the complete verdict with engine breakdowns.

**Path Parameters**

| Parameter | Type     | Description      |
| --------- | -------- | ---------------- |
| `id`      | `string` | Transaction UUID |

**Example Response — 200 OK**

```json theme={null}
{
  "success": true,
  "data": {
    "id": "fae50ecb-d997-4700-bae7-49650678bb06",
    "tenantId": "660e8400-e29b-41d4-a716-446655440001",
    "externalId": "TXN-0791-389803",
    "type": "TRANSFER",
    "channel": "MOBILE_APP",
    "amount": "5000000",
    "currency": "NGN",
    "senderAccountNumber": "0123456789",
    "senderBvn": "22012345678",
    "senderName": "John Doe",
    "senderBankCode": null,
    "receiverAccountNumber": "9876543210",
    "receiverBvn": null,
    "receiverName": "Jane Smith",
    "receiverBankCode": null,
    "receiverCountry": "NG",
    "narration": "Payment for goods",
    "deviceId": null,
    "ipAddress": null,
    "latitude": null,
    "longitude": null,
    "transactionTimestamp": "2026-05-16T12:00:00Z",
    "metadata": null,
    "createdAt": "2026-05-16T12:00:00Z",
    "verdict": {
      "id": "vtx_001",
      "transactionId": "fae50ecb-d997-4700-bae7-49650678bb06",
      "outcome": "REVIEW",
      "riskLevel": "MEDIUM",
      "aggregateScore": 32,
      "engineVerdicts": [
        {
          "engineName": "KYC Verification",
          "score": 40,
          "outcome": "REVIEW",
          "rulesTriggered": 1,
          "latencyMs": 0
        }
      ],
      "actions": ["NOTIFY_OFFICER"],
      "totalLatencyMs": 13447,
      "processedAt": "2026-05-16T12:00:01Z"
    }
  }
}
```

<Warning>
  Transaction data is immutable once screened. If you need to re-evaluate,
  submit a new transaction with a different `externalId`.
</Warning>

***

## KYC Verification

KYC (Know Your Customer) verification validates individual customer identities through BVN, NIN, and biometric liveness checks against national identity providers.

### KYC Application Statuses

| Status              | Meaning                          | Risk Impact        |
| ------------------- | -------------------------------- | ------------------ |
| `PENDING`           | Submitted, awaiting verification | Score 100, `BLOCK` |
| `DOCUMENT_UPLOADED` | ID documents uploaded            | Score 100, `BLOCK` |
| `NIN_VERIFIED`      | NIN check passed                 | Score 100, `BLOCK` |
| `BVN_VERIFIED`      | BVN check passed                 | Score 100, `BLOCK` |
| `LIVENESS_PASSED`   | Biometric liveness check passed  | Score 0, `APPROVE` |
| `APPROVED`          | Fully verified by officer        | Score 0, `APPROVE` |
| `REJECTED`          | Failed verification              | Score 100, `BLOCK` |
| `EXPIRED`           | Verification expired             | Score 100, `BLOCK` |

***

### Submit KYC Application

`POST /api/v1/kyc/applications`

Create a new KYC application for an individual customer.

**Request Body**

| Field         | Type     | Required | Description                                           |
| ------------- | -------- | -------- | ----------------------------------------------------- |
| `entityType`  | `string` | ✅        | `INDIVIDUAL` or `BUSINESS`                            |
| `firstName`   | `string` | ✅        | Applicant first name                                  |
| `lastName`    | `string` | ✅        | Applicant last name                                   |
| `bvn`         | `string` |          | Bank Verification Number (11 digits)                  |
| `nin`         | `string` |          | National Identity Number (11 digits)                  |
| `phone`       | `string` |          | Mobile number with country code                       |
| `email`       | `string` |          | Email address                                         |
| `dateOfBirth` | `string` |          | ISO 8601 date (e.g. `"1990-03-15"`)                   |
| `tier`        | `string` |          | `TIER_1`, `TIER_2`, or `TIER_3`. Defaults to `TIER_1` |

**Example Request**

```bash- theme={null}
curl -X POST /api/v1/kyc/applications \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "entityType": "INDIVIDUAL",
    "firstName": "Chinedu",
    "lastName": "Obi",
    "bvn": "22012345678",
    "nin": "12345678901",
    "phone": "+2348012345678",
    "email": "chinedu.obi@email.com",
    "dateOfBirth": "1990-03-15",
    "tier": "TIER_2"
  }'
```

**Example Response — 201 Created**

```json theme={null}
{
  "success": true,
  "data": {
    "id": "kyc_abc123",
    "tenantId": "660e8400-e29b-41d4-a716-446655440001",
    "entityType": "INDIVIDUAL",
    "firstName": "Chinedu",
    "lastName": "Obi",
    "bvn": "22012345678",
    "nin": "12345678901",
    "phone": "+2348012345678",
    "email": "chinedu.obi@email.com",
    "dateOfBirth": "1990-03-15T00:00:00Z",
    "status": "PENDING",
    "riskLevel": null,
    "assignedTo": null,
    "notes": null,
    "createdAt": "2026-05-16T08:00:00Z",
    "updatedAt": "2026-05-16T08:00:00Z"
  }
}
```

***

### List KYC Applications

## `GET /api/v1/kyc/applications`

**Query Parameters**

| Parameter | Type      | Default | Description      |
| --------- | --------- | ------- | ---------------- |
| `status`  | `string`  | —       | Filter by status |
| `page`    | `integer` | `1`     | Page number      |
| `limit`   | `integer` | `20`    | Items per page   |

***

### Get KYC Application

`GET /api/v1/kyc/applications/:id`

Retrieve a KYC application with full verification results and uploaded documents.

**Example Response — 200 OK**

```json theme={null}
{
  "success": true,
  "data": {
    "id": "kyc_abc123",
    "tenantId": "660e8400-e29b-41d4-a716-446655440001",
    "entityType": "INDIVIDUAL",
    "status": "BVN_VERIFIED",
    "firstName": "Chinedu",
    "lastName": "Obi",
    "bvn": "22012345678",
    "nin": "12345678901",
    "phone": "+2348012345678",
    "email": "chinedu.obi@email.com",
    "address": "12 Lagos Street, Lagos",
    "dateOfBirth": "1990-03-15T00:00:00Z",
    "riskLevel": null,
    "assignedTo": null,
    "notes": null,
    "createdAt": "2026-05-16T08:00:00Z",
    "updatedAt": "2026-05-16T09:00:00Z",
    "verificationResults": [
      {
        "id": "vr_001",
        "applicationId": "kyc_abc123",
        "identityType": "BVN",
        "provider": "PREMBLY",
        "isMatch": true,
        "confidence": 0.95,
        "rawResponse": null,
        "errorMessage": null,
        "verifiedAt": "2026-05-16T09:00:00Z"
      }
    ],
    "documents": [
      {
        "id": "doc_001",
        "applicationId": "kyc_abc123",
        "documentType": "PASSPORT",
        "fileName": "passport.pdf",
        "fileUrl": "kyc-documents/passport-123.pdf",
        "fileSizeBytes": 204800,
        "mimeType": "application/pdf",
        "uploadedAt": "2026-05-16T08:30:00Z"
      }
    ]
  }
}
```

***

### Verify BVN

`POST /api/v1/kyc/applications/:id/verify-bvn`

Verify the applicant's BVN against the configured identity provider (embedded or BYOL).

**Path Parameters**

| Parameter | Type     | Description          |
| --------- | -------- | -------------------- |
| `id`      | `string` | KYC Application UUID |

**Request Body**

| Field         | Type     | Required | Description                   |
| ------------- | -------- | -------- | ----------------------------- |
| `bvn`         | `string` | ✅        | BVN to verify (11 digits)     |
| `firstName`   | `string` | ✅        | First name for cross-match    |
| `lastName`    | `string` | ✅        | Last name for cross-match     |
| `dateOfBirth` | `string` | ✅        | Date of birth for cross-match |

**Example Request**

```bash theme={null}
curl -X POST /api/v1/kyc/applications/kyc_abc123/verify-bvn \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "bvn": "22012345678",
    "firstName": "Chinedu",
    "lastName": "Obi",
    "dateOfBirth": "1990-03-15"
  }'
```

**Example Response — 200 OK**

```json theme={null}
{
  "success": true,
  "data": {
    "isMatch": true,
    "confidence": 0.95,
    "provider": "PREMBLY",
    "providerSource": "EMBEDDED",
    "newStatus": "BVN_VERIFIED"
  }
}
```

- ***

### Verify NIN

`POST /api/v1/kyc/applications/:id/verify-nin`

Verify the applicant's NIN against the configured identity provider.

**Request Body**

| Field         | Type     | Required | Description                   |
| ------------- | -------- | -------- | ----------------------------- |
| `nin`         | `string` | ✅        | NIN to verify (11 digits)     |
| `firstName`   | `string` | ✅        | First name for cross-match    |
| `lastName`    | `string` | ✅        | Last name for cross-match     |
| `dateOfBirth` | `string` | ✅        | Date of birth for cross-match |

**Example Response — 200 OK**

```json theme={null}
{
  "success": true,
  "data": {
    "isMatch": true,
    "confidence": 0.94,
    "provider": "PREMBLY",
    "providerSource": "EMBEDDED",
    "newStatus": "NIN_VERIFIED"
  }
}
```

***

## Liveness Check

`POST /api/v1/kyc/applications/:id/liveness-check`

Run a biometric liveness detection and face-match check. This verifies that the applicant is a real person (not a photo or video spoof) and optionally matches their selfie against an uploaded ID document.

### How It Works

1. **Liveness Detection:** The provider analyzes a selfie image to detect presentation attacks (printed photos, screens, masks, deepfakes).
2. **Face Match (optional):** If a document image is provided, the system compares the selfie face against the document photo.
3. **Status Update:** On success, the application status advances to `LIVENESS_PASSED`.

### When to Use

| Workflow             | When to Call                                         |
| -------------------- | ---------------------------------------------------- |
| Standard KYC         | After BVN/NIN verification, before officer approval  |
| High-risk onboarding | As a mandatory step for politically exposed persons  |
| Re-verification      | When KYC expires and the customer needs to re-verify |

**Path Parameters**

| Parameter | Type     | Description          |
| --------- | -------- | -------------------- |
| `id`      | `string` | KYC Application UUID |

**Request Body**-

| Field                 | Type     | Required | Description                                                                                                 |
| --------------------- | -------- | -------- | ----------------------------------------------------------------------------------------------------------- |
| `selfieImageBase64`   | `string` | ✅        | Base64-encoded selfie image (JPEG/PNG). Must show the applicant's full face with good lighting.             |
| `documentImageBase64` | `string` |          | Base64-encoded ID document image (passport, driver's license, national ID). Used for face-match comparison. |

**Example Request**

```bash theme={null}
curl -X POST /api/v1/kyc/applications/kyc_abc123/liveness-check \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "selfieImageBase64": "/9j/4AAQSkZJRgABAQEASABIAAD...",
    "documentImageBase64": "/9j/4AAQSkZJRgABAQEASABIAAD..."
  }'
```

**Example Response — 200 OK (Pass)**

```json theme={null}
{
  "success": true,
  "data": {
    "isLive": true,
    "confidence": 0.97,
    "faceMatch": true,
    "faceMatchConfidence": 0.92,
    "provider": "PREMBLY",
    "providerSource": "EMBEDDED",
    "newStatus": "LIVENESS_PASSED"
  }
}
```

**Example Response — 200 OK (Fail — Spoof Detected)**

```json theme={null}
{
  "success": true,
  "data": {
    "isLive": false,
    "confidence": 0.12,
    "faceMatch": false,
    "faceMatchConfidence": 0.0,
    "provider": "PREMBLY",
    "providerSource": "EMBEDDED",
    "newStatus": "PENDING"
  }
}
```

* <Note>
    A failed liveness check does **not** automatically reject the application.
    The status remains `PENDING` (or its previous state) so a compliance officer
    can review and decide whether to request a retry or reject.
  </Note>

  ***

### Approve KYC Application

`PATCH /api/v1/kyc/applications/:id/approve`

Final approval of a KYC application. Requires `BANK_ADMIN` or `COMPLIANCE_OFFICER` role.

**Example Response — 200 OK**

```json theme={null}
{
  "success": true,
  "data": {
    "id": "kyc_abc123",
    "status": "APPROVED",
    "riskLevel": "LOW",
    "notes": "All documents verified. BVN and NIN match.",
    "updatedAt": "2026-05-16T10:00:00Z"
  }
}
```

* <Note>
    Approving a KYC application updates the customer's risk profile. Future
    transactions from this customer will reflect the `APPROVED` status (score 0,
    no KYC-related risk).
  </Note>

  ***

### Reject KYC Application

`PATCH /api/v1/kyc/applications/:id/reject`

Reject a KYC application that fails verification. Requires `BANK_ADMIN` or `COMPLIANCE_OFFICER` role.

**Request Body**

| Field    | Type     | Required | Description          |
| -------- | -------- | -------- | -------------------- |
| `reason` | `string` | ✅        | Reason for rejection |

**Example Response — 200 OK**

```json theme={null}
{
  "success": true,
  "data": {
    "id": "kyc_abc123",
    "status": "REJECTED",
    "riskLevel": "HIGH",
    "notes": "BVN name mismatch. Uploaded document appears fraudulent.",
    "updatedAt": "2026-05-16T10:00:00Z"
  }
}
```

***

## KYB Verification

KYB (Know Your Business) verifies corporate entities before they transact. It integrates with the Corporate Affairs Commission (CAC) to validate company registration, verify directors through KYC identity checks, and screen beneficial owners against global sanctions lists.

### KYB Application Statuses

| Status                       | Meaning                                    | Risk Impact        |
| ---------------------------- | ------------------------------------------ | ------------------ |
| `PENDING`                    | Application submitted, awaiting CAC lookup | Score 40, `REVIEW` |
| `CAC_VERIFIED`               | CAC lookup successful                      | Score 40, `REVIEW` |
| `DIRECTORS_VERIFIED`         | Directors identity verified                | Score 40, `REVIEW` |
| `BENEFICIAL_OWNERS_VERIFIED` | Beneficial owners screened                 | Score 100, `BLOCK` |
| `APPROVED`                   | Fully verified                             | Score 0, `APPROVE` |
| `REJECTED`                   | Failed verification or sanctions hit       | Score 85, `BLOCK`  |

### KYB Screening Trigger

KYB checks run automatically during transaction screening when the transaction metadata indicates a business entity:

```json theme={null}
{
  "metadata": {
    "entityType": "BUSINESS",
    "senderRcNumber": "RC123456"
  }
}
```

***

### Submit KYB Application

`POST /api/v1/kyc/kyb/applications`

Create a new KYB application for a corporate entity.

**Request Body**

| Field                           | Type     | Required | Description                       |
| ------------------------------- | -------- | -------- | --------------------------------- |
| `companyName`                   | `string` | ✅        | Registered company name           |
| `rcNumber`                      | `string` |          | CAC registration number           |
| `tin`                           | `string` |          | Tax Identification Number         |
| `address`                       | `string` |          | Registered business address       |
| `incorporationDate`             | `string` |          | ISO 8601 date                     |
| `directors`                     | `array`  |          | Array of director objects         |
| `directors[].name`              | `string` | ✅        | Director full name                |
| `directors[].bvn`               | `string` |          | Director BVN                      |
| `directors[].nin`               | `string` |          | Director NIN                      |
| `directors[].email`             | `string` |          | Director email                    |
| `directors[].phone`             | `string` |          | Director phone                    |
| `directors[].nationality`       | `string` |          | Director nationality              |
| `beneficialOwners`              | `array`  |          | Array of beneficial owner objects |
| `beneficialOwners[].name`       | `string` | ✅        | Owner full name                   |
| `beneficialOwners[].percentage` | `number` | ✅        | Ownership percentage              |
| `beneficialOwners[].bvn`        | `string` |          | Owner BVN                         |
| `beneficialOwners[].nin`        | `string` |          | Owner NIN                         |

**Example Request**-

```bash theme={null}
curl -X POST /api/v1/kyc/kyb/applications \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "companyName": "Acme Nigeria Ltd",
    "rcNumber": "RC123456",
    "tin": "12345678-0001",
    "address": "15 Broad Street, Lagos",
    "incorporationDate": "2015-06-12",
    "directors": [
      { "name": "John Doe", "bvn": "22012345678", "email": "john@acme.ng" }
    ],
    "beneficialOwners": [
      { "name": "Jane Smith", "percentage": 75, "bvn": "22087654321" }
    ]
  }'
```

**Example Response — 201 Created**

```json theme={null}
{
  "success": true,
  "data": {
    "id": "kyb_abc123",
    "tenantId": "660e8400-e29b-41d4-a716-446655440001",
    "companyName": "Acme Nigeria Ltd",
    "rcNumber": "RC123456",
    "tin": "12345678-0001",
    "address": "15 Broad Street, Lagos",
    "incorporationDate": "2015-06-12T00:00:00Z",
    "status": "PENDING",
    "riskLevel": null,
    "directors": [
      { "name": "John Doe", "bvn": "22012345678", "email": "john@acme.ng" }
    ],
    "beneficialOwners": [
      { "name": "Jane Smith", "percentage": 75, "bvn": "22087654321" }
    ],
    "assignedTo": null,
    "notes": null,
    "createdAt": "2026-05-16T08:00:00Z",
    "updatedAt": "2026-05-16T08:00:00Z"
  }
}
```

***

### Verify CAC

`POST /api/v1/kyc/kyb/applications/:id/verify-cac`

Run a CAC lookup by RC number to fetch company details, directors, and beneficial owners.

**Request Body**

| Field      | Type     | Required | Description             |
| ---------- | -------- | -------- | ----------------------- |
| `rcNumber` | `string` | ✅        | CAC registration number |

**Example Response — 200 OK**

```json theme={null}
{
  "success": true,
  "data": {
    "found": true,
    "confidence": 0.95,
    "company": {
      "rcNumber": "RC123456",
      "companyName": "Acme Nigeria Ltd",
      "address": "15 Broad Street, Lagos",
      "registrationDate": "2015-06-12",
      "directors": [
        {
          "name": "John Doe",
          "nationality": "Nigerian",
          "appointmentDate": "2015-06-12"
        }
      ],
      "beneficialOwners": [{ "name": "Jane Smith", "percentage": 75 }]
    },
    "newStatus": "CAC_VERIFIED"
  }
}
```

***

### Verify Directors

`POST /api/v1/kyc/kyb/applications/:id/verify-directors`

Verify each director's identity via BVN/NIN against identity providers.

**Request Body**

| Field                  | Type      | Required | Description                            |
| ---------------------- | --------- | -------- | -------------------------------------- |
| `directors`            | `array`   | ✅        | Array of director verification objects |
| `directors[].name`     | `string`  | ✅        | Director name                          |
| `directors[].nin`      | `string`  |          | Director NIN                           |
| `directors[].bvn`      | `string`  |          | Director BVN                           |
| `directors[].verified` | `boolean` |          | Pre-verified flag                      |

**Example Response — 200 OK**

```json theme={null}
{
  "success": true,
  "data": {
    "allVerified": true,
    "directors": [
      {
        "name": "John Doe",
        "nin": "12345678901",
        "bvn": "22012345678",
        "verified": true
      }
    ],
    "newStatus": "DIRECTORS_VERIFIED"
  }
}
```

***

### Screen Beneficial Owners

`POST /api/v1/kyc/kyb/applications/:id/screen-beneficial-owners`

Screen beneficial owners against global sanctions and PEP watchlists.

**Request Body**

| Field                      | Type      | Required | Description                         |
| -------------------------- | --------- | -------- | ----------------------------------- |
| `beneficialOwners`         | `array`   | ✅        | Array of BO screening objects       |
| `beneficialOwners[].name`  | `string`  | ✅        | Owner name                          |
| `beneficialOwners[].clean` | `boolean` | ✅        | Pre-screened clean flag             |
| `beneficialOwners[].hits`  | `array`   |          | List of watchlist hits if not clean |

**Example Response — 200 OK**

```json theme={null}
{
  "success": true,
  "data": {
    "allClean": true,
    "beneficialOwners": [{ "name": "Jane Smith", "clean": true, "hits": [] }],
    "newStatus": "BENEFICIAL_OWNERS_VERIFIED"
  }
}
```

***

### Approve KYB Application

`PATCH /api/v1/kyc/kyb/applications/:id/approve`

Requires `BANK_ADMIN` or `COMPLIANCE_OFFICER` role.

**Example Response — 200 OK**

```json theme={null}
{
  "success": true,
  "data": {
    "id": "kyb_abc123",
    "status": "APPROVED",
    "riskLevel": "LOW",
    "updatedAt": "2026-05-16T10:00:00Z"
  }
}
```

***

### Reject KYB Application

`PATCH /api/v1/kyc/kyb/applications/:id/reject`

Requires `BANK_ADMIN` or `COMPLIANCE_OFFICER` role.

**Request Body**

| Field    | Type     | Required | Description          |
| -------- | -------- | -------- | -------------------- |
| `reason` | `string` | ✅        | Reason for rejection |

**Example Response — 200 OK**

```json theme={null}
{
  "success": true,
  "data": {
    "id": "kyb_abc123",
    "status": "REJECTED",
    "riskLevel": "HIGH",
    "notes": "Sanctions hit on beneficial owner Jane Smith.",
    "updatedAt": "2026-05-16T10:00:00Z"
  }
}
```

***

## How Screening Works

Every transaction passes through multiple screening layers in sequence. The entire pipeline completes in **80–200ms** (p95).

### The Pipeline

| Stage | Engine                         | Latency | What It Does                                                    |
| ----- | ------------------------------ | ------- | --------------------------------------------------------------- |
| 1     | **Validation**                 | \~5ms   | Schema validation, idempotency check, persist transaction       |
| 2     | **Context Enrichment**         | \~15ms  | Fetch sender's recent history, account dormancy, KYC/KYB status |
| 3     | **Global Sanctions Screening** | \~25ms  | Screen names against OFAC, UN, EU, UK, Interpol, PEP lists      |
| 4     | **Regulatory Compliance**      | \~2ms   | Execute 9 mandatory AML rules                                   |
| 5     | **Identity Risk (KYC)**        | \~1ms   | Evaluate sender's KYC verification status                       |
| 6     | **Identity Risk (KYB)**        | \~1ms   | For business transactions — evaluate KYB verification status    |
| 7     | **Decision Engine**            | \~10ms  | Local rule evaluation using stored AST formulas                 |
| 8     | **Behavioral Analysis**        | \~50ms  | ML scoring for device, velocity, and geographic anomalies       |

### Mandatory Regulatory Rules

These 9 rules are **always active** and **cannot be disabled**:

| Rule                           | Trigger                                                              | Auto-Actions                 |
| ------------------------------ | -------------------------------------------------------------------- | ---------------------------- |
| **AML-001** Cash Threshold     | Cash ≥ ₦5,000,000                                                    | Generate CTR, Notify Officer |
| **AML-002** Transfer Threshold | Transfer ≥ ₦10,000,000                                               | Generate CTR, Notify Officer |
| **AML-003** Structuring        | Single txn ≥ 80% of threshold, OR 3+ txns in 24h exceeding threshold | Generate SAR, Create Case    |
| **AML-004** PEP Screening      | Sender is a Politically Exposed Person                               | Enhanced Due Diligence       |
| **AML-005** Foreign Transfer   | International transfer or receiver country ≠ NG                      | Generate FTR                 |
| **AML-006** Dormant Account    | No activity for ≥ 180 days                                           | Create Case                  |
| **AML-007** Rapid Succession   | ≥ 5 txns within 60 minutes from same account                         | Create Case                  |
| **AML-008** KYC Tier Limit     | Transaction exceeds sender's KYC tier limit                          | Block, Prompt Tier Upgrade   |
| **AML-009** Near Tier Limit    | Transaction at ≥80% of sender's KYC tier limit                       | Review, Notify Officer       |

### Identity Risk Scoring

#### Individual (KYC)

| Sender KYC Status                                      | Risk Score | Outcome |
| ------------------------------------------------------ | ---------- | ------- |
| **APPROVED**                                           | 0          | APPROVE |
| **LIVENESS\_PASSED**                                   | 0          | APPROVE |
| **NIN\_VERIFIED / BVN\_VERIFIED / DOCUMENT\_UPLOADED** | 100        | BLOCK   |
| **PENDING**                                            | 100        | BLOCK   |
| **DOCUMENT\_UPLOADED**                                 | 100        | BLOCK   |
| **NONE** (no application)                              | 100        | BLOCK   |
| **EXPIRED**                                            | 100        | BLOCK   |
| **REJECTED**                                           | 100        | BLOCK   |

#### Business (KYB)

| Sender KYB Status                                                                                                         | Risk Score | Outcome |
| ------------------------------------------------------------------------------------------------------------------------- | ---------- | ------- |
| **APPROVED**                                                                                                              | 0          | APPROVE |
| **PENDING / CAC\_VERIFIED / DIRECTORS\_VERIFIED / BENEFICIAL\_OWNERS\_VERIFIED / DOCUMENT\_UPLOADED / SCREENING\_PASSED** | 100        | BLOCK   |
| **NONE** (no application)                                                                                                 | 100        | BLOCK   |
| **REJECTED**                                                                                                              | 100        | BLOCK   |

### KYC Trust Modes

Tenants can configure how KYC status is resolved during transaction screening:

| Mode       | Behavior                                                               |
| ---------- | ---------------------------------------------------------------------- |
| `STRICT`   | **Default.** KYC records must exist in the DB. Payload KYC is ignored. |
| `HYBRID`   | Uses payload KYC if provided, falls back to DB lookup.                 |
| `EXTERNAL` | Requires KYC status in every transaction payload. No local DB lookup.  |

Set the mode via `PATCH /api/v1/tenants/me` with `kycTrustMode`.

### Score Aggregation

| Category                       | Weight   | Rationale                                                 |
| ------------------------------ | -------- | --------------------------------------------------------- |
| **Regulatory Compliance**      | **1.5×** | Core compliance — highest authority                       |
| **Identity Verification**      | **1.3×** | KYC/KYB integrity is compliance-critical                  |
| **Custom Rules**               | **1.2×** | Institution-specific logic elevated above generic engines |
| **Global Sanctions Screening** | **1.0×** | Standard weight — severity via outcome escalation         |
| **Decision Engine**            | **1.0×** | Rule-mirroring redundancy                                 |
| **Behavioral Analysis**        | **0.8×** | Supplementary evidence                                    |

**Score Floor Guarantee:** `Final Score = MAX(weighted_average, max_engine_score × 0.8)`

The **highest-severity outcome from any engine becomes the final decision.**

***

## Webhooks

Verifow sends real-time notifications to your backend when a compliance officer resolves a case linked to one of your transactions.

<Note>
  **Setup:** Configure your `Webhook URL` and `Webhook Secret` in the
  **Developer Webhooks** section of the API Keys page in your Dashboard.
</Note>

### Events

| Event           | Trigger                                                                                               |
| --------------- | ----------------------------------------------------------------------------------------------------- |
| `case.resolved` | A compliance officer marks a case as `RESOLVED_TRUE_POSITIVE`, `RESOLVED_FALSE_POSITIVE`, or `CLOSED` |

### Payload

```json theme={null}
{
  "event": "case.resolved",
  "timestamp": "2026-05-08T15:00:00Z",
  "data": {
    "caseId": "case_12345abcde",
    "status": "RESOLVED_FALSE_POSITIVE",
    "resolutionNote": "Customer provided valid source of funds documentation.",
    "relatedTransactionId": "TXN-2026-001",
    "updatedAt": "2026-05-08T15:00:00Z"
  }
}
```

### Delivery Guarantees

| Property                    | Value                                    |
| --------------------------- | ---------------------------------------- |
| **Max Attempts**            | 5                                        |
| **Backoff Strategy**        | Exponential (5s → 10s → 20s → 40s → 80s) |
| **Request Timeout**         | 10 seconds per attempt                   |
| **Completed Job Retention** | 24 hours                                 |
| **Failed Job Retention**    | 7 days                                   |

<Warning>
  **Your endpoint must return a `2xx` status code.** Any non-2xx response will
  trigger automatic retries. If all 5 attempts fail, the event is retained in
  the dead-letter queue for 7 days.
</Warning>

### Signature Verification

Every webhook includes an `X-Signature` header — an HMAC-SHA256 hash of the raw JSON body, signed with your Webhook Secret.

<CodeGroup>
  ```javascript Node.js (Express) theme={null}
  const crypto = require('crypto');

  app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-signature'];
  const hash = crypto
  .createHmac('sha256', WEBHOOK_SECRET)
  .update(req.body)
  .digest('hex');

  if (`sha256=${hash}` !== signature) {
  return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(req.body);

  if (event.event === 'case.resolved') {
  if (event.data.status === 'RESOLVED_FALSE_POSITIVE') {
  // Unfreeze the transaction in your system
  } else if (event.data.status === 'RESOLVED_TRUE_POSITIVE') {
  // Permanently block and escalate
  }
  }

  res.status(200).send('OK');
  });

  ```

  ```python Python (Flask) theme={null}
  import hmac
  import hashlib
  import json
  from flask import Flask, request, abort

  app = Flask(__name__)

  @app.route('/webhooks', methods=['POST'])
  def handle_webhook():
      signature = request.headers.get('X-Signature', '')
      expected = 'sha256=' + hmac.new(
          WEBHOOK_SECRET.encode(),
          request.data,
          hashlib.sha256
      ).hexdigest()-
  -
      if not hmac.compare_digest(signature, expected):
          abort(401)

      event = json.loads(request.data)

      if event['event'] == 'case.resolved':
          if event['data']['status'] == 'RESOLVED_FALSE_POSITIVE':
              # Unfreeze the transaction
              pass
          elif event['data']['status'] == 'RESOLVED_TRUE_POSITIVE':
              # Permanently block
              pass

      return 'OK', 200
  ```
</CodeGroup>

***

## Error Handling

Standard HTTP status codes indicate success or failure:

| Code  | Meaning           | Solution                                                                              |
| ----- | ----------------- | ------------------------------------------------------------------------------------- |
| `200` | Success           | Request processed successfully.                                                       |
| `201` | Created           | Resource created successfully.                                                        |
| `400` | Bad Request       | Check validation errors in the response body. Ensure all required fields are present. |
| `401` | Unauthorized      | Your token or API key is missing, invalid, or revoked.                                |
| `403` | Forbidden         | Your role does not have permission for this action.                                   |
| `409` | Conflict          | Duplicate `externalId` or conflicting state.                                          |
| `429` | Too Many Requests | Rate limit exceeded — implement exponential backoff.                                  |
| `500` | Server Error      | Unexpected error — retry with backoff.                                                |

<Note>
  **Fail-Open Policy:** If the Sanctions or Behavioral engines experience
  upstream downtime, Verifow implements a fail-open policy — your legitimate
  transactions continue processing without interruption, while partial
  screenings are logged for retroactive review.
</Note>

***

> **Verifow** — Enterprise-grade AML/CFT compliance for African financial institutions. For support, contact [adams@bevars.com](mailto:adams@bevars.com).
