# Camera issues

| Issue | Solution |
| --- | --- |
| Black screen / no camera preview | Use HTTPS (or `localhost` during development) — `getUserMedia` is gated behind a secure context. |
| Permission denied | User must grant camera access in the browser. Surface the SDK's `permissions.denied` state and link them to browser-specific instructions. |
| Camera in use / `NotReadableError` | Another tab or app holds the camera. The SDK can't release a camera held by another process; instruct the user to close other camera consumers. |
| Camera freezes when remounting | Tear down the previous module before mounting a new camera-using module. Call `manager.stop()` on the old manager / unmount the old element. |
| iOS Safari: video doesn't play inline | The SDK's own `<video>` elements are tagged `playsinline`/`muted`/`autoPlay`. If you build a custom UI in headless mode, replicate those attributes — without `playsinline`, iOS opens video full-screen and breaks face detection. |

# WASM issues

| Issue | Solution |
| --- | --- |
| WASM not loading | Confirm `setup({ wasm: ... })` actually ran. With CDN defaults, paths are optional; with `setup({ wasm: false })` (or omitted), WASM only loads lazily when a selfie or ID-capture step starts. |
| WASM 404 | When self-hosting, verify `wasmPath`, `glueCodePath`, and `modelsBasePath` resolve. The defaults assume `models/` lives next to the WASM binary. |
| `CompileError: WebAssembly.compile()` | Server is returning the WASM file with the wrong MIME type. Configure your host to return `Content-Type: application/wasm` for `.wasm` files (Nginx: add to `mime.types`; Apache: `AddType application/wasm .wasm`). |
| `RangeError`, `Failed to load module script` | Browser is too old or doesn't support WASM SIMD. Check browser support; fall back to non-SIMD by setting `setup({ wasm: { useSimd: false } })`. |
| Face/document detection never triggers | Model files are missing. Check the network tab for failing requests under `modelsBasePath`. |

# Session issues

| Issue | Solution |
| --- | --- |
| "SDK not configured. Call setup({ apiURL: '...' }) first." | Call `setup({ apiURL })` before `createSession()` or any module manager method. |
| "Token is required" | Provide `token` (from `createSession()`) on `<incode-flow>`'s config, or use the self-loading variant with `apiKey` \+ `configurationId` (prototyping only). |
| Token expired mid-flow | Tokens have a TTL set per-tenant in the Dashboard. Long verification sessions can outlive the token. Either bump the TTL in the Dashboard or restart the flow with a fresh session. |
| Invalid API key | Verify the key in the Incode Dashboard under Settings → API Keys. Ensure your domain is allowed under CORS. |
| Invalid configuration | Make sure the `configurationId` exists, is active, and matches the tenant the API key belongs to. |
| "No registered module found for: <KEY>" | A backend-driven step doesn't have a state machine registered in `createOrchestratedFlowManager({ modules: ... })`. Log `flowState.steps` after `flowManager.load()` to see which keys the backend emitted, and register them. |

# Iframe embedding

| Issue | Solution |
| --- | --- |
| Camera prompt doesn't appear in iframe | Add `allow="camera; microphone"` to the iframe element. `getUserMedia` requires explicit feature-policy delegation across iframe boundaries. |
| `<incode-flow-completed>` redirect blocked | The default redirect uses `window.location.href`, which fails if the iframe doesn't have `allow-top-navigation` (or `allow-top-navigation-by-user-activation`). Either grant the sandbox permission or set `disable-redirect` on `<incode-flow-completed>` and handle navigation from the parent. |
| Cookies / storage isolation | The SDK uses `localStorage` for some state (e.g. Deepsight session). Browsers in strict-tracking-prevention mode partition third-party iframe storage; if the iframe is cross-origin, plan for state to be scoped per-iframe and not shared with the host. |

# Mobile Safari

| Issue | Solution |
| --- | --- |
| Camera prompt requires a user gesture | Don't auto-trigger `manager.requestPermission()` (or auto-mount `<incode-selfie>` / `<incode-id>`) on page load. iOS Safari blocks `getUserMedia` outside of user-gesture handlers. Wire it to a tap on a "Continue" button. |
| Camera fails after device rotation | Some iOS versions invalidate the existing `MediaStream` on orientation change. Listen for `orientationchange` (or use a `screen.orientation`-based hook) and call `manager.reset()` or remount the module to acquire a fresh stream. |
| Tutorial Lottie animations don't autoplay | iOS suspends `requestAnimationFrame` in background tabs and after user inactivity. Re-trigger the tutorial on focus if needed. |

# Content Security Policy

If your app uses a strict CSP, the SDK needs:

