> For the complete documentation index, see [llms.txt](https://docs.lumiid.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.lumiid.com/api-integration/sdks/lumiid-hosted-widget-integration-guide.md).

# LumiID Hosted Widget Integration Guide

## LumiID hosted widget integration

Embed the LumiID hosted widget in a web iframe or mobile WebView. The widget handles document capture, liveness checks, and verification results.

You only need a **Widget ID**. It is safe to use in frontend code. Keep secret API keys on your backend.

### Before you begin

* Create and configure a widget in the LumiID dashboard.
* Use `https://lumiid.com/widgets/sdk/verify/?widget_id=WIDGET_ID`.
* Enable camera permissions in the host application.

### Get your Widget ID

Create a widget in the LumiID dashboard before integrating. Its configuration controls branding, supported countries, document types, and verification steps.

1. Open **My SDKs** → **Create Widget**.
2. Configure your brand, countries, documents, and verification steps.
3. Save the widget and copy its `widget_id`.
4. Replace `WIDGET_ID` in the hosted URL.

### Embed on the web

Embed the hosted widget in an iframe. The widget requests camera access when required.

```html
<iframe
  src="https://lumiid.com/widgets/sdk/verify/?widget_id=WIDGET_ID"
  title="LumiID identity verification"
  allow="camera; microphone"
  allowfullscreen
></iframe>
```

### JavaScript SDK integration

Use this option only when your application already loads the LumiID JavaScript SDK. Otherwise, use the hosted iframe above.

Create one LumiID instance on page load. Call `.open()` when verification should begin.

```javascript
<button id="verify-btn">Verify Identity</button>

<script>
const verifier = new LumiID({
  widget_id: 'wgt_3f8a2c1d-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
  reference: 'user_123',  // your own user ID — comes back in callbacks

  onSuccess: (data) => {
    // Verification passed ✓
    console.log('Verified!', data.sessionId, 'Score:', data.score);
    // Redirect, unlock a feature, mark the user verified, etc.
    window.location.href = '/dashboard';
  },

  onError: (data) => {
    // Verification failed — the modal stays open so the user can retry
    console.warn('Failed:', data.code, data.message);
  },

  onClose: () => {
    // User dismissed the modal without completing
    console.log('Closed');
  },
});

document.getElementById('verify-btn').onclick = () => verifier.open();
</script>
```

On success, the modal closes automatically and `onSuccess` fires. On failure, the modal stays open for another attempt. Call `verifier.close()` in `onError` to close it.

### JavaScript SDK options

| Option     | Type     | Required | Description                                                                                                                                               |
| ---------- | -------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| widget\_id | string   | required | Your widget UUID from the dashboard.                                                                                                                      |
| reference  | string   | optional | Your own user ID or reference string. Echoed back in all callbacks so you can tie a result to a user without managing a separate mapping.                 |
| onSuccess  | function | optional | Called when verification completes successfully. The modal closes automatically before this fires. Receives a result payload.                             |
| onError    | function | optional | Called when verification fails. The modal stays open by default. Receives an error payload with a code and message.                                       |
| onClose    | function | optional | Called when the user dismisses the modal without completing (clicks outside, presses Escape, or taps the close button).                                   |
| metadata   | object   | optional | Arbitrary key-value pairs forwarded unchanged to all callbacks and webhook payloads. Useful for passing plan type, source page, experiment variants, etc. |

### JavaScript SDK methods

| Method              | What it does                                                                                                                                             |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| verifier.open()     | Opens the verification modal. Calling it again while the modal is already open is a safe no-op. Returns this so you can chain.                           |
| verifier.close()    | Programmatically closes the modal and fires onClose. Call this from inside onError if you want to close after a failed attempt.                          |
| verifier.destroy()  | Removes all DOM elements and event listeners permanently. Use this when unmounting a component. The instance cannot be reused after this call.           |
| LumiID.prefetch(id) | Static helper. Pre-warms the widget config in the background so the first open() feels instant. Call it after your page has loaded — pass the widget ID. |

### JavaScript SDK patterns

#### Pre-warming on page load

```
// Call after page load — pre-fetches config so open() is instant
window.addEventListener('load', () => {
  LumiID.prefetch('wgt_3f8a2c1d-xxxx-xxxx-xxxx-xxxxxxxxxxxx');
});
```

#### React hook

```cjs
import { useEffect, useRef } from 'react';

export function useLumiID({ widgetId, reference, onSuccess, onError, onClose }) {
  const verifierRef = useRef(null);

  useEffect(() => {
    if (!window.LumiID) return;
    verifierRef.current = new window.LumiID({ widgetId, reference, onSuccess, onError, onClose });
    return () => verifierRef.current?.destroy();
  }, [widgetId]);

  return { open: () => verifierRef.current?.open() };
}

// Usage in a component:
const { open } = useLumiID({
  widgetId:  'wgt_3f8a2c1d-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
  reference: user.id,
  onSuccess: (data) => markVerified(data.sessionId),
  onError:   (data) => toast.error(data.message),
});

return <button onClick={open}>Verify</button>;
```

#### Close modal after a failed attempt

```js
const verifier = new LumiID({
  widget_id: 'wgt_xxx',
  onError: (data) => {
    // Log the failure, then close
    analytics.track('verification_failed', { code: data.code });
    verifier.close();
  },
});
```

#### Passing metadata

```javascript
new LumiID({
  widget_id: 'wgt_xxx',
  reference: user.id,
  metadata:  { plan: 'pro', source: 'onboarding', ab_variant: 'B' },
  onSuccess: (data) => console.log(data.metadata.plan), // → 'pro'
});
```

### Hosted URL

Load the hosted URL inside a mobile WebView. The page handles camera access, liveness, and verification results.

```
https://lumiid.com/widgets/sdk/verify/?widget_id=WIDGET_ID
```

Your WebView must grant camera permissions. Liveness checks require live camera access.

The widget sends events through platform bridges. Implement the bridge for your platform.

### iOS — Swift / WKWebView

Swift 5 · WKWebView

```swift
import WebKit

class VerificationViewController: UIViewController, WKScriptMessageHandler {

    var webView: WKWebView!

    override func viewDidLoad() {
        super.viewDidLoad()

        let config = WKWebViewConfiguration()
        config.allowsInlineMediaPlayback = true                 // required for camera
        config.mediaTypesRequiringUserActionForPlayback = []     // no tap-to-play

        // Register the bridge — the SDK sends to window.webkit.messageHandlers.lumiid
        config.userContentController.add(self, name: "lumiid")

        webView = WKWebView(frame: view.bounds, configuration: config)
        webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        view.addSubview(webView)

        let url = URL(string: "https://lumiid.com/widgets/sdk/verify/?widget_id=wgt_xxx")!
        webView.load(URLRequest(url: url))
    }

    // Called every time the SDK fires an event
    func userContentController(
        _ controller: WKUserContentController,
        didReceive message: WKScriptMessage
    ) {
        guard message.name == "lumiid",
              let body = message.body as? [String: Any],
              let event = body["event"] as? String
        else { return }

        switch event {

        case "verification_complete":
            let status = body["status"] as? String
            if status == "success" {
                let sessionId = body["sessionId"] as? String ?? ""
                handleSuccess(sessionId: sessionId)
            } else {
                handleFailure()
            }

        case "sdk_ready":
            print("LumiID ready. Session:", body["sessionId"] ?? "")

        default:
            break
        }
    }

    func handleSuccess(sessionId: String) {
        // Dismiss WebView, mark user verified, navigate, etc.
        dismiss(animated: true)
    }

    func handleFailure() {
        // Optionally dismiss or let the user retry inside the WebView
    }
}
```

Add `NSCameraUsageDescription` to **Info.plist**. Without it, iOS denies camera access.

### Android — Kotlin / WebView

Kotlin · WebView

```kotlin
import android.webkit.*
import org.json.JSONObject

class VerificationActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val webView = WebView(this)
        setContentView(webView)

        webView.settings.apply {
            javaScriptEnabled = true
            mediaPlaybackRequiresUserGesture = false   // required for camera
            allowFileAccess = true
        }

        // Inject the JS interface — SDK calls window.LumiIDAndroid.onEvent(json)
        webView.addJavascriptInterface(LumiIDBridge(), "LumiIDAndroid")

        // Handle camera permission prompt
        webView.webChromeClient = object : WebChromeClient() {
            override fun onPermissionRequest(request: PermissionRequest) {
                request.grant(request.resources)
            }
        }

        webView.loadUrl("https://lumiid.com/widgets/sdk/verify/?widget_id=wgt_xxx")
    }

    inner class LumiIDBridge {
        @JavascriptInterface
        fun onEvent(jsonString: String) {
            val json = JSONObject(jsonString)
            when (json.optString("event")) {
                "verification_complete" -> {
                    val status = json.optString("status")
                    val sessionId = json.optString("sessionId")
                    runOnUiThread {
                        if (status == "success") handleSuccess(sessionId)
                        else handleFailure()
                    }
                }
                "sdk_ready" -> {
                    // SDK initialised, session created
                }
            }
        }
    }

    fun handleSuccess(sessionId: String) {
        // Navigate away, mark user verified, etc.
    }

    fun handleFailure() {
        // Optionally finish() or let the user retry inside the WebView
    }
}
```

Add `CAMERA` to **AndroidManifest.xml**:

```xml
<uses-permission android:name="android.permission.CAMERA" />
```

On Android 6 and later, request this permission at runtime before loading the WebView.

### React Native

react-native-webview

#### Install the package

```bash
npm install react-native-webview
# iOS: cd ios && pod install
```

#### Component

```javascript
import React from 'react';
import { StyleSheet } from 'react-native';
import { WebView } from 'react-native-webview';

export default function VerificationScreen({ widgetId, onComplete }) {

  const handleMessage = (event) => {
    try {
      const msg = JSON.parse(event.nativeEvent.data);

      if (msg.event === 'verification_complete') {
        const passed = msg.status === 'success';
        onComplete(passed, msg.sessionId);
      }
    } catch (e) { /* ignore non-JSON messages */ }
  };

  return (
    <WebView
      style={styles.webview}
      source={{
        uri: `https://lumiid.com/widgets/sdk/verify/?widget_id=${widgetId}`,
      }}
      onMessage={handleMessage}
      mediaCapturePermissionGrantType="grant"
      allowsInlineMediaPlayback
      mediaPlaybackRequiresUserAction={false}
      javaScriptEnabled
      originWhitelist={['*']}
    />
  );
}

