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

# KYC Integration

> Complete guide to identity verification with Zentra

Learn how to verify customer identities using Zentra's KYC (Know Your Customer) APIs. Support BVN, NIN, and document verification.

<Warning>
  Identity is a reviewed but tenant-gated namespace in the current route manifest. Use these flows only when your environment explicitly enables the identity routes and verify the exact shape against your approved tenant contract.
</Warning>

## What You'll Learn

* Verify BVN (Bank Verification Number)
* Verify NIN (National Identification Number)
* Perform liveness checks
* Handle verification results
* Build a complete onboarding flow

## Prerequisites

* Zentra API account with KYC enabled
* Customer information to verify
* Webhook endpoint for async results

## KYC Flow Overview

```
┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│   Customer   │────▶│  Your Server │────▶│  Zentra API  │
│   (Submit)   │     │   (Verify)   │     │   (Check)    │
└──────────────┘     └──────────────┘     └──────────────┘
       │                    │                    │
       │   1. Enter         │   2. Submit        │
       │      Details       │      for KYC       │
       │                    │   ──────────────▶  │
       │                    │                    │   3. Verify with
       │                    │                    │      NIBSS/NIMC
       │                    │   ◀──────────────  │
       │   4. Return        │   5. Result        │
       │      Result        │      (sync/async)  │
       │   ◀────────────────│                    │
```

## Step 1: Verify BVN

BVN verification is the primary KYC method in Nigeria:

<CodeGroup>
  ```javascript lib/kyc.js theme={null}
  const Zentra = require('@zentra/sdk');

  const client = new Zentra.Client({
    apiKey: process.env.ZENTRA_SECRET_KEY
  });

  async function verifyBVN(customerData) {
    const result = await client.identity.verifyBVN({
      bvn: customerData.bvn,
      first_name: customerData.firstName,
      last_name: customerData.lastName,
      date_of_birth: customerData.dateOfBirth, // YYYY-MM-DD
      phone: customerData.phone // Optional, for additional matching
    });

    return {
      verified: result.verified,
      match_score: result.match_score,
      details: {
        first_name_match: result.first_name_match,
        last_name_match: result.last_name_match,
        dob_match: result.dob_match,
        phone_match: result.phone_match
      }
    };
  }
  ```

  ```python kyc.py theme={null}
  from zentra import Client
  import os

  client = Client(api_key=os.getenv("ZENTRA_SECRET_KEY"))

  def verify_bvn(customer_data):
      result = client.identity.verify_bvn({
          "bvn": customer_data["bvn"],
          "first_name": customer_data["first_name"],
          "last_name": customer_data["last_name"],
          "date_of_birth": customer_data["date_of_birth"],
          "phone": customer_data.get("phone")
      })

      return {
          "verified": result["verified"],
          "match_score": result["match_score"],
          "details": {
              "first_name_match": result["first_name_match"],
              "last_name_match": result["last_name_match"],
              "dob_match": result["dob_match"],
              "phone_match": result["phone_match"]
          }
      }
  ```
</CodeGroup>

### BVN Verification Response

```json theme={null}
{
  "verified": true,
  "match_score": 95,
  "first_name_match": true,
  "last_name_match": true,
  "dob_match": true,
  "phone_match": true,
  "bvn_data": {
    "first_name": "JOHN",
    "last_name": "DOE",
    "middle_name": "JAMES",
    "date_of_birth": "1990-05-15",
    "phone": "08012345678",
    "gender": "Male",
    "registration_date": "2015-03-20"
  }
}
```

<Note>
  BVN data is returned in uppercase. Normalize before comparing with your records.
</Note>

## Step 2: Verify NIN

Verify National Identification Number:

```javascript theme={null}
async function verifyNIN(customerData) {
  const result = await client.identity.verifyNIN({
    nin: customerData.nin,
    first_name: customerData.firstName,
    last_name: customerData.lastName,
    date_of_birth: customerData.dateOfBirth
  });

  return {
    verified: result.verified,
    match_score: result.match_score,
    photo: result.photo, // Base64 encoded photo
    details: result.nin_data
  };
}
```

<Warning>
  NIN verification returns the person's photo. Only store and display with proper consent.
</Warning>

## Step 3: Liveness Check

Verify the person is real and matches their documents:

```javascript theme={null}
async function performLivenessCheck(customerId, selfieData) {
  const result = await client.identity.verifySelfie({
    customer_id: customerId,
    selfie: selfieData.imageBase64,
    reference_image: selfieData.referenceImage, // From BVN/NIN
    liveness_check: true
  });

  return {
    is_live: result.liveness_passed,
    face_match: result.face_match_score > 80,
    face_match_score: result.face_match_score,
    fraud_signals: result.fraud_signals
  };
}
```

### Liveness Response

```json theme={null}
{
  "liveness_passed": true,
  "face_match_score": 92,
  "fraud_signals": [],
  "quality_score": 85,
  "details": {
    "eyes_open": true,
    "face_centered": true,
    "good_lighting": true
  }
}
```

## Step 4: Complete Onboarding Flow

Build a full KYC onboarding:

