Integration
Modal, Embed & Hosted flow
Integration

Verification UI & SDK Integration

Choose how users verify: an in-page modal dialog on your site, an embedded widget on your own custom page, a centered popup window, or a full-page hosted redirect.

Integration approaches#

Every verification session created on your server returns a url (for example, https://lkp-three.vercel.app/verify/vs_3f9c...). You can present this verification flow to your users in four ways:

Option 1: Recommended
In-Page Modal Dialog

A sleek modal overlay directly over your website. Zero popup blockers, smooth backdrop blur, and mobile responsive bottom sheet.

Option 2: Deep Customization
Custom Page & Embedded Widget

Host the verification on your own domain (e.g. acme.com/verify or checkout). Mounts inside any container element on your page.

Option 3: Desktop Favorite
Popup Window

Opens in a centered desktop browser popup window and reports completion back to your opener page via postMessage.

Option 4: Zero Frontend Code
Hosted Page Redirect

Redirect the browser to session.url. LKP handles everything and redirects the user back to your redirect URL when finished.

What the user sees#

  • Your app name and logo (configured in the console).
  • Your verification requirements in plain language (e.g. "18 years or older") and an explicit summary of what is kept private.
  • Returning users: A single biometric passkey prompt (Touch ID, Face ID, Windows Hello) unlocks their vault, runs the Groth16 zk-SNARK circuit in WebAssembly, and submits the proof in ~40ms.
  • First-time users: A simple onboarding prompt to create a passkey vault. If they lack the required credential:
    • When kycWorkflow: "age_estimation": The user takes a 1 to 2 second passive selfie (no identity document required).
    • When kycWorkflow: "full_id": The user scans a government ID document (passport, national ID, or driver's license).
    • When kycWorkflow: "either" (default): The user chooses between the quick selfie check or government ID scan.
  • After issuance, the credential is saved in the user's encrypted vault, the proof verifies, and the user returns to your application.

Option 1: In-page modal overlay#

Include the lightweight SDK script from LKP and call LKP.verify(session.url, { mode: "modal" }) or LKP.modal(session.url). The modal renders over your existing page without leaving your website or opening a separate browser window.

<!-- 1. Include the LKP browser SDK -->
<script src="https://lkp-three.vercel.app/sdk/lkp.js"></script>

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

<script>
  document.getElementById("verify-btn").addEventListener("click", async () => {
    // 2. Fetch the session URL created by your backend
    const session = await fetch("/api/verify/start", { method: "POST" }).then((r) => r.json());

    // 3. Open in-page modal dialog
    const { sessionId, status } = await LKP.verify(session.url, { mode: "modal" });
    
    // status is "verified", "failed", or "cancelled"
    if (status === "verified") {
      window.location.href = "/checkout/success?session_id=" + sessionId;
    }
  });
</script>

The modal includes an accessible close button, ESC key handler, click-backdrop-to-close behavior, and smooth mobile bottom-sheet styling.

Option 2: Custom page on your own website#

If you want users to remain completely within your own domain (e.g. https://acme.com/verify or inside a multi-step onboarding wizard), you can mount the verification widget directly inside any element on your page.

Vanilla HTML & JavaScript

<!-- Your custom page container -->
<div class="my-custom-wrapper">
  <h2>Verify your eligibility</h2>
  <p>Decrypted locally in your browser with zero personal data shared.</p>
  
  <!-- Mount target -->
  <div id="lkp-embed-box" style="max-width: 480px; margin: 24px auto;"></div>
</div>

<script src="https://lkp-three.vercel.app/sdk/lkp.js"></script>
<script>
  async function initCustomPage() {
    const session = await fetch("/api/verify/start", { method: "POST" }).then((r) => r.json());

    // Mounts inline and automatically handles iframe sizing and completion
    const controller = LKP.mount("#lkp-embed-box", session.url, {
      autoResize: true,
      onComplete: ({ sessionId, status }) => {
        if (status === "verified") {
          window.location.href = "/dashboard?verified=" + sessionId;
        }
      },
    });
  }

  initCustomPage();
</script>

React / Next.js Component

import { useEffect, useRef } from "react";

export function CustomVerificationPage({ sessionUrl }: { sessionUrl: string }) {
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!containerRef.current || !window.LKP) return;

    const controller = window.LKP.mount(containerRef.current, sessionUrl, {
      autoResize: true,
      onComplete: ({ sessionId, status }) => {
        if (status === "verified") {
          window.location.href = `/verified?session_id=${sessionId}`;
        }
      },
    });

    return () => controller.unmount();
  }, [sessionUrl]);

  return (
    <div className="mx-auto max-w-xl py-12 px-4">
      <h1 className="text-2xl font-bold">Age Verification</h1>
      <div ref={containerRef} className="mt-6 min-h-[540px] w-full" />
    </div>
  );
}

