Flonk
Flonk Docs

Script Tag (No Build Step)

Embed the Flonk KYC widget on a page with no bundler: one script tag from api.flonk.id, configured with data-* attributes, driven through the window.KYCWidget global.

Last updated: 8/25/2026
5 min read

For pages that have no build step — WordPress, Shopify, a Rails or Django template, a CMS block, a plain HTML landing page — load the widget with one <script> tag and drive it through the window.KYCWidget global. No bundler, no ES modules, no npm install.

<script src="https://api.flonk.id/v1/public/widget-v2.js"
data-pk="pk_live_..."></script>
html

The file is the same browser SDK the npm package ships, built as a single self-contained IIFE. Behaviour, wire protocol and diagnostics are identical to @flonkid/kyc/core; only the way you configure and call it differs.

Which path do I want? React → @flonkid/kyc. Vue / Angular / Svelte / vanilla-TS with a bundler → FlonkKYC from @flonkid/kyc/core. No build step at all → this page. Native app or fully custom UI → Direct API.


1. Load and configure the tag

Configuration comes from data-* attributes on the script tag itself. They map onto the same options the npm SDK takes — there is no separate loader-only configuration.

AttributeMaps toDefault
data-publishable-key (alias data-pk)publishableKey — your pk_live_* / pk_sandbox_*. Browser-safe.none
data-apiapiBase — the REST base including the /v1 suffix.https://api.flonk.id/v1
data-primary-colorcolors.primaryColor for preview() and embed() only — the live widget takes its branding from your key or session.none
data-overlay-colorAny CSS color for the modal backdrop, alpha included.dark translucent scrim
data-emailRequired for init() and redirect(). The email of the person being verified; merged into clientMetadata as email. Pass it on the call instead if it is only known then. preview() and embed() never ask for it — they render mock data and reach no API.none
data-client-idMerged into clientMetadata as clientId. Optional.none
data-widget-urlOrigin the widget iframe is loaded from. Leave it unset in production. It exists because this script is served by the API, so — unlike the v1 loader — it cannot infer the widget origin from its own URL; set it only to point a page at a local or staging widget build.the SDK's default
data-prewarmconnect, intent, eager, none. See Prewarming. Unrecognised values are ignored.connect
data-debugPresent (no value needed) → mirror SDK diagnostics to the console.off

Attributes not in this table are ignored.

Every attribute is a default: an argument you pass to init(), preview(), embed() or redirect() wins over the tag. Only data-api is fixed at load — it is read once, when the script constructs its client.

Pass data-pk even when your backend creates the session. With the key on the tag the script resolves your brand color in parallel with session setup and paints it on the first frame; without it the loading screen is neutral grey until a round-trip completes. The key is publishable — it is meant to sit in page source.

2. Start a verification

The secret key never touches the browser on this path either. Your backend creates the session; the page receives only sessionId + embedToken.

<script src="https://api.flonk.id/v1/public/widget-v2.js"
data-pk="pk_live_..."
data-api="https://api.flonk.id/v1"></script>
<button id="verify">Verify identity</button>
<script>
document.getElementById('verify').onclick = async () => {
// Your authenticated backend creates the session with the SECRET key and
// returns { sessionId, embedToken, qrCodeUrl }.
const res = await fetch('/api/kyc/create-session', {
method: 'POST',
headers: { Authorization: 'Bearer ' + token }, // your app's session/JWT
});
const { sessionId, embedToken, qrCodeUrl } = await res.json();
window.KYCWidget.init({
sessionId,
embedToken,
qrCodeUrl, // without it the desktop -> mobile QR step cannot render
// Required by init(): the email of the person being verified. Put it on
// the tag as data-email instead when it is known at page render.
clientMetadata: { email: user.email },
onSuccess: (result) => console.log('Verified:', result),
onError: (error) => console.error('Failed:', error),
onCancel: () => console.log('Cancelled'),
});
};
</script>
html

You can also let the script call your endpoint for you — pass serverUrl instead of a session, exactly like the SDK's Option A:

<script>
window.KYCWidget.init({
serverUrl: '/api/kyc/create-session',
requestHeaders: { Authorization: 'Bearer ' + token },
// Required by init(), here or as data-email on the tag.
clientMetadata: { email: user.email },
onSuccess: (result) => console.log('Verified:', result),
onError: (error) => console.error('Failed:', error),
onCancel: () => console.log('Cancelled'),
});
</script>
html

