Flonk
Flonk Docs

Frontend SDK

Embed the Flonk KYC widget in any web app — React or vanilla TypeScript. Props, init flows, instant branding, and framework notes.

Last updated: 8/19/2026
5 min read

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.

npm install @flonkid/kyc
bash
ImportRuntimeUse
@flonkid/kycBrowserFlonkKYC class + FlonkKYCWidget React component
@flonkid/kyc/coreBrowser (no React)FlonkKYC class only — React-free entry for Vue / Angular / Svelte / vanilla
@flonkid/kyc/serverNode.jsSessions API + webhook signature verification
@flonkid/kyc/typesTypeScript types only

React is an optional peer dependency. The default @flonkid/kyc entry includes the FlonkKYCWidget React component, so it imports react. If you're not on React, import the FlonkKYC class from @flonkid/kyc/core — a React-free entry, so your bundler never has to resolve React. (Added in 1.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.

1import { FlonkKYCWidget } from '@flonkid/kyc';
2
3export default function KYCVerification() {
4 return (
5 <FlonkKYCWidget
6 publishableKey="pk_live_..."
7 serverUrl="/api/kyc/create-session"
8 clientMetadata={{
9 email: 'user@example.com',
10 userId: 'user_123', // optional
11 }}
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}
19
20// With authentication (JWT)
21function KYCWithAuth({ token }: { token: string }) {
22 return (
23 <FlonkKYCWidget
24 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}
tsx

React imports from @flonkid/kyc; Vue / Angular / vanilla import the FlonkKYC class from @flonkid/kyc/core (React-free); no build step uses the widget.js script tag. See the sections below for each.

If your endpoint needs auth, pass requestHeaders:

<FlonkKYCWidget
publishableKey="pk_live_..."
serverUrl="/api/kyc/create-session"
requestHeaders={{ Authorization: `Bearer ${token}` }}
/>
tsx

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.

1import { useState } from 'react';
2import { FlonkKYCWidget } from '@flonkid/kyc';
3
4export default function KYCVerification() {
5 const [session, setSession] = useState<{ sessionId: string; embedToken: string; qrCodeUrl?: string } | null>(null);
6
7 const startKYC = async () => {
8 // Your backend creates the session with the secret key
9 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 };
16
17 return (
18 <>
19 <button onClick={startKYC}>Start Verification</button>
20
21 {session && (
22 <FlonkKYCWidget
23 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}
tsx

Which one?

Option A (serverUrl)Option B (sessionId + embedToken)
Setup1 componentButton + state + API call + component
Session lifecycleSDK manages itYou manage it
Best forQuick, simple flowsCustom UX, conditional flows
Auth to your backendrequestHeaders propYour 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.

publishableKey is 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.

FieldTypeNotes
publishableKeystringpk_live_* / pk_sandbox_*. Enables instant branding. Optional but recommended.
serverUrlstringYour session-creation endpoint (Option A). Absolute URLs must be HTTPS (localhost exempt); relative paths are fine.
sessionIdstringPre-created session id (Option B).
embedTokenstringShort-lived token for sessionId (Option B).
qrCodeUrlstringThe session's qrCodeUrl — needed for the desktop→mobile QR. Return it from your serverUrl endpoint, or pass it in Option B.
clientMetadataobjectPassed through to your backend / webhooks. email is required by most flows.
requestHeadersRecord<string,string>Extra headers on the serverUrl POST — e.g. Authorization.
langWidgetLanguageUI language, e.g. 'de', 'en', 'uk'.
overlayColorstringBackdrop color behind the widget.
allowManualUploadbooleanOverride the project default for manual document upload.
autoOpenbooleanReact only. Open on mount. Default true.
onSuccess(result) => voidFlow finished — branch on result.status, it is not always approved.
onError(error: string) => voidSession or verification failed.
onCancel() => voidUser closed the widget.
onReady() => voidWidget iframe loaded and ready.
onDiagnostic(event) => voidObservability 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.statusMeaningWhat your UI should do
completedVerifiedUnlock the flow — but confirm server-side via the verification.completed webhook.
manual_reviewUnder human reviewShow 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_requiredOne step still missingThe user must redo one part (result.nextAction, e.g. resubmit_back). Reopening the widget resumes exactly that step.
failed / otherNot verifiedLet 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/kyc entry re-exports the React component, so importing it makes your bundler resolve react. In a non-React project that has no react installed, that breaks the production build. @flonkid/kyc/core omits the component entirely — zero React, zero config.

import { 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();
ts

Vue 3

Call init from a method/handler and tear down in onUnmounted:

<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 authed
onSuccess: (result) => console.log('Verified:', result),
onError: (error) => console.error('Failed:', error),
});
}
onUnmounted(() => widget?.destroy());
</script>
<template>
<button @click="verify">Verify identity</button>
</template>
vue

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.

<!-- data-api points at the API origin (no /v1 suffix). The widget origin is
taken 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>
html

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.
versionThe 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 public pk_*). It self-prewarms on load; add data-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.

import { 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 });
ts
levelWhat it warmsBest for
'connect' (default)preconnect + idle asset prefetchmost pages
'intent'warms on hover/focus/scroll-into-view of triggergeneral pages — zero cost for visitors who never engage
'eager'also pre-mounts a hidden iframe (full bundle)dedicated, high-click-through KYC pages
'none'nothingdisable

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.

FlonkKYC.preloadBranding({ publishableKey: 'pk_live_…' });
ts

Classic <script src=".../widget.js"> integration? It self-prewarms on load; add data-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:

const 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;
ts

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':

'use client';
import { FlonkKYCWidget } from '@flonkid/kyc';
export function Verify() {
return <FlonkKYCWidget publishableKey="pk_live_..." serverUrl="/api/kyc/create-session" />;
}
tsx

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.

Next: Frontend ↔ Backend Integration → · Webhooks →