| Directive | Why |
| --- | --- |
| `script-src 'wasm-unsafe-eval'` (or `'unsafe-eval'` for older browsers) | WASM compilation/instantiation needs eval-style permissions. |
| `worker-src blob:` | Some ML model loading paths spawn workers from blob URLs. |
| `connect-src https://*.incodesmile.com` (replace with your environment) | XHR/fetch to Incode API endpoints. |
| `img-src 'self' blob: data:` | Captured frames are rendered to canvas/`<img>` from blob/data URIs. |
| `media-src 'self' blob:` | Camera streams. |
| `style-src 'self' 'unsafe-inline'` (or hash/nonce) | Some module styles inline values during render. |

Test against your CSP before deploying — browsers fail silently for some directives, especially WASM-related ones.

# Styling issues

| Issue | Solution |
| --- | --- |
| Styles not applied | Import `@incodetech/web/base.css` plus a theme (`@incodetech/web/themes/light.css` or `dark.css`), plus per-module CSS like `@incodetech/web/flow/styles.css`. |
| Overrides not taking effect | Override at `:root` with `--variable: value;`. Make sure the override loads after the SDK theme. |
| Component not visible | Parent container needs explicit dimensions. The element has `display: block` but no intrinsic size — use `height: 100vh` (or any height) on the parent or a `style="display:block; height:100vh"` on the element itself. |
| Brand color tweak doesn't propagate | Override the `--primitive-color-brand-*` scale rather than individual component tokens — semantic and component tokens cascade from the primitives. |

# Build / TypeScript issues

| Issue | Solution |
| --- | --- |
| `Property 'incode-flow' does not exist on type 'JSX.IntrinsicElements'` | React 18 strict-checks JSX. Add the JSX augmentation from [Framework Integration → TypeScript: JSX support for `incode-*` tags](https://developer.incode.com/docs/web-sdk-2-framework-integration#typescript-jsx-support-for-incode--tags). React 19+ doesn't need this. |
| `<incode-flow config={{token}}>` renders nothing on React 18 | React 18 serializes JSX props through `setAttribute`, so object/function props become `"[object Object]"`. Use the ref + `useEffect` pattern from [Framework Integration → Universal pattern](https://developer.incode.com/docs/web-sdk-2-framework-integration#universal-pattern-works-on-react-168). React 19+ can use the simpler form. |
| Bundle too large | Use subpath imports (`@incodetech/web/selfie`, never the package root). See [Bundle Optimization](https://developer.incode.com/docs/web-sdk-2-bundle-optimization). |
| Tree-shaking not working | Use subpath imports (`@incodetech/web/<module>`), not the package root. Verify with your bundler's size analyzer. |
| Circular import or `Cannot find module '@incodetech/infra'` | `@incodetech/infra` is internal — never import from it directly. Use the `@incodetech/core/<subpath>` re-exports. |

# Network issues

| Issue | Solution |
| --- | --- |
| CORS errors on `/omni/start` (or other API endpoints) | Allowlist your domain in the Incode Dashboard. |
| API requests failing | Verify the `apiURL` matches the environment your API key was issued for (demo vs. production tenants are separate). |
| Firewall blocking | Allow `*.incodesmile.com` in your egress allow-list (and the CDN domain you point WASM paths at, if self-hosting). |
| Upload progress stuck at 0% | Older browsers don't expose upload progress on `fetch`. The SDK falls back to `XMLHttpRequest` when an `onUploadProgress` callback is set. |

# Flow-level edge cases

| Issue | Solution |
| --- | --- |
| Selfie ends in `closed` state and the flow doesn't continue | `closed` is final — the manager won't transition further on its own. In orchestrated flows, call `flowManager.completeModule()`. In standalone usage, navigate the user out. |
| ID capture stuck on `capture.success` | Call `idManager.nextStep()` to advance to `frontFinished` or `processing`. The state machine waits for the explicit advance. |
| `expired` state — `reset()` does nothing | The `expired` state only responds to `RETRY_CAPTURE`. Call `idManager.retryCapture()`. |
| Mandatory consent screen shows for every flow | The screen only renders when the upload response sets `showMandatoryConsent: true`, which is driven by `regulationType` in the session. Verify the configuration in the Dashboard. |

# Getting help

1. Check the browser console for SDK errors. The SDK prefixes most warnings/errors clearly.
2. Inspect the Network tab — failing API requests usually have a meaningful error body.
3. Subscribe to flow events via `subscribeEvent` from `@incodetech/core/events` to capture the SDK's analytics stream — useful when reproducing issues for support.
4. Contact support@incode.com with: SDK alpha version, browser + OS, the failing flow's `configurationId`, and the steps to reproduce.
