Troubleshooting
Solutions for common KYC integration issues.
SDK Issues
Module Not Found
Symptom: Cannot find module '@flonkid/kyc'
Solution: Install the SDK:
bashnpm install @flonkid/kyc
SSR Error (Next.js)
Symptom: window is not defined or document is not defined
Solution: Use dynamic import to avoid SSR:
typescript// Wrong: top-level import runs on serverimport { FlonkKYC } from '@flonkid/kyc';// Correct: dynamic import runs only in browserconst startKYC = async () => {const { FlonkKYC } = await import('@flonkid/kyc');const kyc = new FlonkKYC();// ...};
serverUrl HTTPS Error
Symptom: serverUrl must use HTTPS in production
Solution: The SDK enforces HTTPS for absolute URLs. Use a relative path or HTTPS:
typescript// Relative path (OK)serverUrl: '/api/kyc/create-session'// HTTPS (OK)serverUrl: 'https://api.myapp.com/kyc/create-session'// HTTP (rejected, except localhost)serverUrl: 'http://api.myapp.com/kyc/create-session' // Error!
Widget / Loader Issues
Turn on debug logging first
Before anything else, make the SDK tell you what it's doing. Every degradation
(blocked loader script, prewarm skipped, no READY from the iframe, protocol
mismatch) is reported — silent by default, visible when you opt in:
typescript// Anywhere before the widget opens — no rebuild needed:window.__FLONK_DEBUG__ = true;// Or capture events in code:const kyc = new FlonkKYC({onDiagnostic: (e) => console.log(`[flonk:${e.code}] ${e.message}`, e.detail),});
On the script-tag path, add data-debug to the tag (or set
the same global). Then reproduce and read the [flonk:*] codes.
Loader stuck / widget never appears
Symptom: the loading overlay stays up; the widget content never shows.
Diagnose: enable debug logging (above) and look for:
| Code | Cause | Fix |
|---|---|---|
READY_TIMEOUT_REVEAL | The iframe never sent READY (usually a CSP frame-src block or a hard error inside the iframe). | Allow the widget origin in frame-src — https://verify.flonk.id unless you pinned widgetUrl (see below); check the iframe's own console. |
LOADER_SCRIPT_BLOCKED | The branded loader script was blocked (CSP script-src, CORP, or offline). | Allow https://api.flonk.id in script-src. The SDK still works with its bundled loader. |
PROTOCOL_VERSION_MISMATCH | The SDK and the cached iframe speak different wire versions. | Hard-refresh / clear the site's cache to drop the stale widget. |
The SDK reveals the widget on a safety timeout even if READY is missing, so a
permanent "stuck loader" should not happen — if it does, it's almost always a
frame-src CSP block stopping the iframe from loading at all.
Content Security Policy (CSP) blocks the widget
Symptom: the iframe or loader script is blocked; console shows a CSP
violation or ERR_BLOCKED_BY_RESPONSE.
Solution: if your site sends a Content-Security-Policy, allow our origins:
Content-Security-Policy:frame-src https://verify.flonk.id;script-src https://api.flonk.id;connect-src https://api.flonk.id;
Three directives, one origin each:
frame-src— the widget iframe.https://verify.flonk.idis the default from SDK 2.0; if you pinnedwidgetUrl: 'https://widget.flonk.id'to stay on the previous widget, name that instead.script-src— the server-hosted loader (api.flonk.id/v1/public/loader.js), the only cross-origin script the SDK adds to your page. On the script-tag path it also carries the SDK bundle itself, so there it is not optional.connect-src— the SDK's ownfetchcalls, all to the API. The widget's own REST and WebSocket traffic happens inside the frame and answers to the frame's policy, not yours.
Upgrading from a pre-2.0 policy: the widget origin is removed from
script-src, not renamed. It hands your page no script — only the framed document, which loads its own bundle under its own policy.img-srcis gone too: both loaders draw the spinner withcreateElementNSand fetch no image.
You do not need any CORS or Cross-Origin-Resource-Policy config on your
side — our public assets already send the right cross-origin headers, and the
API handles CORS for the SDK's requests. If the loader script specifically is
blocked, the SDK falls back to its bundled loader and still works (you just lose
dashboard-driven loader branding).
The camera never starts, and CSP looks fine
Symptom: the widget loads and reaches the capture step, but the camera prompt never appears — or appears and immediately fails.
Solution: this is almost always Permissions-Policy, not CSP. The SDK
delegates camera to the iframe via its allow attribute, but allow can only
hand down a capability your page already holds. A restrictive header on your
side kills capture in the sub-frame:
Permissions-Policy: camera=(self "https://verify.flonk.id"), microphone=(self "https://verify.flonk.id")
If you send no Permissions-Policy at all, nothing is restricted and the camera
works. The header only becomes a problem once you start sending one.
API Issues
401 Unauthorized
Symptom: invalid_api_key error.
Checklist:
- Using correct secret key (not publishable key)
- Key is for correct environment (test/live)
-
Bearerprefix in Authorization header - No extra spaces in key
typescript// Correctheaders: {'Authorization': `Bearer ${secretKey}`,}// Wrongheaders: {'Authorization': secretKey, // Missing Bearer'Authorization': `Bearer ${secretKey} `, // Extra space}
400 Bad Request
Symptom: Request validation errors.
Common causes:
- Invalid or missing data:
typescript{"clientMetadata": {"email": "user@example.com", // recommended"userId": "user_123" // optional}}// All clientMetadata fields are optional, but email is// recommended so webhook events can be matched to users.
- Invalid JSON:
typescript// Use JSON.stringifybody: JSON.stringify(data)// Notbody: data
Webhook Issues
Webhooks Not Received
Checklist:
- Endpoint is publicly accessible (not localhost)
- HTTPS enabled
- URL registered in Flonk Dashboard
- Server returns 200 OK
Test with ngrok for local development:
bashngrok http 3000# Use ngrok URL in Dashboard
Invalid Signature
Symptom: Signature verification fails.
Solutions:
- Use raw body, not parsed JSON:
typescript// NestJS - enable raw bodyapp.useGlobalPipes(new ValidationPipe());app.use(json({ verify: (req, res, buf) => {req.rawBody = buf;}}));
- Verify correct webhook secret
- Pass the right header —
X-Signature(recommended, replay-protected) or theX-Signature-256.constructEventaccepts either. If you verify manually, use a constant-time compare (crypto.timingSafeEqual), not===.
Duplicate Webhooks
Symptom: Same event processed multiple times.
Cause: delivery is at-least-once (Flonk retries on non-200), so duplicates
are expected. Dedupe by event.id in your own store — a unique DB index or
Redis SET NX:
typescriptconst fresh = await redis.set(`whk:${event.id}`, '1', 'PX', 6 * 60_000, 'NX');if (fresh === null) return; // retry — already handledawait processEvent(event);
See Webhooks → Idempotency for the DB-unique-index variant.
Getting Help
If you can't resolve an issue:
- Check Integration Guide
- Check API Reference
- Contact support: support@flonk.id
Include in support requests:
- Session ID
- Error messages
- Timestamp
- Request/response logs (without sensitive data)