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.
html<script src="https://api.flonk.id/v1/public/widget-v2.js"data-pk="pk_live_..."></script>
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 →FlonkKYCfrom@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.
| Attribute | Maps to | Default |
|---|---|---|
data-publishable-key (alias data-pk) | publishableKey — your pk_live_* / pk_sandbox_*. Browser-safe. | none |
data-api | apiBase — the REST base including the /v1 suffix. | https://api.flonk.id/v1 |
data-primary-color | colors.primaryColor for preview() and embed() only — the live widget takes its branding from your key or session. | none |
data-overlay-color | Any CSS color for the modal backdrop, alpha included. | dark translucent scrim |
data-email | Required 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-id | Merged into clientMetadata as clientId. Optional. | none |
data-widget-url | Origin 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-prewarm | connect, intent, eager, none. See Prewarming. Unrecognised values are ignored. | connect |
data-debug | Present (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-pkeven 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.
html<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>
You can also let the script call your endpoint for you — pass serverUrl
instead of a session, exactly like the SDK's Option A:
html<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>
Your endpoint must respond with { sessionId, embedToken, qrCodeUrl } — see
Frontend & Backend → Backend.
onSuccessfiring does not mean the user is verified. Branch onresult.status(completed,manual_review,action_required,failed) and confirm server-side from the webhook orGET /v1/verifications/{sessionId}.
3. The window.KYCWidget surface
| Member | What 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. |
version | Version 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:
html<script src="https://api.flonk.id/v1/public/widget-v2.js"data-pk="pk_live_..."data-prewarm="eager"></script>
| Value | What it warms |
|---|---|
connect (default) | preconnect + idle prefetch of the loader/branding assets |
eager | also pre-mounts a hidden iframe, so the full widget bundle is loaded before the click |
none | nothing |
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-Policyis what breaks the camera, not CSP. If your site sends a restrictive one, the iframe'sallow="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.
html<script src="https://api.flonk.id/v1/public/widget-v2.js"data-pk="pk_live_..."data-debug></script>
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.js | widget-v2.js | |
|---|---|---|
| URL | https://widget.flonk.id/v1/widget.js | https://api.flonk.id/v1/public/widget-v2.js |
| CSP | script-src https://widget.flonk.id | script-src https://api.flonk.id — the origin your policy already names for the loader and the API |
| Widget origin | derived from the script's own URL | the SDK default, https://verify.flonk.id; not settable from the tag |
data-api | API origin, without /v1 | full REST base, with /v1 |
redirect() | not available | available |
generateSecondaryColor() | exposed on the global | removed — the widget derives the secondary color from your brand color itself |
| Everything else | init, preview, embed, version, noConflict | unchanged 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.