Concepts

Web SDK

The VerifyMe Web SDK opens the hosted, tenant-branded flow inside your page as a modal, an inline frame, a popup or a full-page redirect, and reports progress through events. One script tag, no dependencies, no build step.

Installation

<script src="https://kyc.example.com/sdk/v1/verifyme.js"></script>

The script defines the global window.VerifyMe (it is also safe to require() in a CommonJS bundler). It is served with Access-Control-Allow-Origin: * and Cross-Origin-Resource-Policy: cross-origin, so it works with crossorigin and strict cross-origin isolation setups. It uses ES5 syntax and runs in all evergreen browsers.

The SDK is a UX layer, not a trust boundary

Your server creates the request with a secret key and passes onboardingUrl to the page. The SDK callbacks tell your UI what the applicant is doing; they can be forged by anyone who controls the browser. Decide outcomes only from webhooks and GET /v1/onboarding-requests/{id}/result.

Quick start

VerifyMe.init({
  publicKey: "vm_pk_test_xxxxxxxxxxxxxxxxxx",
  onComplete: (r) => console.log("submitted", r.requestId, r.status),
  onClose: (c) => console.log("closed", c.reason),
  onError: (e) => console.warn(e.code, e.message),
});

document.getElementById("verify").addEventListener("click", () => {
  VerifyMe.open({ url: onboardingUrlFromYourServer, mode: "modal" });
});

init(options)

VerifyMe.init(options) configures the SDK and returns VerifyMe. It is idempotent: calling it again merges the new options into the old ones.

OptionTypeDescription
publicKeystringYour workspace publishable key (vm_pk_...). The hosted UI refuses links that do not belong to this workspace
hoststringVerifyMe origin. Defaults to the origin the SDK script was loaded from. Set it when your onboardingUrl is on a different origin, such as a custom domain
onEventfunctionReceives every event as { type, data }
onCompletefunctionApplicant submitted: { requestId, status, externalReference }
onClosefunctionThe dialog or popup closed: { reason }
onErrorfunctionSDK or hosted-UI error: { code, message }
themeobject{ overlayColor?, radius? } for the modal backdrop and panel. The form itself is branded by your tenant branding settings
zIndexnumberStacking order of the modal. Default 2147483000
localestringHint for the hosted UI language (English only today)
fallbackbooleanDefault true. When framing is blocked, fall back to popup and then redirect

open(options)

VerifyMe.open(options) starts the flow and returns true if it was started. Failures are never thrown; they arrive as error events.

OptionDescription
urlThe onboardingUrl from POST /v1/onboarding-requests. Its origin must equal the SDK host, otherwise ORIGIN_MISMATCH. Must be an onboarding link (/start/...)
tokenAlternative to url: the raw vm_t_... token
modemodal (default), inline, popup, redirect
containerElement or CSS selector. Required for inline. Give it a height of at least 640 px
returnUrlOptional URL on your site to navigate to after the applicant finishes (modal or popup). requestId, status and externalReference are appended as hints. For redirect mode set returnUrl when creating the request

Modes

ModeWhat happensUse when
modalAccessible full-screen dialog (on wide screens a centred panel) containing the hosted flow in an iframeDefault for web apps
inlineThe same iframe inside your containerThe flow is a page section of its own
popupwindow.open with the hosted flow. Must be called from a click handlerFraming is not possible on your page
redirectlocation.assign(url). The applicant returns to the returnUrl set on the requestSimplest, most robust; mobile browsers

If the hosted page does not report ready within 8 seconds (typically because framing is blocked for your origin), the SDK emits error FRAME_TIMEOUT, then fallback { to: "popup" } and, if the popup is blocked, fallback { to: "redirect" }, so the applicant is never left on a blank frame. Set fallback: false to handle this yourself.

Other methods

MethodDescription
VerifyMe.close()Closes the dialog or popup. Fires onClose({ reason: "api" })
VerifyMe.destroy()Closes everything, removes listeners and resets configuration. No callbacks fire. Call init again before reuse. Use in component unmount
VerifyMe.getState(){ version, initialized, open, mode, host, status, requestId, lastEvent } where status is idle, loading, ready, completed, closed or error
VerifyMe.on(type, fn) / VerifyMe.off(type, fn?)Event emitter. Use type "event" for all events. Omit fn in off to remove all handlers of a type
VerifyMe.versionThe SDK version string, "1.0.0"

