Integrations
Persona KYC & Biometric Liveness
Identity and selfie verification with Persona — embedded in-domain during signup and high-limit upgrades. In Furlpay this is wired through components/PersonaKycButton.tsx, lib/kyc/persona.ts and the webhook route.
1. Frontend embed flow
Launch a Persona Inquiry using a configured Template ID (Selfie + Government ID). The embedded flow keeps the user within the Furlpay domain.
// components/PersonaKycButton.tsx
const client = new window.Persona.Client({
templateId: process.env.NEXT_PUBLIC_PERSONA_TEMPLATE_ID!,
referenceId: userId, // maps the Furlpay user id to the inquiry
environment: 'sandbox', // 'production' for live verification
onComplete: ({ inquiryId }) => onSuccess(inquiryId),
onCancel: () => onCancel(),
onError: (error) => console.error('Persona error:', error),
});
client.open();Load the SDK once
persona-v4.js once and caches it across mounts.2. Backend webhooks
Persona processes liveness asynchronously. Verify the persona-signature header before updating the user’s account. Persona sends a hex HMAC-SHA256 over the raw body; during secret rotation the header carries multiple space-separated signature sets — so use a constant-time, rotation-aware comparison (never a plain !==).
// lib/kyc/persona.ts
import crypto from 'crypto';
export function verifyPersonaSignature(rawBody: string, header: string | null, secret: string) {
if (!header || !secret) return false;
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
for (const segment of header.trim().split(/\s+/)) {
const v1 = Object.fromEntries(segment.split(',').map(kv => kv.split('=')))['v1'] ?? segment;
if (v1.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(v1, 'hex'), Buffer.from(expected, 'hex'))) {
return true;
}
}
return false;
}// app/api/webhooks/persona/route.ts
export async function POST(request: Request) {
const payload = await request.text();
const sig = request.headers.get('persona-signature');
if (!verifyPersonaSignature(payload, sig, process.env.PERSONA_WEBHOOK_SECRET!)) {
return Response.json({ error: 'Signature verification failed' }, { status: 401 });
}
const event = JSON.parse(payload);
if (event.eventType === 'inquiry.completed') {
// UPDATE furlpay_database SET kyc_status = 'verified' WHERE user_id = referenceId
}
return Response.json({ received: true });
}3. Advanced 2026 API features
Inquiry Search
Instead of paginating the list endpoint, filter registrations dynamically with the June 2026 Inquiry Search API (requires Persona-Version: 2026-06-09).
await fetch('https://withpersona.com/api/v1/inquiries/search', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PERSONA_API_KEY}`,
'Content-Type': 'application/json',
'Persona-Version': '2026-06-09',
},
body: JSON.stringify({
query: 'reference_id:"USER_ID" AND status:"completed" AND tags:"high_velocity"',
}),
});KYB — Business Associated Persons
For corporate merchants, the Business Associated Persons report returns anownership_information object listing ultimate beneficial owners (UBOs). Furlpay screens every owner against TRM Labs / sanctions databases automatically.
4. Biometric security best practices
- Presentation Attack Detection (PAD): run active & passive liveness side-by-side to block deepfakes, screen replays and printed-mask attacks.
- Strict device binding: match device telemetry against historical signatures via the Sardine SDK to defeat virtual-camera injection.
- Data segregation (GDPR): Furlpay never stores biometric data, selfie streams or raw IDs — they stay encrypted in Persona’s SOC2/ISO vaults. We retain only the
inquiryIdand a booleanverifiedfor audit trails.