```javascript theme={null}
async function onboardCustomer(userData) {
  // Step 1: Create an app-level customer record in your own backend
  const customer = await db.customers.create({
    email: userData.email,
    first_name: userData.firstName,
    last_name: userData.lastName,
    phone: userData.phone,
    date_of_birth: userData.dateOfBirth,
    kyc_status: 'pending'
  });

  // Step 2: Verify BVN
  const bvnResult = await client.identity.verifyBVN({
    bvn: userData.bvn,
    first_name: userData.firstName,
    last_name: userData.lastName,
    date_of_birth: userData.dateOfBirth
  });

  if (!bvnResult.verified) {
    throw new Error('BVN verification failed');
  }

  // Step 3: Persist verified status in your own system
  await db.customers.update({
    where: { id: customer.id },
    data: {
      kyc_status: 'verified',
      kyc_level: 1,
      bvn_verified: true,
      bvn_verified_at: new Date()
    }
  });

  // Step 4: Optionally provision add-on rails after KYC passes
  const railCustomerId = await ensureRailCustomerForTenant(customer);
  const account = await client.virtualAccounts.create({
    customer_id: railCustomerId,
    account_name: `${userData.firstName} ${userData.lastName}`,
    bvn: userData.bvn
  });

  return {
    customer_id: customer.id,
    rail_customer_id: railCustomerId,
    account,
    kyc_status: 'verified'
  };
}
```

## Step 5: Handle Async Results

Some verifications complete asynchronously:

```javascript theme={null}
app.post('/webhooks/zentra', async (req, res) => {
  const { event, data } = req.body;

  switch (event) {
    case 'identity.verified':
      await handleVerificationSuccess(data);
      break;
    case 'identity.failed':
      await handleVerificationFailed(data);
      break;
    case 'identity.pending':
      await handleVerificationPending(data);
      break;
  }

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

async function handleVerificationSuccess(data) {
  const { customer_id, verification_type, details } = data;

  await db.customers.update({
    where: { zentra_id: customer_id },
    data: {
      kyc_status: 'verified',
      kyc_verified_at: new Date(),
      kyc_details: details
    }
  });

  // Notify customer
  await notificationService.send(customer_id, {
    title: 'Verification Complete',
    body: 'Your identity has been verified. You can now access all features.'
  });
}

async function handleVerificationFailed(data) {
  const { customer_id, reason, retry_allowed } = data;

  await db.customers.update({
    where: { zentra_id: customer_id },
    data: {
      kyc_status: 'failed',
      kyc_failure_reason: reason
    }
  });

  // Notify customer
  await notificationService.send(customer_id, {
    title: 'Verification Failed',
    body: retry_allowed 
      ? 'Please try again with correct details.'
      : 'Contact support for assistance.'
  });
}
```

## KYC Levels

Implement tiered verification for different access levels:

| Level | Requirements        | Limits         |
| ----- | ------------------- | -------------- |
| 0     | Email + Phone       | View only      |
| 1     | BVN verified        | ₦50,000/day    |
| 2     | BVN + NIN           | ₦200,000/day   |
| 3     | Full KYC + Liveness | ₦1,000,000/day |

```javascript theme={null}
function getTransactionLimit(kycLevel) {
  const limits = {
    0: 0,
    1: 5000000,   // ₦50,000
    2: 20000000,  // ₦200,000
    3: 100000000  // ₦1,000,000
  };
  return limits[kycLevel] || 0;
}
```

## Error Handling

Handle common KYC errors:

```javascript theme={null}
try {
  const result = await client.identity.verifyBVN(data);
  return result;
} catch (error) {
  switch (error.code) {
    case 'invalid_bvn':
      throw new Error('Invalid BVN format');
    case 'bvn_not_found':
      throw new Error('BVN not found in database');
    case 'name_mismatch':
      throw new Error('Name does not match BVN records');
    case 'dob_mismatch':
      throw new Error('Date of birth does not match');
    case 'verification_limit':
      throw new Error('Daily verification limit reached');
    case 'service_unavailable':
      throw new Error('Verification service temporarily unavailable');
    default:
      throw new Error('Verification failed. Please try again.');
  }
}
```

## Testing in Sandbox

Use test data in sandbox mode:

| BVN           | Result                  |
| ------------- | ----------------------- |
| `12345678901` | Successful verification |
| `12345678902` | Name mismatch           |
| `12345678903` | DOB mismatch            |
| `12345678904` | BVN not found           |

<Tip>
  Test all failure scenarios to ensure your app handles them gracefully.
</Tip>

## Best Practices

1. **Normalize Data**: Convert names to uppercase before verification
2. **Handle Partial Matches**: Accept 80%+ match scores with manual review
3. **Rate Limit**: Prevent brute-force attempts
4. **Audit Trail**: Log all verification attempts
5. **Data Security**: Encrypt and minimize storage of PII

## Next Steps

<CardGroup cols={2}>
  <Card title="Identity API" icon="user-check" href="/api-reference/identity/overview">
    Full API reference
  </Card>

  <Card title="Virtual Accounts" icon="building-columns" href="/api-reference/virtual-accounts/overview">
    Create accounts after KYC
  </Card>

  <Card title="Webhooks" icon="webhook" href="/guides/handling-webhooks">
    Handle KYC events
  </Card>

  <Card title="Going Live" icon="rocket" href="/guides/going-live">
    Production checklist
  </Card>
</CardGroup>
