v1.0.0

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.

🌐
Web (JS)
One script tag
🍎
iOS
WKWebView / Swift
🤖
Android
WebView / Kotlin
⚛️
React Native
react-native-webview
💙
Flutter
webview_flutter
No backend code needed to embed the widget. Your Widget ID is the only thing required — it's a public identifier, safe to use in frontend code.

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.

1

Open the dashboard

Log in and go to My SDKs+ Create Widget.

2

Configure your widget

Set your brand name, colour, accepted countries, document types, and whether to include liveness or data lookup.

3

Copy your Widget ID

After saving, copy the widget_id shown in the output panel. It looks like wgt_3f8a2c1d-xxxx-xxxx-xxxx-xxxxxxxxxxxx.

4

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>
On a successful verification the modal closes automatically and onSuccess fires. On failure the modal stays open so the user can retry — call verifier.close() inside onError if you prefer to close it yourself.

All options

OptionTypeDescription
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

MethodWhat 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
Your WebView must grant camera permissions. The liveness check requires live camera access — see the Camera permissions section.

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 · WKWebView
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/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
    }
}
Add NSCameraUsageDescription to your Info.plist — otherwise the OS will silently deny camera access and liveness will fail to initialise.

Android — Kotlin / WebView

Kotlin · WebView
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/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
    }
}
Add CAMERA to your AndroidManifest.xml: <uses-permission android:name="android.permission.CAMERA"/>. On Android 6+ also request it at runtime before loading the WebView.

React Native

react-native-webview

Install 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.0

Add 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);
}
On iOS add NSCameraUsageDescription to Info.plist. On Android add CAMERA to AndroidManifest.xml. See the camera permissions section for full instructions.

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.

sdk_ready First event

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.

screen_change

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.

country_selected

Fires when the user picks a country. Contains country (ISO code, e.g. "NG") and countryName.

idtype_selected

Fires when the user picks a document type. Contains idType (e.g. "national_id").

verification_result Result ready

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.

verification_complete User action

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 / retry_liveness

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.

Web
iOS
Android
React Native
Flutter
// 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

Hosted widget URL
https://lumiid.com/vflow/verify/?widget_id=YOUR_ID
JS SDK script tag
https://lumiid.com/static/js/lumiid.js
iOS bridge name
window.webkit.messageHandlers.lumiid
Android bridge name
window.LumiIDAndroid.onEvent(json)
Key success event
verification_complete (status: "success")
Key failure event
verification_complete (status: "failed")

LumiID Integration Guide · v1.0.0 · Questions? support@lumiid.io