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

# KYC/KYB Frontend Integration Guide

> Step-by-step guide for integrating Yativo identity verification into web and mobile applications

This guide walks you through integrating Yativo KYC (individual) and KYB (business) verification into your application. You can choose a hosted approach for quick integration or a fully custom API integration for maximum control.

## Integration approaches

| Approach               | Best For                   | Effort | Customization |
| ---------------------- | -------------------------- | ------ | ------------- |
| Hosted KYC/KYB Link    | Quick launch, iframe embed | Low    | Limited       |
| Custom API Integration | Full control, native UI    | High   | Full          |

***

## Option A: Hosted links (recommended)

The simplest integration. You create a customer, then redirect them to Yativo's hosted verification flow at `https://kyc.yativo.com`.

### Step 1: Get an access token

```javascript theme={null}
async function getAuthToken() {
  const response = await fetch('https://api.yativo.com/api/v1/auth/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      account_id: process.env.YATIVO_ACCOUNT_ID,
      app_secret: process.env.YATIVO_APP_SECRET,
    }),
  });

  const data = await response.json();
  // Token expires in 600 seconds — cache and refresh as needed
  return data.data.access_token;
}
```

### Step 2: Create a customer

```javascript theme={null}
async function createCustomer(token, customerData) {
  const response = await fetch('https://api.yativo.com/api/v1/customer', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': `create-customer-${customerData.email}-${Date.now()}`,
    },
    body: JSON.stringify({
      customer_name: customerData.name,
      customer_email: customerData.email,
      customer_phone: customerData.phone,
      customer_country: customerData.country, // ISO 3166-1 alpha-2, e.g. "US"
      customer_type: 'individual', // or 'business'
    }),
  });

  const data = await response.json();
  return data.data.customer_id;
}
```

### Step 3: Redirect to hosted KYC

```javascript theme={null}
function redirectToKYC(customerId) {
  // Redirect individual customers
  window.location.href = `https://kyc.yativo.com/individual/${customerId}`;
}

function redirectToKYB(customerId) {
  // Redirect business customers
  window.location.href = `https://kyc.yativo.com/business/${customerId}`;
}
```

### Embedding in an iframe

If you prefer to embed verification within your application instead of a full redirect:

```html theme={null}
<iframe
  src="https://kyc.yativo.com/individual/CUSTOMER_ID"
  allow="camera; microphone"
  width="100%"
  height="700px"
  style="border: none; border-radius: 8px;"
  title="Identity Verification"
></iframe>
```

<Warning>
  The `allow="camera; microphone"` attribute is required for document capture and selfie steps to work correctly in the iframe.
</Warning>

### Step 4: Poll for verification status

After the customer completes the hosted flow, poll the status endpoint to detect when verification is complete.

```javascript theme={null}
function pollKycStatus(token, customerId, onComplete) {
  const intervalId = setInterval(async () => {
    try {
      const response = await fetch(
        `https://api.yativo.com/api/v1/customer/kyc/${customerId}`,
        { headers: { 'Authorization': `Bearer ${token}` } }
      );

      const data = await response.json();
      const status = data.data?.status;

      if (status === 'approved') {
        clearInterval(intervalId);
        onComplete({ success: true, status, isVaApproved: data.data.is_va_approved });
      } else if (status === 'rejected') {
        clearInterval(intervalId);
        onComplete({
          success: false,
          status,
          reasons: data.data.kyc_rejection_reasons,
        });
      }
    } catch (err) {
      console.error('Error polling KYC status:', err);
    }
  }, 5000); // Poll every 5 seconds

  // Return cleanup function
  return () => clearInterval(intervalId);
}

// Usage
const stopPolling = pollKycStatus(token, customerId, (result) => {
  if (result.success) {
    console.log('Customer verified! VA approved:', result.isVaApproved);
  } else {
    console.log('Verification rejected. Reasons:', result.reasons);
  }
});
```

<Note>
  Instead of polling, consider using [webhooks](/yativo-fiat/webhooks) to receive real-time notifications when KYC status changes.
</Note>

***

## Option B: Custom API integration

Build your own multi-step form that collects customer data and submits it directly to the KYC API.

### Step 1: Collect personal information

```jsx theme={null}
import { useState } from 'react';

