LumiID Integration Guide
Everything you need to embed LumiID into your product — web modal, mobile WebView, or any hybrid app. No backend code required on your side.
Get your Widget ID
Before integrating, create a widget in the LumiID dashboard. This configures your branding, which document types to accept, and which verification steps to run.
Open the dashboard
Log in and go to My SDKs → + Create Widget.
Configure your widget
Set your brand name, colour, accepted countries, document types, and whether to include liveness or data lookup.
Copy your Widget ID
After saving, copy the widget_id shown in the output panel. It looks like wgt_3f8a2c1d-xxxx-xxxx-xxxx-xxxxxxxxxxxx.
Paste it into your integration
Use it in the JS SDK or hosted URL examples below. That's it.
Installation
Add one script tag to your page. That's the entire installation.
<!-- Add before </body> or in <head> with defer -->
<script src="https://lumiid.com/static/js/lumiid.js" defer></script>
The SDK is served with a 1-hour browser cache and stale-while-revalidate, so it loads instantly on repeat visits. It is safe to load cross-origin.
Basic usage
Create a LumiID instance once on page load and call .open() whenever you need to verify the user — on a button click, after signup, on a protected route, etc.
<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>
All options
| Option | Type | 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. |
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. |
Advanced 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
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
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
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
For mobile apps, load the hosted verification URL inside a WebView. The page handles the full flow — camera access, liveness, result — and sends events back to your app via the native bridge.
https://lumiid.com/vflow/verify/?widget_id=YOUR_WIDGET_ID
# Optionally pass a session_id you created server-side:
# https://lumiid.com/vflow/verify/?widget_id=wgt_xxx&session_id=ses_yyy
The page communicates with your native code through four bridges simultaneously — iOS message handlers, the Android JS interface, React Native's bridge, and standard postMessage. You only need to implement the one for your platform.
iOS — Swift / WKWebView
Swift 5 · WKWebViewimport 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/vflow/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
}
}
Android — Kotlin / WebView
Kotlin · WebViewimport 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/vflow/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
}
}
React Native
react-native-webviewInstall the package
npm install react-native-webview
# iOS: cd ios && pod install
Component
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}
onMessage={handleMessage}
// Camera
mediaCapturePermissionGrantType="grant"
allowsInlineMediaPlayback
mediaPlaybackRequiresUserAction={false}
// General
javaScriptEnabled
originWhitelist={['*']}
/>
);
}
const styles = StyleSheet.create({
webview: { flex: 1 },
});
Usage in a screen
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.0Add the dependency
# pubspec.yaml
dependencies:
webview_flutter: ^4.4.0
Widget
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/vflow/verify/?widget_id=${widget.widgetId}',
));
}
@override
Widget build(BuildContext context) => WebViewWidget(controller: _controller);
}
All events
These events are fired across all channels — web postMessage, iOS window.webkit.messageHandlers.lumiid, Android LumiIDAndroid.onEvent(), and the React Native bridge. Each event object always includes an event name and a ts Unix timestamp in milliseconds.
Fires when the widget has loaded, the config has been fetched, and a session has been created. The sessionId here is the value to save if you want to poll the result server-side later.
Fires every time the user moves to a new screen inside the flow. Useful for funnel analytics. The screenId values are: s-welcome, s-country, s-idtype, s-idinput, s-liveness, s-processing, s-result.
Fires when the user picks a country. Contains country (ISO code, e.g. "NG") and countryName.
Fires when the user picks a document type. Contains idType (e.g. "national_id").
Fires when the verification result is determined — before the user taps the CTA on the result screen. Contains status ("success" or "failed"), score, sessionId, country, and idType.
Fires after verification_result, when the user taps the final CTA ("Continue" on pass, "Exit" on fail). This is the primary signal to close the WebView or redirect the user in native apps.
retry fires when the user restarts the entire flow from the result screen. retry_liveness fires when they retry only the face scan step.
Payload shapes
onSuccess payload
{
"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
{
"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
{
"event": "close",
"sessionId": "ses_9b3c1d2e-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"widgetId": "wgt_3f8a2c1d-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"reference": "user_123",
"ts": 1722505430000
}
Camera permissions
The liveness check requires live camera access. Here's how to enable it on each platform.
// The browser handles the permission prompt automatically.
// The iframe must have the allow attribute set:
<iframe
src="https://lumiid.com/vflow/verify/?widget_id=wgt_xxx"
allow="camera; microphone"
allowfullscreen
></iframe>
// The JS SDK (LumiID) handles this automatically — no iframe needed.<!-- Info.plist — add this key -->
<key>NSCameraUsageDescription</key>
<string>Camera access is needed to perform your identity liveness check.</string>
// In your WKWebViewConfiguration:
config.allowsInlineMediaPlayback = true
config.mediaTypesRequiringUserActionForPlayback = []<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.CAMERA" />
// Grant in WebChromeClient:
webView.webChromeClient = object : WebChromeClient() {
override fun onPermissionRequest(request: PermissionRequest) {
request.grant(request.resources)
}
}
// Also request at runtime (Android 6+):
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.CAMERA),
CAMERA_REQUEST_CODE
)// react-native-webview handles camera prompts via these props:
<WebView
mediaCapturePermissionGrantType="grant"
allowsInlineMediaPlayback
mediaPlaybackRequiresUserAction={false}
/>
// iOS Info.plist — add NSCameraUsageDescription
// Android AndroidManifest.xml — add CAMERA permission# iOS — ios/Runner/Info.plist
<key>NSCameraUsageDescription</key>
<string>Needed for identity liveness check.</string>
# Android — android/app/src/main/AndroidManifest.xml
<uses-permission android:name="android.permission.CAMERA" />
// Set JS mode in WebViewController:
_controller.setJavaScriptMode(JavaScriptMode.unrestricted);Quick reference
LumiID Integration Guide · v1.0.0 · Questions? support@lumiid.io