const styles = StyleSheet.create({
  webview: { flex: 1 },
});
```

#### Usage in a screen

```javascript
import VerificationScreen from './VerificationScreen';

function OnboardingFlow({ navigation }) {
  return (
    <VerificationScreen
      widgetId="wgt_3f8a2c1d-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
      onComplete={(passed, sessionId) => {
        if (passed) navigation.replace('Home');
        else navigation.goBack();
      }}
    />
  );
}
```

### Flutter

`webview_flutter` 4.0 or later

#### Add the dependency

```dart
# pubspec.yaml
dependencies:
  webview_flutter: ^4.4.0
```

#### Widget

```dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class VerificationPage extends StatefulWidget {
  final String widgetId;
  final Function(bool passed, String sessionId) onComplete;

  const VerificationPage({
    required this.widgetId,
    required this.onComplete,
    super.key,
  });

  @override
  State<VerificationPage> createState() => _VerificationPageState();
}

class _VerificationPageState extends State<VerificationPage> {
  late WebViewController _controller;

  @override
  void initState() {
    super.initState();

    _controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..addJavaScriptChannel(
        'LumiIDFlutter',          // window.LumiIDFlutter.postMessage(...)
        onMessageReceived: (msg) {
          try {
            final data = jsonDecode(msg.message) as Map<String, dynamic>;
            if (data['event'] == 'verification_complete') {
              final passed = data['status'] == 'success';
              final sessionId = data['sessionId'] ?? '';
              widget.onComplete(passed, sessionId);
            }
          } catch (_) {}
        },
      )
      ..loadRequest(Uri.parse(
        'https://lumiid.com/widgets/sdk/verify/?widget_id=${widget.widgetId}',
      ));
  }