function PersonalInfoStep({ onNext }) {
  const [form, setForm] = useState({
    first_name: '',
    last_name: '',
    email: '',
    phone: '',
    calling_code: '+1',
    gender: '',
    birth_date: '',
    nationality: '',
    taxId: '',
    // Nigerian nationals only
    bvn: '',
    nin: '',
  });

  const handleChange = (field) => (e) =>
    setForm((prev) => ({ ...prev, [field]: e.target.value }));

  const isNigerian = form.nationality === 'NG';

  return (
    <form onSubmit={(e) => { e.preventDefault(); onNext(form); }}>
      <input
        placeholder="First Name"
        value={form.first_name}
        onChange={handleChange('first_name')}
        required
      />
      <input
        placeholder="Last Name"
        value={form.last_name}
        onChange={handleChange('last_name')}
        required
      />
      <input
        type="email"
        placeholder="Email"
        value={form.email}
        onChange={handleChange('email')}
        required
      />
      {/* Gender accepts only "male" or "female" */}
      <select value={form.gender} onChange={handleChange('gender')} required>
        <option value="">Select gender</option>
        <option value="male">Male</option>
        <option value="female">Female</option>
      </select>
      <input
        type="date"
        value={form.birth_date}
        onChange={handleChange('birth_date')}
        required
      />
      <input
        placeholder="Nationality (ISO alpha-2, e.g. US)"
        value={form.nationality}
        onChange={handleChange('nationality')}
        required
      />
      {isNigerian && (
        <>
          <input
            placeholder="BVN (11 digits)"
            value={form.bvn}
            onChange={handleChange('bvn')}
            maxLength={11}
            required
          />
          <input
            placeholder="NIN (11 digits)"
            value={form.nin}
            onChange={handleChange('nin')}
            maxLength={11}
            required
          />
        </>
      )}
      <button type="submit">Next</button>
    </form>
  );
}
```

<Note>
  When `nationality` is `"NG"`, both `bvn` and `nin` (each exactly 11 digits) are required by the API. Show these fields conditionally based on the selected nationality.
</Note>

### Step 2: Upload files via Yativo storage

Before submitting the KYC form, upload document images using Yativo's storage endpoint. This keeps payloads small and avoids base64 encoding large files.

```javascript theme={null}
/**
 * Upload a file to Yativo storage and return a hosted URL
 * to pass into the KYC payload.
 */
