@flonkid/kyc
v1.9.4
Published
Official Flonk KYC SDK — identity verification for any application
Maintainers
Readme
@flonkid/kyc
Official SDK for Flonk identity verification.
- Dashboard: dashboard.flonk.id
- Docs: docs.flonk.id
Install
npm install @flonkid/kyc| Import | Use |
|--------|-----|
| @flonkid/kyc | Browser — widget + React component |
| @flonkid/kyc/server | Node.js — sessions API + webhook verification |
Keys
You'll find two keys in Dashboard → Project Settings → API Keys:
| Key | Prefix | Where to use | Purpose |
|-----|--------|--------------|---------|
| Publishable key | pk_live_* / pk_sandbox_* | Frontend (browser) | Loads your project branding (logo, colors) instantly |
| Secret key | sk_live_* / sk_sandbox_* | Backend only | Creates sessions, authenticates API calls |
Never expose your secret key in client-side code.
React Component (recommended)
Option A: SDK handles session creation
import { FlonkKYCWidget } from '@flonkid/kyc';
<FlonkKYCWidget
publishableKey="pk_live_..."
serverUrl="/api/kyc/create-session"
clientMetadata={{ email: '[email protected]', userId: 'user_123' }}
lang="de"
onSuccess={(result) => console.log('Verified:', result)}
onError={(error) => console.error(error)}
onCancel={() => console.log('Cancelled')}
/>
// With authenticated backend
<FlonkKYCWidget
publishableKey="pk_live_..."
serverUrl="/api/kyc/create-session"
requestHeaders={{ Authorization: `Bearer ${token}` }}
...
/>Option B: You control session creation
import { FlonkKYCWidget } from '@flonkid/kyc';
// 1. Create session on your backend first (it returns { sessionId, embedToken,
// qrCodeUrl } — map the API's `id` to `sessionId`)
const { sessionId, embedToken, qrCodeUrl } = await fetch('/api/kyc/create-session', {
method: 'POST',
body: JSON.stringify({ email: user.email }),
}).then(r => r.json());
// 2. Pass credentials to widget
<FlonkKYCWidget
publishableKey="pk_live_..." // optional — instant branded loader (see note)
sessionId={sessionId}
embedToken={embedToken}
qrCodeUrl={qrCodeUrl} // needed for the desktop→mobile QR
lang="de"
onSuccess={(result) => console.log('Verified:', result)}
onError={(error) => console.error(error)}
onCancel={() => console.log('Cancelled')}
/>
publishableKeyenables the instant branded loader in both flows. With it, the brand color is resolved straight from the key (in parallel with session setup) and painted on the first frame; without it, branding is resolved from the session and the loader shows the default color until that request returns.
Props
| Prop | Type | Description |
|------|------|-------------|
| publishableKey | string | Your pk_live_* key — enables instant branded loader |
| serverUrl | string | Your backend endpoint — SDK auto-creates session |
| sessionId | string | Pre-created session ID (Option B) |
| embedToken | string | JWT token from your backend (Option B) |
| requestHeaders | Record<string, string> | Extra headers for serverUrl (e.g. JWT auth) |
| clientMetadata | Record<string, unknown> | Custom data passed to session |
| lang | 'en' \| 'de' \| 'uk' | Widget language |
| onSuccess | (result) => void | Verification completed |
| onError | (error) => void | Error occurred |
| onCancel | () => void | User closed widget |
| onReady | () => void | Widget loaded |
| autoOpen | boolean | Open on mount (default: true) |
Imperative API (non-React)
import { FlonkKYC } from '@flonkid/kyc';
const kyc = new FlonkKYC();
const widget = await kyc.init({
publishableKey: 'pk_live_...',
serverUrl: '/api/kyc/create-session',
clientMetadata: { email: '[email protected]' },
lang: 'de',
onSuccess: (result) => console.log('Verified:', result),
onError: (error) => console.error(error),
onCancel: () => console.log('Cancelled'),
});
// Cleanup
widget.destroy();Server — Node.js
import { FlonkKYCServer } from '@flonkid/kyc/server';
const flonk = new FlonkKYCServer({
secretKey: 'sk_live_...',
// Transient failures (429 / 5xx / network) are retried with jittered
// exponential backoff. GETs always retry; writes retry only when idempotent.
maxRetries: 2, // default; set 0 to disable
});
// Create session — idempotent: a retry (same key) returns the original session,
// never a duplicate. A key is auto-generated per call; pass `idempotencyKey` to
// make a specific create idempotent across process restarts.
const session = await flonk.createSession({
clientMetadata: { email: '[email protected]', userId: 'user_123' },
expiryMinutes: 30,
language: 'de',
});
// → { id, embedToken, status, expiresAt, widgetUrl, qrCodeUrl }
// Verify webhook
const event = flonk.webhooks.constructEvent(rawBody, signature, secret);Webhook Events
| Event | Description |
|-------|-------------|
| verification.completed | AI verification finished |
| verification.status_changed | Admin changed status (approved/rejected) |
| verification.updated | Admin edited verification data |
const event = flonk.webhooks.constructEvent(rawBody, signature, secret);
switch (event.type) {
case 'verification.completed':
console.log('Extracted:', event.data.object.extracted_data);
break;
case 'verification.status_changed':
console.log('New status:', event.data.object.status);
break;
case 'verification.updated':
console.log('Updated by:', event.data.object.updated_by);
break;
}Signature formats & replay protection
Flonk sends two signature headers, both HMAC-SHA256 with your webhook
secret; constructEvent verifies whichever you pass. Both prove authenticity +
integrity — the only difference is replay protection:
| Header | Format | Replay-protected? |
|--------|--------|-------------------|
| X-Signature | t=<unix>, v1=<hex> | ✅ Timestamp is signed; requests outside the skew window (default 300s) are rejected. |
| X-Signature-256 | sha256=<hex> | ❌ No timestamp — a captured request stays valid (close the gap with event-id dedup, below). |
Both are fully valid signatures; prefer X-Signature when you want replay
protection at the signature layer. Verification is constant-time.
Since delivery is at-least-once (Flonk retries on non-200), also dedupe by
event.id so a retry is processed once. Do this in your own store — a unique
DB index, or Redis SET id 1 NX EX <ttl> — exactly as you would for Stripe:
app.post('/webhooks/flonk', async (req, res) => {
const event = flonk.webhooks.constructEvent(req.rawBody, req.headers['x-signature'], secret);
// Idempotency: SET NX returns null if the key already existed → it's a retry.
const fresh = await redis.set(`whk:${event.id}`, '1', 'PX', 6 * 60_000, 'NX');
if (fresh === null) return res.status(200).end(); // already handled
await process(event);
res.status(200).end();
});Content Security Policy & CORS
The widget runs in an iframe on widget.flonk.id and loads a small branded
loader script from the API. If your site sends a Content-Security-Policy
header, allow our origins:
Content-Security-Policy:
frame-src https://widget.flonk.id;
script-src https://widget.flonk.id https://api.flonk.id;
connect-src https://api.flonk.id https://widget.flonk.id;
img-src https://widget.flonk.id data:;frame-src— the widget iframe (widget.flonk.id).script-src—widget.js(widget.flonk.id) and the server-hosted loader (api.flonk.id/v1/public/loader.js). If the loader script is blocked, the SDK falls back to its bundled loader — it still works, just without dashboard-driven branding/fixes.connect-src— session/token/design-token fetches.
You do not need any CORS or Cross-Origin-Resource-Policy configuration on
your side. Our public assets (loader.js, loader-config, design-tokens)
already send Cross-Origin-Resource-Policy: cross-origin, and the API handles
CORS for the SDK's fetch/POST calls.
Debugging blocked resources
Every degradation is observable — no silent failures. Pass onDiagnostic, or
flip the global debug flag to mirror events to the console:
const kyc = new FlonkKYC({
onDiagnostic: (e) => console.log(`[flonk:${e.code}] ${e.message}`, e.detail),
});
// Or, without code — anywhere before the widget opens:
window.__FLONK_DEBUG__ = true; // SDK + widget.js both honor this
// <script ... data-debug> also enables it for widget.jsCodes you may see when something is blocked or degraded:
| Code | Meaning |
|------|---------|
| LOADER_SCRIPT_BLOCKED | Server loader script failed to load (CSP script-src, CORP, offline). Bundled loader used. |
| LOADER_FALLBACK_BUNDLED | Server loader wasn't ready at show time; bundled loader used. |
| PREWARM_SKIPPED | Prewarm disabled (prewarm: 'none') or no DOM (SSR). |
| READY_TIMEOUT_REVEAL | No READY from the iframe within 4s; revealed on safety timeout (check frame-src). |
| IFRAME_FRESH | A fresh widget iframe was built. |
Console output is silent unless onDiagnostic is set or __FLONK_DEBUG__ is
truthy — zero noise in production by default.
Versioning
Three versions, deliberately separate:
- Package version (npm) — changes every release.
- SDK↔iframe wire protocol — the widget iframe and SDK deploy independently,
so the wire is additive-only; a
PROTOCOL_VERSION_MISMATCHdiagnostic surfaces a stale-cached peer. - REST API version — date-pinned, sent as the
Flonk-Versionheader on every server request. The API is additive-only within a version; a breaking change would mint a new date and serve the old shape to SDKs pinned to the old one. Override withnew FlonkKYCServer({ apiVersion: '2026-06-01' }); the server echoes the resolved version back in theFlonk-Versionresponse header.
Links
License
Proprietary. Copyright (c) 2026 Flonk. All rights reserved. See Terms of Service for usage terms.