  @override
  Widget build(BuildContext context) => WebViewWidget(controller: _controller);
}
```

On iOS, add `NSCameraUsageDescription` to **Info.plist**. On Android, add `CAMERA` to **AndroidManifest.xml**.

### All events

The widget sends events through supported platform bridges. Every event includes `event` and `ts`, a Unix timestamp in milliseconds.

| Event                   | When it fires                                 | Key fields                                          |
| ----------------------- | --------------------------------------------- | --------------------------------------------------- |
| `sdk_ready`             | Widget configuration and session are ready.   | `sessionId`                                         |
| `screen_change`         | The user moves through the verification flow. | `screenId`                                          |
| `country_selected`      | The user selects a country.                   | `country`, `countryName`                            |
| `idtype_selected`       | The user selects a document type.             | `idType`                                            |
| `verification_result`   | A verification result is available.           | `status`, `score`, `sessionId`, `country`, `idType` |
| `verification_complete` | The user completes the result-screen action.  | `status`, `sessionId`                               |
| `retry`                 | The user restarts the complete flow.          | —                                                   |
| `retry_liveness`        | The user retries only the face scan.          | —                                                   |

Use `verification_complete` as the primary signal to close a WebView or redirect a user.

### JavaScript callback payloads

#### `onSuccess` payload

```json
{
  "event":      "success",
  "sessionId":  "ses_9b3c1d2e-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "widgetId":   "wgt_3f8a2c1d-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "reference":  "user_123",        // your reference, echoed back
  "score":      91,                 // confidence score 0–100
  "country":    "NG",
  "idType":     "national_id",
  "metadata":   {},                 // whatever you passed in
  "ts":         1722505465000
}
```

#### `onError` payload

```json
{
  "event":      "error",
  "sessionId":  "ses_9b3c1d2e-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "widgetId":   "wgt_3f8a2c1d-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "reference":  "user_123",
  "code":       "SPOOFING_DETECTED",    // machine-readable reason
  "message":    "Spoof guard triggered.", // human-readable message
  "metadata":   {},
  "ts":         1722505465000
}
```

#### `onClose` payload

```json
{
  "event":     "close",
  "sessionId": "ses_9b3c1d2e-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "widgetId":  "wgt_3f8a2c1d-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "reference": "user_123",
  "ts":        1722505430000
}
```

### Camera permissions

Liveness checks require live camera access. Grant camera access before loading the hosted widget.

#### Web

```html
<iframe
  src="https://lumiid.com/widgets/sdk/verify/?widget_id=WIDGET_ID"
  allow="camera; microphone"
  allowfullscreen
></iframe>
```

#### iOS and Android

Add the required camera usage declaration to each application manifest. Request runtime permission before loading the WebView.

### Quick reference

| Item              | Value                                                        |
| ----------------- | ------------------------------------------------------------ |
| Hosted widget URL | `https://lumiid.com/widgets/sdk/verify/?widget_id=WIDGET_ID` |
| iOS bridge        | `window.webkit.messageHandlers.lumiid`                       |
| Android bridge    | `window.LumiIDAndroid.onEvent(json)`                         |
| Completion event  | `verification_complete`                                      |
| Success status    | `success`                                                    |
| Failure status    | `failed`                                                     |


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.lumiid.com/api-integration/sdks/lumiid-hosted-widget-integration-guide.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
