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
| 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
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.
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>
);
}
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);
}
Webhooks (recommended over polling)
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);
}