Events

Events arrive through onEvent, the specific callbacks (onComplete, onClose, onError) and VerifyMe.on(type, fn).

typedataWhen
ready{ requestId, status }Hosted UI loaded and the handshake completed
step{ stepKey, stepType, status: "completed", completed, total }The applicant completed a step
submitted{ requestId, status: "SUBMITTED", externalReference }The applicant submitted. Also calls onComplete and emits the alias complete
closed{ reason }The dialog or popup was closed. Reasons include user, escape, completed, terminal, api, popup-closed
error{ code, message }An SDK or hosted-UI problem
fallback{ to } (popup or redirect)The SDK switched to a different mode after a frame timeout
redirect{ url }The SDK is about to navigate (token redacted in url)
popup-detached{ message }The popup's state cannot be observed (cross-origin isolation). Rely on webhooks
{ "type": "step", "data": { "stepKey": "pan", "stepType": "PanVerification", "status": "completed", "completed": 3, "total": 6 } }

Error codes

CodeMeaning
INVALID_HOSThost is not an http(s) origin
INVALID_MODEmode is not one of the four values
INVALID_URL / INVALID_TOKEN / MISSING_TARGETurl is not an onboarding link, the token is malformed, or neither was given
ORIGIN_MISMATCHThe url origin differs from the configured host
MISSING_CONTAINERinline mode without a container
POPUP_BLOCKEDThe browser blocked the popup. Open from a click handler
FRAME_TIMEOUTThe hosted page did not respond in 8 seconds. Framing may be blocked
LINK_INVALID, SESSION_EXPIRED, or a final request status such as EXPIRED, CANCELLED, REJECTED, APPROVEDReported by the hosted UI when the link or session cannot continue (for example the applicant reopened a finished request)
SDK_ERRORAn unexpected internal error, caught so it never reaches your app

postMessage protocol

You normally never touch this; the SDK does it. It is documented so you can audit it or embed the iframe yourself.

  1. The parent (SDK) sends { source: "verifyme-host", type: "init", origin: <your page origin> } to the hosted page, repeatedly (every 400 ms, up to 25 times) until it receives ready. The target origin is the VerifyMe origin.
  2. The hosted page checks that event.source is its parent (or its opener, for popups) and that event.origin equals the origin the message claims. Only then does it remember that origin and post only to it. Before the handshake it posts only to the document.referrer origin, if there is one.
  3. The hosted page sends { source: "verifyme", type, ...data } where type is ready, step, submitted, closed or error.
  4. The SDK accepts a message only if event.origin equals the configured host origin and event.source is its own iframe or popup window, and source === "verifyme".
// Minimal listener if you embed the iframe yourself
window.addEventListener("message", (e) => {
  if (e.origin !== "https://kyc.example.com") return;
  if (e.source !== iframe.contentWindow) return;
  const m = e.data;
  if (!m || m.source !== "verifyme") return;
  if (m.type === "submitted") showPending(m.requestId);
});
// ...and complete the handshake so the page knows where to send messages:
iframe.contentWindow.postMessage(
  { source: "verifyme-host", type: "init", origin: location.origin },
  "https://kyc.example.com"
);

Framing, CSP and permissions

ConcernRequirement
Allowed originsYour page origin must be on the portal's Allowed origins list. VerifyMe sets frame-ancestors to that list for ?embed=1 links; everyone else is refused. In production an empty list blocks framing. Resolution uses the host the link is served on, so use your tenant (custom) domain in production
Your CSPscript-src must allow the SDK origin and frame-src the VerifyMe origin. The SDK sets styles through the DOM API and needs no inline <style>
iframe attributes (set by the SDK)allow="camera; geolocation; microphone; clipboard-write", referrerpolicy="no-referrer", sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"
CookiesThe hosted UI does not depend on third-party cookies in embed mode: it uses a session header returned for embedded sessions
Referrerno-referrer. The onboarding URL token is stripped of unknown query parameters by the hosted page after load
Permissions policyYour own Permissions-Policy must not disable camera, geolocation or microphone for the VerifyMe origin
HTTPSCamera and location need a secure context. Use HTTPS in production
ModalPopupRedirect
Stays on your pageYesPartlyNo
Needs framing allowedYesNoNo
Events back to your pageYesOften not (COOP isolation)No. Use returnUrl and webhooks
Works in in-app browsersMixedOften blockedBest
Camera insideNeeds allow (SDK sets it)NativeNative