Your endpoint must respond with { sessionId, embedToken, qrCodeUrl } — see Frontend & Backend → Backend.

onSuccess firing does not mean the user is verified. Branch on result.status (completed, manual_review, action_required, failed) and confirm server-side from the webhook or GET /v1/verifications/{sessionId}.

3. The window.KYCWidget surface

MemberWhat it does
init(config)Opens the widget in a full-screen modal. Takes { serverUrl } or { sessionId, embedToken } or { publishableKey }, plus lang, overlayColor, qrCodeUrl, clientMetadata, requestHeaders and the onSuccess / onError / onCancel / onReady callbacks. Returns { iframe, destroy() }.
redirect(config)Navigates the whole page to the widget instead of framing it — the mode for in-app browsers, where the camera prompt has to come from a first-party origin. One-way: the page is gone, so only onError (failure before the navigation) can still fire. Pass replace: true to keep Back from returning here.
preview(config)Renders the widget with mock data and no camera, for layout and branding checks. Creates no session.
embed(config)The same preview, mounted into a container element of yours instead of a modal. Preview only — see below.
versionVersion of the SDK this script was built from.
noConflict()Restores whatever window.KYCWidget pointed at before this script loaded, and returns the Flonk object so you can keep a reference to it. Useful while the legacy loader is still on the page.

embed() renders a preview, not a verification

embed() mounts a themed preview — mock data, no camera, no session, no result. It exists for dashboard-style "this is how the widget will look in your brand colors" panels. It cannot verify anyone, and no callback will ever report a verification from it.

There is no script-tag equivalent of the npm SDK's mountInline() — the mode that runs a live verification inside your own layout. If you need a real verification embedded in the page rather than in a modal, use the npm path with a bundler. On this path, a real verification is init() (modal) or redirect() (full page).

Prewarming

The script warms the connection to the widget origin as soon as it runs. Raise the level on a page where the click is likely:

<script src="https://api.flonk.id/v1/public/widget-v2.js"
data-pk="pk_live_..."
data-prewarm="eager"></script>
html
ValueWhat it warms
connect (default)preconnect + idle prefetch of the loader/branding assets
eageralso pre-mounts a hidden iframe, so the full widget bundle is loaded before the click
nonenothing

intent is accepted too, but it warms on hover/focus of a trigger element that only the npm path can pass; from a script tag there is none, so it degrades to idle warming — the same thing connect does.

Content Security Policy

This path adds a script to your page, so script-src is a hard requirement here — unlike the npm path, where a blocked loader script only costs you dashboard-driven branding:

Content-Security-Policy:
script-src https://api.flonk.id;
frame-src https://verify.flonk.id;
connect-src https://api.flonk.id;

If script-src blocks api.flonk.id, nothing runs at all: window.KYCWidget is never defined and your init() call throws a TypeError.

The widget origin belongs in frame-src only — it hands your page no script. redirect() needs neither, since it navigates your page instead of framing anything.

Permissions-Policy is what breaks the camera, not CSP. If your site sends a restrictive one, the iframe's allow="camera;microphone" delegation is refused and verification dies at the first capture step. See Troubleshooting.

Debugging

Add data-debug to the tag (or set window.__FLONK_DEBUG__ = true before the widget opens) to mirror every SDK diagnostic to the console — blocked loader script, prewarm skipped, no READY from the iframe, protocol mismatch. Silent by default.

<script src="https://api.flonk.id/v1/public/widget-v2.js"
data-pk="pk_live_..."
data-debug></script>
html

Migrating from the legacy widget.js

Existing pages load https://widget.flonk.id/v1/widget.js. That loader keeps working; the migration is a URL swap, because the global keeps its name and the methods keep their signatures.

Legacy widget.jswidget-v2.js
URLhttps://widget.flonk.id/v1/widget.jshttps://api.flonk.id/v1/public/widget-v2.js
CSPscript-src https://widget.flonk.idscript-src https://api.flonk.id — the origin your policy already names for the loader and the API
Widget originderived from the script's own URLthe SDK default, https://verify.flonk.id; not settable from the tag
data-apiAPI origin, without /v1full REST base, with /v1
redirect()not availableavailable
generateSecondaryColor()exposed on the globalremoved — the widget derives the secondary color from your brand color itself
Everything elseinit, preview, embed, version, noConflictunchanged names and shapes

Both scripts register window.KYCWidget. If a page ends up with both during a migration, the second one to load wins; call noConflict() from the one you don't want.