async function uploadDocument(token, file) {
  const formData = new FormData();
  formData.append('document', file); // field name must be "document"

  const response = await fetch('https://api.yativo.com/api/v1/storage/upload', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`,
      // Do NOT set Content-Type — let the browser set it with the boundary
    },
    body: formData,
  });

  if (!response.ok) {
    const err = await response.json();
    throw new Error(err.message || 'File upload failed');
  }

  const data = await response.json();
  return data.data.url; // Pass this URL in the KYC payload
}
```

You can also pass files as base64-encoded strings inline, but the hosted URL approach is recommended for files over \~500 KB.

```javascript theme={null}
function fileToBase64(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result); // Returns data URL (base64)
    reader.onerror = reject;
    reader.readAsDataURL(file);
  });
}
```

### Step 3: Collect residential address

```jsx theme={null}
function AddressStep({ token, onNext }) {
  const [address, setAddress] = useState({
    street_line_1: '',
    city: '',
    state: '',
    postal_code: '',
    country: '',
  });
  const [proofFile, setProofFile] = useState(null);
  const [uploading, setUploading] = useState(false);

  const handleFileChange = async (e) => {
    const file = e.target.files[0];
    if (!file) return;
    setUploading(true);
    try {
      const url = await uploadDocument(token, file);
      setProofFile(url);
    } finally {
      setUploading(false);
    }
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    onNext({ ...address, proof_of_address_file: proofFile });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        placeholder="Street Address"
        value={address.street_line_1}
        onChange={(e) => setAddress((a) => ({ ...a, street_line_1: e.target.value }))}
        required
      />
      <input
        placeholder="City"
        value={address.city}
        onChange={(e) => setAddress((a) => ({ ...a, city: e.target.value }))}
        required
      />
      <input
        placeholder="State / Province"
        value={address.state}
        onChange={(e) => setAddress((a) => ({ ...a, state: e.target.value }))}
        required
      />
      <input
        placeholder="Postal Code"
        value={address.postal_code}
        onChange={(e) => setAddress((a) => ({ ...a, postal_code: e.target.value }))}
        required
      />
      <label>
        Proof of Address (utility bill, bank statement, etc.)
        <input type="file" accept="image/*,.pdf" onChange={handleFileChange} required />
        {uploading && <span>Uploading…</span>}
        {proofFile && <p>✓ Uploaded</p>}
      </label>
      <button type="submit" disabled={uploading || !proofFile}>
        Next
      </button>
    </form>
  );
}
```

### Step 4: Collect ID document

```jsx theme={null}
function IdDocumentStep({ token, onNext }) {
  const [doc, setDoc] = useState({
    type: 'passport',
    issuing_country: '',
    number: '',
    date_issued: '',
    expiration_date: '',
    image_front_file: null,
    image_back_file: null,
  });

  const handleFileUpload = (field) => async (e) => {
    const file = e.target.files[0];
    if (!file) return;
    const url = await uploadDocument(token, file);
    setDoc((d) => ({ ...d, [field]: url }));
  };

  const needsBack = doc.type !== 'passport';

  return (
    <form onSubmit={(e) => { e.preventDefault(); onNext([doc]); }}>
      <select
        value={doc.type}
        onChange={(e) => setDoc((d) => ({ ...d, type: e.target.value }))}
      >
        <option value="passport">Passport</option>
        <option value="national_id">National ID</option>
        <option value="other">Other</option>
      </select>
      <input
        placeholder="Document Number"
        value={doc.number}
        onChange={(e) => setDoc((d) => ({ ...d, number: e.target.value }))}
        required
      />
      <label>
        Issue Date
        <input
          type="date"
          value={doc.date_issued}
          onChange={(e) => setDoc((d) => ({ ...d, date_issued: e.target.value }))}
          required
        />
      </label>
      <label>
        Expiration Date
        <input
          type="date"
          value={doc.expiration_date}
          onChange={(e) => setDoc((d) => ({ ...d, expiration_date: e.target.value }))}
          required
        />
      </label>
      <label>
        Front of Document
        <input type="file" accept="image/*,.pdf" onChange={handleFileUpload('image_front_file')} required />
        {doc.image_front_file && <p>✓ Front uploaded</p>}
      </label>
      {needsBack && (
        <label>
          Back of Document
          <input type="file" accept="image/*,.pdf" onChange={handleFileUpload('image_back_file')} required />
          {doc.image_back_file && <p>✓ Back uploaded</p>}
        </label>
      )}
      <button type="submit">Next</button>
    </form>
  );
}
```

### Step 5: Collect financial information

<Warning>
  The values sent to the API are snake\_case identifiers, not display labels. Use the exact values below — the API will reject any other values.
</Warning>

```jsx theme={null}
function FinancialInfoStep({ onNext }) {
  const [financial, setFinancial] = useState({
    employment_status: '',
    most_recent_occupation_code: '',
    source_of_funds: '',
    account_purpose: '',
    account_purpose_other: '',
    expected_monthly_payments_usd: '',
    acting_as_intermediary: false,
  });

  const showOtherPurpose = financial.account_purpose === 'other';

  return (
    <form onSubmit={(e) => { e.preventDefault(); onNext(financial); }}>
      <select
        value={financial.employment_status}
        onChange={(e) => setFinancial((f) => ({ ...f, employment_status: e.target.value }))}
        required
      >
        <option value="">Employment Status</option>
        <option value="employed">Employed</option>
        <option value="self_employed">Self-Employed</option>
        <option value="unemployed">Unemployed</option>
        <option value="retired">Retired</option>
        <option value="student">Student</option>
        <option value="homemaker">Homemaker</option>
        <option value="exempt">Exempt</option>
      </select>

      <select
        value={financial.source_of_funds}
        onChange={(e) => setFinancial((f) => ({ ...f, source_of_funds: e.target.value }))}
        required
      >
        <option value="">Source of Funds</option>
        <option value="salary">Salary</option>
        <option value="business_income">Business Income</option>
        <option value="company_funds">Company Funds</option>
        <option value="savings">Savings</option>
        <option value="investments_loans">Investments / Loans</option>
        <option value="inheritance">Inheritance</option>
        <option value="gifts">Gifts</option>
        <option value="government_benefits">Government Benefits</option>
        <option value="pension_retirement">Pension / Retirement</option>
        <option value="sale_of_assets_real_estate">Sale of Assets / Real Estate</option>
        <option value="ecommerce_reseller">E-commerce / Reseller</option>
        <option value="gambling_proceeds">Gambling Proceeds</option>
        <option value="someone_elses_funds">Someone Else's Funds</option>
      </select>

      <select
        value={financial.account_purpose}
        onChange={(e) => setFinancial((f) => ({ ...f, account_purpose: e.target.value }))}
        required
      >
        <option value="">Account Purpose</option>
        <option value="receive_salary">Receive Salary</option>
        <option value="business_transactions">Business Transactions</option>
        <option value="purchase_goods_and_services">Purchase Goods & Services</option>
        <option value="personal_or_living_expenses">Personal / Living Expenses</option>
        <option value="payments_to_friends_or_family_abroad">Payments to Friends/Family Abroad</option>
        <option value="receive_payment_for_freelancing">Receive Payment for Freelancing</option>
        <option value="investment_purposes">Investment Purposes</option>
        <option value="protect_wealth">Protect Wealth</option>
        <option value="ecommerce_retail_payments">E-commerce / Retail Payments</option>
        <option value="operating_a_company">Operating a Company</option>
        <option value="charitable_donations">Charitable Donations</option>
        <option value="other">Other</option>
      </select>

      {showOtherPurpose && (
        <input
          placeholder="Describe your account purpose"
          value={financial.account_purpose_other}
          onChange={(e) => setFinancial((f) => ({ ...f, account_purpose_other: e.target.value }))}
          required
        />
      )}

      <select
        value={financial.expected_monthly_payments_usd}
        onChange={(e) => setFinancial((f) => ({ ...f, expected_monthly_payments_usd: e.target.value }))}
        required
      >
        <option value="">Expected Monthly Volume (USD)</option>
        <option value="0_4999">Less than $5,000</option>
        <option value="5000_9999">$5,000 – $9,999</option>
        <option value="10000_49999">$10,000 – $49,999</option>
        <option value="50000_plus">$50,000 or more</option>
      </select>

      <label>
        <input
          type="checkbox"
          checked={financial.acting_as_intermediary}
          onChange={(e) => setFinancial((f) => ({ ...f, acting_as_intermediary: e.target.checked }))}
        />
        Acting on behalf of a third party
      </label>

      <button type="submit">Next</button>
    </form>
  );
}
```

### Step 6: Submit to the KYC API

```javascript theme={null}
async function submitKYC(token, customerId, formData) {
  const payload = {
    customer_id: customerId,
    // Personal info
    ...formData.personal,
    // Residential address (including proof_of_address_file URL)
    residential_address: formData.address,
    // ID documents (array of document objects with uploaded URLs)
    identifying_information: formData.documents,
    // Financial & purpose
    ...formData.financial,
    // Selfie image URL from storage upload
    selfie_image: formData.selfieUrl,
    // Supporting documents
    uploaded_documents: formData.supportingDocs,
  };

  // Remove account_purpose_other unless account_purpose is "other"
  if (payload.account_purpose !== 'other') {
    delete payload.account_purpose_other;
  }

  const response = await fetch('https://kyc.yativo.com/api/individual-kyc/submit', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': `kyc-${customerId}-${Date.now()}`,
    },
    body: JSON.stringify(payload),
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message || 'KYC submission failed');
  }

  return response.json();
}
```

<Note>
  The KYC submission endpoint is at `https://kyc.yativo.com`, not the main API. The `Authorization` bearer token is the same one issued by `POST /auth/login`.
</Note>

***

## Status polling (TypeScript)

```typescript theme={null}
type KycStatus = 'not_started' | 'submitted' | 'manual_review' | 'approved' | 'rejected' | 'under_review';

interface KycStatusResult {
  status: KycStatus;
  isVaApproved: boolean;
  rejectionReasons: string[];
  kycLink: string;
}

function pollKycStatus(
  token: string,
  customerId: string,
  onStatusChange: (result: KycStatusResult) => void,
  intervalMs = 5000
): () => void {
  const intervalId = setInterval(async () => {
    const response = await fetch(
      `https://api.yativo.com/api/v1/customer/kyc/${customerId}`,
      { headers: { Authorization: `Bearer ${token}` } }
    );

    const { data } = await response.json();
    const result: KycStatusResult = {
      status: data.status,
      isVaApproved: data.is_va_approved,
      rejectionReasons: data.kyc_rejection_reasons ?? [],
      kycLink: data.kyc_link,
    };

    onStatusChange(result);

    if (data.status === 'approved' || data.status === 'rejected') {
      clearInterval(intervalId);
    }
  }, intervalMs);

  return () => clearInterval(intervalId);
}
```

***

## Webhooks (recommended over polling)

Instead of polling, register a webhook to receive real-time notifications:

```javascript theme={null}
// Register webhook
await fetch('https://api.yativo.com/api/v1/webhook', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': 'webhook-kyc-events',
  },
  body: JSON.stringify({
    url: 'https://your-app.com/webhooks/yativo',
    events: [
      'customer.kyc.approved',
      'customer.kyc.rejected',
    ],
  }),
});
```

Then in your webhook handler:

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

  if (event === 'customer.kyc.approved') {
    const { customer_id } = data;
    // Activate customer account, send welcome email, etc.
    activateCustomer(customer_id);
  }

  if (event === 'customer.kyc.rejected') {
    const { customer_id, rejection_reasons } = data;
    // Notify customer, share kyc_link for re-submission
    notifyCustomerRejection(customer_id, rejection_reasons);
  }

  res.sendStatus(200);
});
```

