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