Declarative Web Component

You can also use the drop-in custom element:

<script src="https://lkp-three.vercel.app/sdk/lkp.js"></script>

<lkp-verify
  session-url="https://lkpidentity.com/verify/vs_3f9c..."
  mode="inline"
  auto-resize="true"
></lkp-verify>

<script>
  document.querySelector("lkp-verify").addEventListener("lkp-complete", (e) => {
    const { sessionId, status } = e.detail;
    console.log("Verified:", sessionId, status);
  });
</script>

Option 3: Popup window#

To open verification in a centered desktop browser popup window:

button.addEventListener("click", async () => {
  const session = await fetch("/api/verify/start", { method: "POST" }).then((r) => r.json());
  const { sessionId, status } = await LKP.verify(session.url, { mode: "popup" });
  if (status === "verified") {
    // Confirm on your server with your secret key before granting access
    location.href = "/verified?session=" + sessionId;
  }
});

If the user has an aggressive popup blocker installed that intercepts the window, the SDK automatically falls back to a clean full-page redirect.

Option 4: Full-page redirect#

For maximum simplicity without loading any frontend JavaScript: add your return destination to your app's Redirect URLs in the console, then create the session with a matching redirectUrl:

const session = await lkp("/v1/verification-sessions", {
  requirements: [{ kind: "AGE_OVER", minAge: 21 }],
  redirectUrl: "https://shop.example/checkout/verified",
});

// Redirect the user
res.redirect(303, session.url);

When the session finishes, LKP sends the user back to your URL with two query parameters added:

https://shop.example/checkout/verified?lkp_session=vs_3f9c...&lkp_status=verified
Treat query parameters as untrusted
Anyone can type ?lkp_status=verified into an address bar. Use lkp_session only to know which session to check, verify that it belongs to this user, and fetch the verified confirmation from your server using your secret key.

SDK API reference#

LKP.verify(session, options?)

sessionstring | { url: string }required
The session object or session url string returned by your server.
options.mode"modal" | "popup" | "redirect"optional
Defaults to "modal". Use "popup" for a popup window, or "redirect" for full-page navigation.
options.onComplete(result) => voidoptional
Callback invoked when verification finishes with { sessionId, status }.
options.onCancel() => voidoptional
Callback invoked if the user closes or dismisses the modal or popup.
options.zIndexnumberoptional
Custom z-index for the modal overlay (defaults to 999999).

LKP.mount(target, session, options?)

targetHTMLElement | stringrequired
DOM element or CSS selector string (e.g. "#verify-container").
sessionstring | { url: string }required
The session URL string or object.
options.autoResizebooleanoptional
Automatically adjusts iframe height to match content height (defaults to true).
options.onComplete(result) => voidoptional
Callback invoked upon completion.
options.onResize(height) => voidoptional
Callback invoked when verification content height changes.
options.styleRecord<string, string>optional
Custom CSS style overrides applied to the embedded iframe.

Mobile and in-app browsers#

WebAuthn passkeys (Face ID, Touch ID, Windows Hello) are supported on all modern platforms (iOS, Android, macOS, Windows, Linux). Certain in-app webviews (such as the embedded browsers inside Instagram, TikTok, WhatsApp, or Telegram) restrict access to hardware passkey APIs.

LKP automatically detects embedded webviews and displays instructions guiding the user to open the verification link in their default browser (Chrome or Safari). For mobile apps, we recommend opening the session URL via Android Custom Tabs or Safari View Controller.

Session lifetime#

  • Sessions expire 30 minutes after creation. Expired sessions show as "Abandoned" in the console.
  • If a proof fails, the user can retry on the same session until it expires.
  • A completed session cannot be reused. Create a new session for each distinct check.