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-3
      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;

      console.log('KYC status:', 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: '',
  });

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

  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
      />
      <select value={form.gender} onChange={handleChange('gender')} required>
        <option value="">Select gender</option>
        <option value="male">Male</option>
        <option value="female">Female</option>
        <option value="other">Other</option>
      </select>
      <input
        type="date"
        value={form.birth_date}
        onChange={handleChange('birth_date')}
        required
      />
      {/* Add remaining fields... */}
      <button type="submit">Next</button>
    </form>
  );
}

Step 2: Collect residential address with proof of address

function AddressStep({ onNext }) {
  const [address, setAddress] = useState({
    street_line_1: '',
    city: '',
    state: '',
    postal_code: '',
    country: '',
  });
  const [proofFile, setProofFile] = useState(null);

  const handleFileChange = async (e) => {
    const file = e.target.files[0];
    if (file) {
      const base64 = await fileToBase64(file);
      setProofFile(base64);
    }
  };

  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"
        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)
        <input type="file" accept="image/*,.pdf" onChange={handleFileChange} required />
        {proofFile && <img src={proofFile} alt="Preview" style={{ maxWidth: '200px' }} />}
      </label>
      <button type="submit">Next</button>
    </form>
  );
}

Step 3: Collect ID document

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

  const handleFileChange = (field) => async (e) => {
    const file = e.target.files[0];
    if (file) {
      const base64 = await fileToBase64(file);
      setDoc((d) => ({ ...d, [field]: base64 }));
    }
  };

  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="drivers_license">Driver's License</option>
      </select>
      <input
        placeholder="Document Number"
        value={doc.number}
        onChange={(e) => setDoc((d) => ({ ...d, number: e.target.value }))}
        required
      />
      <input
        type="date"
        placeholder="Date Issued"
        value={doc.date_issued}
        onChange={(e) => setDoc((d) => ({ ...d, date_issued: e.target.value }))}
        required
      />
      <input
        type="date"
        placeholder="Expiration Date"
        value={doc.expiration_date}
        onChange={(e) => setDoc((d) => ({ ...d, expiration_date: e.target.value }))}
        required
      />
      <label>
        Front of Document
        <input type="file" accept="image/*" onChange={handleFileChange('image_front_file')} required />
      </label>
      {doc.type !== 'passport' && (
        <label>
          Back of Document
          <input type="file" accept="image/*" onChange={handleFileChange('image_back_file')} required />
        </label>
      )}
      <button type="submit">Next</button>
    </form>
  );
}

Step 4: Collect financial information

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

  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>
      </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">Business</option>
        <option value="Investment">Investment</option>
        <option value="Savings">Savings</option>
      </select>
      <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</option>
        <option value="LessThan5K">Less than $5,000</option>
        <option value="5KTo50K">$5,000 – $50,000</option>
        <option value="MoreThan50K">More than $50,000</option>
      </select>
      <button type="submit">Submit KYC</button>
    </form>
  );
}

Step 5: Submit to the KYC API

async function submitKYC(token, customerId, formData) {
  const payload = {
    customer_id: customerId,
    type: 'individual',
    ...formData.personal,
    residential_address: formData.address,
    identifying_information: formData.documents,
    ...formData.financial,
    usd_virtual_account: true,
    eur_virtual_account: false,
    eurde_virtual_account: false,
  };

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

File handling

Converting file uploads to base64

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

Using file URLs instead

If you have your own cloud storage (S3, GCS, Cloudinary, etc.), you can upload the file there first and pass the public URL to the KYC API instead of base64. This reduces payload size significantly.
async function uploadToStorage(file) {
  const formData = new FormData();
  formData.append('file', file);

  const response = await fetch('https://your-storage-api.com/upload', {
    method: 'POST',
    body: formData,
  });

  const { url } = await response.json();
  return url; // Use this URL in the KYC payload
}

Status polling (TypeScript)

type KycStatus = 'not_started' | 'submitted' | 'manual_review' | 'approved' | 'rejected' | 'under_review';

interface KycStatusResult {
  status: KycStatus;
  isVaApproved: boolean;
  rejectionReasons: 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 ?? [],
    };

    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, provide re-submission link
    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 example: Personal Info → Address → Document Upload → Financial Info → Review. Clear error messages Display field-level validation errors immediately, not just on submit. Translate API error codes into user-friendly language. 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({ file, label }) {
  if (!file) return null;
  const isImage = file.startsWith('data:image');
  return (
    <div style={{ marginTop: 8 }}>
      <p style={{ fontSize: 12, color: '#666' }}>{label} preview:</p>
      {isImage ? (
        <img
          src={file}
          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, share the Yativo hosted link from the kyc_link field in the status response so they can re-submit with corrections.
const statusData = await getKycStatus(token, customerId);
if (statusData.status === 'rejected') {
  const kycLink = statusData.kyc_link;
  // Show retry button that redirects to kycLink
}