***

## UI/UX best practices

**Progress indicators**

Show a clear multi-step progress bar so customers know where they are in the verification process. For individual KYC: Personal Info → Address & Documents → ID Document → Financial Info → Review & Submit.

**Clear error messages**

Display field-level validation errors immediately, not just on submit. Translate API error codes into user-friendly language. The API returns a `data` object with field-specific error arrays on `422` responses.

**Document upload previews**

Always show a preview of uploaded documents before submission. This helps customers catch blurry or incorrectly oriented images before they fail review.

```jsx theme={null}
function DocumentPreview({ url, label }) {
  if (!url) return null;
  const isImage = /\.(jpg|jpeg|png|heic)$/i.test(url) || url.startsWith('data:image');
  return (
    <div style={{ marginTop: 8 }}>
      <p style={{ fontSize: 12, color: '#666' }}>{label} preview:</p>
      {isImage ? (
        <img
          src={url}
          alt={label}
          style={{ maxWidth: 300, maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
        />
      ) : (
        <p style={{ color: '#888' }}>📄 PDF uploaded</p>
      )}
    </div>
  );
}
```

**Mobile camera support**

Use `capture="environment"` on file inputs to open the rear camera directly on mobile:

```html theme={null}
<!-- Opens rear camera for document capture -->
<input type="file" accept="image/*" capture="environment" />

<!-- Opens front camera for selfie -->
<input type="file" accept="image/*" capture="user" />
```

**Selfie guidance**

For the selfie step, display on-screen guidelines (oval face outline, lighting tips) before the customer takes their photo to reduce re-submission rates.

**Retry on rejection**

When a customer's KYC is rejected, use the `kyc_link` from the status response to send them back to the hosted flow for corrections.

```javascript theme={null}
const statusData = await getKycStatus(token, customerId);
if (statusData.status === 'rejected') {
  const kycLink = statusData.kyc_link;
  // Show retry button that redirects to kycLink
  console.log('Rejection reasons:', statusData.kyc_rejection_reasons);
}
```
