Notable changes to the Flonk KYC platform and the official SDK. Dates are in UTC. The SDK follows semantic versioning.
Platform
2026-06 — API versioning + idempotency
- Date-pinned API versioning. Requests may send a
Flonk-Versionheader; the server resolves it (absent/unknown → current baseline), echoes it back, and the API is additive-only within a version. Lays the rails for future breaking changes without breaking deployed clients. No behaviour change for existing callers. - Idempotent session creation.
POST /v1/sessionsaccepts anIdempotency-Keyheader — a retry returns the original session instead of a duplicate (concurrent in-flight retry →409). Keys scoped per project, 24h.
2026-06 — Replay-protected webhook signatures
- Webhooks now include a timestamped
X-Signatureheader (t=<unix>, v1=<hex>) in addition to the existingX-Signature-256(sha256=). The timestamp is signed, so a captured request is rejected once it's outside the skew window (5 min) — real replay protection.X-Signature-256is unchanged and stays for backward compatibility; existing integrations keep working. Verify either with the SDK'sconstructEvent, and dedupe retries byevent.id.
2026-05 — Faster widget theming
- Design tokens are now cached in Redis per project. The
GET /public/design-tokensendpoint — the widget's first, blocking request — previously ran a database join on every widget load. It now serves from a Redis cache (5-minute TTL) keyed by project, with the cache invalidated automatically whenever a project's branding color changes. The widget's brand color resolves from a sub-millisecond cache read on the hot path.
SDK
The browser/Node package published as
@flonkid/kyc.
2.0.0
Breaking — read this before upgrading.
verify.flonk.idis the default widget origin. An integration that never passedwidgetUrlwas rendering the previous widget and now renders widget-v2: different capture UX, auto-capture and face detection. No exported signature moved, which is exactly why this is a major — it would otherwise arrive silently on annpm update. Pinnew FlonkKYC({ widgetUrl: 'https://widget.flonk.id' })to stay where you were. If you already setwidgetUrl, nothing changes for you.- The CSP block changed, and it is not a rename.
frame-srcnamesverify.flonk.id;script-srcnamesapi.flonk.idonly — the widget origin hands the host page no script and is removed from that directive, not renamed;img-srcis gone entirely because both loaders draw the spinner withcreateElementNSand fetch no image. Copy the block from the README rather than editing the old one in place. Permissions-Policyis documented for the first time. It is the most common cause of a camera that never starts on an otherwise correct integration: the SDK delegatescamerato the frame, butallowcan only hand down what the host page holds. Not new behaviour — it worked this way on the previous widget too — but it now appears where a 2.0 upgrader will read it.
Added
- A
<script>tag build.GET /v1/public/widget-v2.jsregisterswindow.KYCWidgetand reads its configuration fromdata-*attributes, so a site with no build step can integrate.data-emailis required — the person being verified has to be named. - A live inline mount.
mountInline()runs a real verification inside your own layout — same session flows, same wire — with no overlay, no scroll lock and no z-index takeover. Not to be confused withembed(), which is a themed mock for dashboards. redirect()sends the whole page to the widget, creating the session first so a failing backend surfaces throughonErrorbefore the page is left. For in-app browsers and WebViews, where a first-party origin is far likelier to be granted the camera than an iframe.
Fixed
- The modal was an invisible sheet over a page that looked live. Nothing painted a backdrop once the loader was destroyed at reveal, so the host page sat bright and apparently interactive under a sheet that swallowed every click. There is now one scrim, alive from open to close, carrying the dialog semantics and the focus ring — and it owns the body-scroll lock, which two places used to fight over and leave the host page permanently fixed after close.
- The loader painted Flonk's own brand colour on the merchant's page before the project's colour resolved, and a later fix stopped an unknown colour from overwriting the one the server had already baked in.
- Two of four subpaths shipped no type declarations (
/serverand/types— a race in the build's clean step), andrequire.typespointed at ESM declarations, so a CJS consumer undernode16hit TS1479. - The server client refused the one key shape that enables test mode.
- The desktop modal hid 136 px of every content-heavy screen.
prewarmat levelintentbehaved likeeagerwhen given no trigger — it loaded the full widget bundle on every page view instead of degrading toconnect. The script-tag path cannot supply a trigger at all, so every page that asked for the middle level was silently getting the heaviest one. Its design-token prefetch was also requesting a URL that had 404'd since it was written, so that half of prewarm never warmed anything.
1.9.4
- Faster widget start — dropped a redundant token round-trip. In the
serverUrlandsessionId+embedTokenflows the SDK passed the embed token to the widget iframe only astoken, notembedToken. The widget keys its fast path on theembedTokenparam, so without it it fell back to a deprecated server exchange — an extraPOST /public/session/:id/tokenbefore verification could start. The SDK now sendsembedTokentoo (as the classicwidget.jsloader already did), so the widget initializes directly and that request is gone. No integration changes needed.
1.9.3
- New:
@flonkid/kyc/core— a React-free entry. The default@flonkid/kycentry re-exports theFlonkKYCWidgetReact component, so it importsreact. In a non-React project (Vue, Angular, Svelte, vanilla) that has noreactinstalled, that broke the production bundler build. Import theFlonkKYCclass from@flonkid/kyc/coreinstead — same class, zero React, zero config. Fully additive — React users keep importing from@flonkid/kycunchanged. See Frontend SDK → Other frameworks.
1.9.2
- Fixed: desktop→mobile QR didn't render in the
serverUrlflow. The SDK now forwardsqrCodeUrlto the widget. Your create-session endpoint should respond with{ sessionId, embedToken, qrCodeUrl }(thesessionId+embedTokenflow can also passqrCodeUrlininit). Previously only the classicwidget.jsloader passed it. - Docs: clarified that the
serverUrlendpoint must return{ sessionId, embedToken }(the API'sidmapped tosessionId) — returning the raw session object silently broke the widget at the welcome step.
1.9.1
- Observability —
onDiagnostichook. The SDK now emits a structured diagnostic at every degradation/branch point (loader fallback, blocked loader script, prewarm skipped, ready-timeout reveal, protocol mismatch) instead of failing silently. PassonDiagnostic, or setwindow.__FLONK_DEBUG__ = trueto mirror events to the console. Zero-cost when unused. See Frontend SDK → Debugging. - Versioned SDK ⇄ iframe contract. The wire between the SDK and the widget
iframe is now versioned (
PROTOCOL_VERSION) separately from the package version and is additive-only, so any SDK build stays compatible with any iframe build. APROTOCOL_VERSION_MISMATCHdiagnostic surfaces a stale-cached widget. - Webhooks — added a replay-protected signature. Flonk now also sends a
timestamped
X-Signature(t=,v1=) header alongsideX-Signature-256(both HMAC-SHA256 with your secret). The signed timestamp adds replay protection (old requests rejected outside the skew window);X-Signature-256stays fully valid for authenticity + integrity.constructEventverifies either. Signature comparison is now branchless constant-time. Dedupe retries byevent.idin your own store — see Webhooks → Idempotency.
1.9.0
- Self-prewarming. New static
FlonkKYC.prewarm({ level })— preconnect + idle asset prefetch, withlevel:'intent'(warm on hover/focus/in-view) andlevel:'eager'(pre-mount a hidden iframe so the full bundle loads before the click). The classicwidget.jsloader self-prewarms on load (data-prewarm="eager"for the eager level). Never pre-creates a session; SSR-safe. See Frontend SDK → Performance. - Server-hosted loader. The branded loading screen is now served from the
API (
/v1/public/loader.js) so it can be restyled and fixed for all integrations without a client SDK bump. The SDK falls back to its bundled loader if the script is blocked (CSP/CORP/offline). - Fixed: loader could hang on "Initialisierung…". The widget signalled
READYfrom arequestAnimationFramecallback, which the browser pauses for a hidden/backgrounded iframe — deadlocking the reveal for some users.READYis now driven by timers only, and the SDK reveals on a safety timeout even ifREADYis missing, so a permanent stuck loader can't happen.
1.7.0
- Fixed:
React is not definedunder SSR / Turbopack. The build emitted bareReact.createElement(...)calls without importing React, which crashed in environments without a globalReact(Next.js App Router, Turbopack runtime). The bundle now compiles JSX with React's automatic runtime (react/jsx-runtime), so noglobalThis.Reactshim is needed.reactandreact/jsx-runtimeremain peer/external — your app's React is used. - Improved: instant branding now works in the
sessionId+embedTokenflow. PreviouslypublishableKeyonly sped up branding in theserverUrlflow; in the manual-session flow it was ignored and branding waited on the session request. Now, whenpublishableKeyis supplied, the SDK resolves the brand color from the key in parallel with the session (raced against an 800 ms budget, color updated in place when full tokens arrive) — matching theserverUrlfast path. - Docs:
WidgetInitConfig.publishableKeyJSDoc updated to describe instant branding in both flows.
1.5.1
- Fixed branded loader not appearing after the
serverUrlsession resolved.
1.5.0
- Added the
requestHeadersprop to send authentication headers (e.g. a JWT) with theserverUrlPOST request. - Introduced
publishableKey-driven instant branded loader for theserverUrlflow.
1.4.x
1.4.2— Hardened against double initialization in React StrictMode using a generation counter.1.4.1— Surface server error bodies; show errors in the loader for theserverUrlflow instead of failing silently.1.4.0— Replaced looseanytypes with precise types across the SDK; alignedonSuccessto emit a typedVerificationResult.
See the Frontend SDK guide for current usage.