The browser SDK (@flonkid/kyc) renders the verification widget in an iframe and
streams results back to your app via callbacks. It ships first-class React
bindings and a framework-agnostic class for everything else.
bashnpm install @flonkid/kyc
| Import | Runtime | Use |
|---|---|---|
@flonkid/kyc | Browser | FlonkKYC class + FlonkKYCWidget React component |
@flonkid/kyc/core | Browser (no React) | FlonkKYC class only — React-free entry for Vue / Angular / Svelte / vanilla |
@flonkid/kyc/server | Node.js | Sessions API + webhook signature verification |
@flonkid/kyc/types | — | TypeScript types only |
React is an optional peer dependency. The default
@flonkid/kycentry includes theFlonkKYCWidgetReact component, so it importsreact. If you're not on React, import theFlonkKYCclass from@flonkid/kyc/core— a React-free entry, so your bundler never has to resolve React. (Added in1.9.3.)
Two ways to start verification
The widget needs a verification session. You choose who creates it.
Option A — SDK creates the session (serverUrl)
You point the SDK at a backend endpoint; it creates the session, opens the widget, and cleans up. One component, no state to manage.
tsx1import { FlonkKYCWidget } from '@flonkid/kyc';23export default function KYCVerification() {4 return (5 <FlonkKYCWidget6 publishableKey="pk_live_..."7 serverUrl="/api/kyc/create-session"8 clientMetadata={{9 email: 'user@example.com',10 userId: 'user_123', // optional11 }}12 lang="de"13 onSuccess={(result) => console.log('KYC completed:', result)}14 onError={(error) => console.error('KYC failed:', error)}15 onCancel={() => console.log('KYC cancelled')}16 />17 );18}1920// With authentication (JWT)21function KYCWithAuth({ token }: { token: string }) {22 return (23 <FlonkKYCWidget24 publishableKey="pk_live_..."25 serverUrl="/api/kyc/create-session"26 requestHeaders={{ Authorization: `Bearer ${token}` }}27 lang="de"28 onSuccess={(result) => console.log('KYC completed:', result)}29 onError={(error) => console.error('KYC failed:', error)}30 onCancel={() => console.log('KYC cancelled')}31 />32 );33}
React imports from
@flonkid/kyc; Vue / Angular / vanilla import theFlonkKYCclass from@flonkid/kyc/core(React-free); no build step uses thewidget.jsscript tag. See the sections below for each.
If your endpoint needs auth, pass requestHeaders:
tsx<FlonkKYCWidgetpublishableKey="pk_live_..."serverUrl="/api/kyc/create-session"requestHeaders={{ Authorization: `Bearer ${token}` }}/>
Option B — you create the session (sessionId + embedToken)
Your code creates the session first (e.g. behind a paywall, an age gate, or a confirmation screen), then hands the credentials to the widget. More control over when and whether a session is created.
tsx1import { useState } from 'react';2import { FlonkKYCWidget } from '@flonkid/kyc';34export default function KYCVerification() {5 const [session, setSession] = useState<{ sessionId: string; embedToken: string; qrCodeUrl?: string } | null>(null);67 const startKYC = async () => {8 // Your backend creates the session with the secret key9 const res = await fetch('/api/kyc/create-session', {10 method: 'POST',11 headers: { 'Content-Type': 'application/json' },12 body: JSON.stringify({ email: user.email }),13 });14 setSession(await res.json()); // { sessionId, embedToken, qrCodeUrl }15 };1617 return (18 <>19 <button onClick={startKYC}>Start Verification</button>2021 {session && (22 <FlonkKYCWidget23 publishableKey="pk_live_..." // instant branded loader (optional)24 sessionId={session.sessionId}25 embedToken={session.embedToken}26 qrCodeUrl={session.qrCodeUrl}27 lang="de"28 onSuccess={(result) => { setSession(null); console.log('Verified:', result); }}29 onError={(error) => { setSession(null); console.error('Failed:', error); }}30 onCancel={() => setSession(null)}31 />32 )}33 </>34 );35}
Which one?
Option A (serverUrl) | Option B (sessionId + embedToken) | |
|---|---|---|
| Setup | 1 component | Button + state + API call + component |
| Session lifecycle | SDK manages it | You manage it |
| Best for | Quick, simple flows | Custom UX, conditional flows |
| Auth to your backend | requestHeaders prop | Your own fetch |
Instant branding with publishableKey
Pass your publishableKey (from Dashboard → API Keys) and the loading screen
paints your brand color on the first frame — no flash of the default green.
Under the hood the SDK resolves branding straight from the key (a Redis-cached read on the API) in parallel with session setup, races it against an 800 ms budget, and updates the color in place once the full tokens arrive. If the key hasn't resolved within the budget, it shows a neutral fallback rather than a blank screen.
This works in both flows — Option A and Option B. Without a
publishableKey, branding is resolved from the session instead, so the loader
shows the default color until that request returns.
publishableKeyis safe to expose in client code — it only reads public branding and issues short-lived widget tokens. Your secret key stays on the server.
Props / config reference
The React <FlonkKYCWidget> props and the kyc.init(config) object share the
same shape.
| Field | Type | Notes |
|---|---|---|
publishableKey | string | pk_live_* / pk_sandbox_*. Enables instant branding. Optional but recommended. |
serverUrl | string | Your session-creation endpoint (Option A). Absolute URLs must be HTTPS (localhost exempt); relative paths are fine. |
sessionId | string | Pre-created session id (Option B). |
embedToken | string | Short-lived token for sessionId (Option B). |
qrCodeUrl | string | The session's qrCodeUrl — needed for the desktop→mobile QR. Return it from your serverUrl endpoint, or pass it in Option B. |
clientMetadata | object | Passed through to your backend / webhooks. email is required by most flows. |
requestHeaders | Record<string,string> | Extra headers on the serverUrl POST — e.g. Authorization. |
lang | WidgetLanguage | UI language, e.g. 'de', 'en', 'uk'. |
overlayColor | string | Backdrop color behind the widget. |
allowManualUpload | boolean | Override the project default for manual document upload. |
autoOpen | boolean | React only. Open on mount. Default true. |
onSuccess | (result) => void | Flow finished — branch on result.status, it is not always approved. |
onError | (error: string) => void | Session or verification failed. |
onCancel | () => void | User closed the widget. |
onReady | () => void | Widget iframe loaded and ready. |
onDiagnostic | (event) => void | Observability hook — see Debugging below. |
Handling the outcome — result.status
onSuccess means the flow finished, not that the user is verified. The result carries a
status you must branch on:
result.status | Meaning | What your UI should do |
|---|---|---|
completed | Verified | Unlock the flow — but confirm server-side via the verification.completed webhook. |
manual_review | Under human review | Show a waiting state: "Your verification is being reviewed — you'll receive the result by email (check spam). Usually takes minutes — fast during business hours — and up to 24 hours." Don't offer a new verification; wait for the webhook. |
action_required | One step still missing | The user must redo one part (result.nextAction, e.g. resubmit_back). Reopening the widget resumes exactly that step. |
failed / other | Not verified | Let the user try again. |
Your backend also receives an informational verification.status_changed webhook when a session
enters manual_review or action_required (with next_action), and the final decision events
after review — gate account access on webhooks, never on the browser callback alone. See
Webhooks.
Other frameworks — the FlonkKYC class
Off React, drive the widget with the framework-agnostic FlonkKYC class.
Import it from @flonkid/kyc/core — the React-free entry — so your bundler
(Vite, Angular, Rollup) never tries to resolve react. It takes the same config
as <FlonkKYCWidget> and returns a handle with destroy() for cleanup. Vue,
Svelte, Angular, Solid, or plain TypeScript all use the same pattern:
new FlonkKYC() → await kyc.init(...) → widget.destroy() on teardown.
Why
/core? The default@flonkid/kycentry re-exports the React component, so importing it makes your bundler resolvereact. In a non-React project that has noreactinstalled, that breaks the production build.@flonkid/kyc/coreomits the component entirely — zero React, zero config.
tsimport { FlonkKYC } from '@flonkid/kyc/core';const kyc = new FlonkKYC();const widget = await kyc.init({publishableKey: 'pk_live_...',serverUrl: '/api/kyc/create-session',clientMetadata: { email: 'user@example.com' },lang: 'de',onSuccess: (result) => console.log('Verified:', result),onError: (error) => console.error('Failed:', error),});// Later, when navigating away:widget.destroy();
Vue 3
Call init from a method/handler and tear down in onUnmounted:
vue<script setup lang="ts">import { onUnmounted } from 'vue';import { FlonkKYC } from '@flonkid/kyc/core';let widget: { destroy(): void } | null = null;async function verify() {const kyc = new FlonkKYC();widget = await kyc.init({publishableKey: 'pk_live_...',serverUrl: '/api/kyc/create-session',requestHeaders: { Authorization: `Bearer ${token}` }, // if your endpoint is authedonSuccess: (result) => console.log('Verified:', result),onError: (error) => console.error('Failed:', error),});}onUnmounted(() => widget?.destroy());</script><template><button @click="verify">Verify identity</button></template>
Svelte (on:click={verify} + onDestroy) and Angular (call from a component
method, destroy() in ngOnDestroy) follow the identical shape.
No build step — <script> tag
No bundler, no ES modules? Load the classic widget loader and drive it through
the window.KYCWidget global. This is the path for Angular without a wrapper,
WordPress, Rails/Django server-rendered pages, a CMS, or plain HTML — anywhere
you can't import the SDK.
html<!-- data-api points at the API origin (no /v1 suffix). The widget origin istaken from this script's own URL. --><script src="https://widget.flonk.id/v1/widget.js"data-api="https://api.flonk.id"></script><button id="verify">Verify identity</button><script>document.getElementById('verify').onclick = async () => {// Your authed backend creates the session and returns { sessionId, embedToken }.const res = await fetch('/api/kyc/create-session', {method: 'POST',headers: { Authorization: 'Bearer ' + token },});const { sessionId, embedToken } = await res.json();window.KYCWidget.init({sessionId,embedToken,onSuccess: (result) => console.log('Verified:', result),onError: (error) => console.error('Failed:', error),onCancel: () => console.log('Cancelled'),});};</script>
The loader exposes:
KYCWidget.… | What |
|---|---|
init(config) | Open the widget. Server-to-server: { sessionId, embedToken }. Client-side: { publishableKey } (the loader fetches a short-lived widget token itself). Returns { destroy() }. |
preview(config) | Render in preview mode (no real session) for layout/branding checks. |
embed(config) | Mount inline into an element instead of the full-screen overlay. |
version | The loader version string. |
Keep the secret server-side here too. The script-tag path still creates the session on your authenticated backend — the browser only ever receives
sessionId+embedToken(or uses the publicpk_*). It self-prewarms on load; adddata-prewarm="eager"to warm the full bundle early.
Performance — prewarming
The widget appears faster if you warm it before the user clicks. These are static methods — call them on page mount or route enter. They never create a session and are SSR-safe (no-op on the server).
FlonkKYC.prewarm(opts)
Preconnects to the widget origin and prefetches the loader/branding assets at
idle. With level: 'eager' it also mounts a hidden background iframe so the
full bundle is loaded before the click. Returns a cleanup function.
tsimport { FlonkKYC } from '@flonkid/kyc';// Warm assets at idle as soon as the page loads:FlonkKYC.prewarm({ publishableKey: 'pk_live_…' });// High-intent page — also load the full widget bundle in the background:FlonkKYC.prewarm({ publishableKey: 'pk_live_…', level: 'eager' });// Or warm only when the user shows intent (hover/focus/in-view of a button):const stop = FlonkKYC.prewarm({ publishableKey: 'pk_live_…', level: 'intent', trigger: btnEl });
level | What it warms | Best for |
|---|---|---|
'connect' (default) | preconnect + idle asset prefetch | most pages |
'intent' | warms on hover/focus/scroll-into-view of trigger | general pages — zero cost for visitors who never engage |
'eager' | also pre-mounts a hidden iframe (full bundle) | dedicated, high-click-through KYC pages |
'none' | nothing | disable |
FlonkKYC.preloadBranding(opts)
Resolves the project's brand color ahead of time (module-level cache, 5-min TTL) so the loader paints your color on the first frame with no branding round-trip at click time. Safe to call repeatedly; concurrent calls dedupe.
tsFlonkKYC.preloadBranding({ publishableKey: 'pk_live_…' });
Classic
<script src=".../widget.js">integration? It self-prewarms on load; adddata-prewarm="eager"to the tag for the eager level.
Debugging
Every SDK degradation is observable — nothing fails silently. Pass
onDiagnostic, or flip a global flag to mirror events to the console:
tsconst kyc = new FlonkKYC({onDiagnostic: (e) => console.log(`[flonk:${e.code}] ${e.message}`, e.detail),});// Or, without code (also honored by the classic widget.js loader):window.__FLONK_DEBUG__ = true;
Console output is silent unless onDiagnostic is set or __FLONK_DEBUG__ is
truthy, so there's no noise in production. Common codes: LOADER_SCRIPT_BLOCKED,
LOADER_FALLBACK_BUNDLED, PREWARM_SKIPPED, READY_TIMEOUT_REVEAL,
PROTOCOL_VERSION_MISMATCH. See
Troubleshooting → Widget / Loader Issues.
Framework notes
Next.js / SSR
FlonkKYCWidget is a client component — it touches the DOM and uses
useEffect. In the App Router, render it from a file marked 'use client':
tsx'use client';import { FlonkKYCWidget } from '@flonkid/kyc';export function Verify() {return <FlonkKYCWidget publishableKey="pk_live_..." serverUrl="/api/kyc/create-session" />;}
The SDK compiles JSX with React's automatic runtime (react/jsx-runtime),
so it works under SSR and bundlers like Turbopack without any globalThis.React
shim. See the Changelog → SDK for the version this landed
in (1.7.0).
React StrictMode
In development StrictMode mounts effects twice. The SDK guards against double-initialization, so you don't need to disable StrictMode or add your own mount guard.
Vue / Nuxt / other SSR
The FlonkKYC class touches window, so call init only on the client —
from a Vue method, onMounted, or behind a <ClientOnly> boundary in Nuxt,
never during server render. The prewarm/preloadBranding statics are SSR-safe
(they no-op on the server).
Cleanup
- React: unmounting
<FlonkKYCWidget>tears the widget down automatically. - Vanilla: call
widget.destroy()when you're done.