Accessibility

The modal is an accessible dialog: focus moves into it and is trapped, background content is made inert and page scroll is locked, Esc closes it (the hosted page forwards Esc pressed inside the frame as closed with reason escape), focus returns to the control that opened it, and safe-area insets are respected on notched phones. The SDK close button is hidden once the hosted UI shows its own. The frame has the title "Identity verification". Provide a visible, labelled button that opens the flow, and announce completion to assistive technology yourself in onComplete.

Versioning, SRI and migration

  • The script path is versioned: /sdk/v1/verifyme.js. Breaking changes will ship under /sdk/v2/; v1 only receives backwards compatible fixes. The file is cacheable for about five minutes.
  • Subresource Integrity. The v1 file can change with compatible fixes, so an SRI hash on it will eventually break. If your policy requires SRI, self-host a copy you have reviewed: openssl dgst -sha384 -binary verifyme.js | openssl base64 -A, then <script src="/static/verifyme.js" integrity="sha384-..." crossorigin="anonymous"></script>.
  • Migrating from a plain redirect: keep your server code and returnUrl. Add the script and replace res.redirect(onboardingUrl) with VerifyMe.open({ url, mode: "modal" }). Leave the webhook handling untouched.
  • Migrating from your own iframe: remove your postMessage code, delete the allow attributes you set and call VerifyMe.open. Add your origin to Allowed origins.

Complete example

Three files: a tiny Express server that creates the request, an HTML page and an external script (inline scripts would violate a strict CSP).

// npm i express    (Node 18+)
import express from "express";

const HOST = process.env.VERIFYME_HOST;        // https://kyc.example.com
const KEY = process.env.VERIFYME_API_KEY;      // vm_test_...  (secret, server only)

const app = express();
app.use(express.static("public"));

app.post("/api/kyc/start", async (req, res) => {
  const r = await fetch(`${HOST}/v1/onboarding-requests`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": `demo-${Date.now()}`,
    },
    body: JSON.stringify({ externalReference: `demo-${Date.now()}`, mobile: "9876541234" }),
  });
  if (!r.ok) return res.status(502).json({ error: "could_not_start" });
  const { requestId, onboardingUrl } = await r.json();
  res.json({ requestId, onboardingUrl });         // never send the API key to the browser
});

app.listen(3000, () => console.log("http://localhost:3000"));
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>Verify your identity</title>
</head>
<body>
  <main>
    <h1>Verify your identity</h1>
    <button id="verify" type="button">Start verification</button>
    <p id="status" role="status" aria-live="polite"></p>
  </main>

  <script src="https://kyc.example.com/sdk/v1/verifyme.js"></script>
  <script src="/kyc.js"></script>
</body>
</html>
const statusEl = document.getElementById("status");
const button = document.getElementById("verify");

VerifyMe.init({
  publicKey: "vm_pk_test_xxxxxxxxxxxxxxxxxx",
  onEvent: ({ type, data }) => console.debug("[verifyme]", type, data),
  onComplete: ({ requestId }) => {
    statusEl.textContent = "Thanks. We are verifying your details.";
    button.hidden = true;
    // Optional: tell your backend so it can start polling or wait for the webhook.
    fetch("/api/kyc/submitted", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ requestId }),
    });
  },
  onClose: ({ reason }) => {
    if (reason !== "completed") statusEl.textContent = "Verification was not finished.";
  },
  onError: ({ code, message }) => {
    statusEl.textContent = "Something went wrong. Please try again.";
    console.warn(code, message);
  },
});

button.addEventListener("click", async () => {
  button.disabled = true;
  statusEl.textContent = "";
  try {
    const r = await fetch("/api/kyc/start", { method: "POST" });
    if (!r.ok) throw new Error("start failed");
    const { onboardingUrl } = await r.json();
    VerifyMe.open({ url: onboardingUrl, mode: "modal" });
  } catch (e) {
    statusEl.textContent = "Could not start verification.";
  } finally {
    button.disabled = false;
  }
});

A runnable sandbox demo is available at /sdk-demo/ on your VerifyMe sandbox host: paste an onboardingUrl, try the four modes and watch the events live.

Docs version 1.0.0API version v1Last updated 1 Oct 2026