Furlpay Docs
Open App

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.

tsx
// 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

The reference snippet removed the Persona script on unmount, which breaks a re-open. Furlpay loadspersona-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 !==).

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

typescript
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 inquiryId and a boolean verified for audit trails.