Skip to main content
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

ApproachBest ForEffortCustomization
Hosted KYC/KYB LinkQuick launch, iframe embedLowLimited
Custom API IntegrationFull control, native UIHighFull

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

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

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

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:
<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>
The allow="camera; microphone" attribute is required for document capture and selfie steps to work correctly in the iframe.

Step 4: Poll for verification status

After the customer completes the hosted flow, poll the status endpoint to detect when verification is complete.
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);
  }
});
Instead of polling, consider using webhooks to receive real-time notifications when KYC status changes.

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

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>
  );
}
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.

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.
/**
 * 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.
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

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

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

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

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();
}
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.

Status polling (TypeScript)

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);
}

Instead of polling, register a webhook to receive real-time notifications:
// 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:
// 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.
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:
<!-- 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.
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);
}