Skip to main content

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.

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