Jumio NetVerify React: 14-Step Web SDK Setup, 40 Min [2026]

Identity verification is the first thing a regulated iGaming player touches and the last thing an operator can afford to get wrong. In 2026 the threat model has moved on: Jumio reported a 700% year-over-year jump in injection attacks, where AI-generated video is piped straight into the verification stream to defeat liveness, and the Sumsub Identity Fraud Report 2025 measured a roughly 300% rise in deepfake injection across financial services between 2023 and 2025. Against that backdrop, bolting a verification iframe onto a React page is no longer enough.

This tutorial wires Jumio NetVerify – delivered today through the Jumio KYX Platform and the @jumio/websdk Web Component – into a modern React iGaming frontend built on Next.js 16 and React 19. We use the official <jumio-sdk> custom element (not the legacy iframe), mint short-lived SDK tokens server-side, drive the flow with the SDK’s workflow:* events, and – critically – treat the server callback as the only authoritative source of a verification decision. Budget about 40 minutes end to end on a sandbox account.

Google · Preferred Sources

Don't miss new tech stories on Google

Add Tech Insider once in the Google app and our stories appear in your news suggestions.

Add Now

Why the Web SDK Beats the Iframe for React KYC in 2026

For years the standard pattern was an iframe pointed at a Jumio-hosted URL. It works, but it is opaque: you cannot style it cleanly, you cannot react to lifecycle events without postMessage gymnastics, and camera permission prompts behave inconsistently across mobile browsers. The current @jumio/websdk package – version 0.5.0, published 24 June 2026 with zero runtime dependencies – ships the verification UI as a native custom element you mount directly in your component tree. It registers a <jumio-sdk> tag, exposes real DOM events, and lets you theme it with CSS variables and slots.

The shift from one-and-done to continuous identity

The other 2026 change is conceptual. On 30 April 2026 Jumio launched Jumio Watch, a continuous identity-monitoring product that, in the words of President and Chief Product & Technology Officer Bala Kumar, exists because “risk evolves, and so should your identity strategy.” Jumio says Watch surfaces up to 25% more risk after onboarding than point-in-time checks alone. The practical lesson for your React integration: onboarding KYC is a gate, not a guarantee, so build it to re-trigger.

What you will build

A Next.js 16 App Router project with a server-side OAuth2 token service, a session route that creates a Jumio workflow execution, a React 19 client component that mounts <jumio-sdk>, a webhook callback that fetches the authoritative decision, and a Content-Security-Policy that allows the camera without opening the door to everything. Every player-facing state change is confirmed against Jumio’s Retrieval API before a deposit or wager is ever unlocked.

2026 fraud / onboarding metric Figure Source
Injection attacks, year over year +700% Jumio (2026)
Deepfake injection, financial services 2023–2025 ~+300% Sumsub Identity Fraud Report 2025
Extra risk detected post-onboarding up to 25% Jumio Watch (Apr 2026)
Operator onboarding time (888.com, reported) 72h → under 2 min Jumio

Prerequisites and Exact Versions

Pin these versions. The custom-element ergonomics described here depend on React 19’s first-class handling of unknown elements and on Next.js 16’s App Router, and the SDK token flow depends on the KYX (not the legacy NetVerify v4) endpoints.

Component Version (June 2026) Why it matters
Node.js 22.x LTS Native fetch, stable for Next.js 16 builds
Next.js 16.2.7 App Router, Route Handlers, Turbopack default
React 19.2 Custom elements pass attributes/properties cleanly
@jumio/websdk 0.5.0 Zero-dependency <jumio-sdk> element
TypeScript 5.7+ Typing the custom element and decisions
Jumio KYX account Sandbox + Production Client ID/secret, workflow keys, callback allow-list
HTTPS tunnel ngrok / Cloudflare Tunnel Camera APIs and callbacks require HTTPS

Jumio Customer Portal credentials

In the Jumio Customer Portal under Settings → API credentials, generate an OAuth2 client ID and client secret. The token your backend uses must carry the Initialize permission (to create accounts and workflow executions) and the Retrieve & Delete permission (to read decisions and honor data-deletion requests). Note your provisioned workflow definition keys: the standard iGaming KYC workflow is key 10013 (ID document + Identity/selfie with liveness). Doc Proof, Authentication, and Standalone Risk Signal workflows are provisioned separately and appear in your Portal. Keep the official Jumio Web SDK documentation open alongside this guide – the keys and credential categories enabled on your contract are account-specific.

Datacenter and regional routing

Jumio runs three regional datacenters. The Web SDK’s dc attribute uses short codes, while the REST API uses longer region slugs in the hostname. Get this mapping wrong and every API call returns 401/404. Pick the region that matches your licensing and data-residency obligations.

dc attribute KYX region Auth host Account host Data residency
us amer-1 auth.amer-1.jumio.ai account.amer-1.jumio.ai United States
eu emea-1 auth.emea-1.jumio.ai account.emea-1.jumio.ai European Union
sgp apac-1 auth.apac-1.jumio.ai account.apac-1.jumio.ai Singapore

Step 1 – Scaffold the Next.js 16 App Router Project

Create a fresh TypeScript project. Next.js 16 ships Turbopack as the default bundler for both dev and build, so the cold start is fast even with the SDK assets in the tree.

npx create-next-app@latest igaming-kyc \
  --typescript --app --eslint --src-dir=false --import-alias "@/*"
cd igaming-kyc
npm install @jumio/[email protected]
npm run dev

Confirm package.json pins "next": "16.2.7" and "react": "19.2.0". The @jumio/websdk install adds no transitive dependencies – the package is fully self-contained, which keeps your supply-chain surface small for a compliance-critical flow.

Project layout

We separate server-only Jumio logic (lib/jumio) from client UI (components) so the OAuth secret never crosses the network boundary. Route Handlers under app/api/kyc are the only place the secret is read.

igaming-kyc/
├─ app/
│  ├─ kyc/page.tsx              # client page that mounts the SDK
│  └─ api/kyc/
│     ├─ session/route.ts       # POST → mint SDK token
│     ├─ callback/route.ts      # POST → Jumio webhook
│     └─ status/route.ts        # GET  → server-verified status
├─ components/JumioKyc.tsx       # wraps <jumio-sdk>
├─ lib/jumio/{config,auth,account,retrieve}.ts
├─ lib/db.ts                     # player KYC store
├─ types/jumio-sdk.d.ts          # TSX element typing
├─ middleware.ts                 # CSP + Permissions-Policy
└─ .env.local

Step 2 – Store Credentials and Environment Config

Never hardcode the client secret and never expose it with a NEXT_PUBLIC_ prefix – anything prefixed that way is inlined into the browser bundle. Only the datacenter code is safe to expose publicly because the SDK needs it client-side.

# .env.local  (server-only unless NEXT_PUBLIC_)
JUMIO_CLIENT_ID=your_oauth_client_id
JUMIO_CLIENT_SECRET=your_oauth_client_secret
JUMIO_DATACENTER=us                       # us | eu | sgp
JUMIO_WORKFLOW_KEY=10013                   # ID + Identity (doc + selfie)
JUMIO_CALLBACK_URL=https://kyc.youroperator.com/api/kyc/callback

# Safe to expose: the SDK needs the region client-side
NEXT_PUBLIC_JUMIO_DC=us

Add .env.local to .gitignore. In production, inject these through your platform’s secret manager (Vercel, AWS Secrets Manager, Doppler) rather than a checked-in file.

Region resolver

Centralize the dc-to-hostname mapping so a single environment variable controls every endpoint. This is the file you change when you move an operator from the EU to the US datacenter.

// lib/jumio/config.ts
export type Dc = "us" | "eu" | "sgp";

const REGION: Record<Dc, string> = { us: "amer-1", eu: "emea-1", sgp: "apac-1" };

export const dc = (process.env.JUMIO_DATACENTER ?? "us") as Dc;
export const region = REGION[dc];
export const AUTH_HOST = `https://auth.${region}.jumio.ai`;
export const ACCOUNT_HOST = `https://account.${region}.jumio.ai`;

Step 3 – Mint an OAuth2 Access Token (Server Only)

Jumio’s KYX REST API is authorized with OAuth2 Bearer tokens obtained via the client_credentials grant and HTTP Basic authentication. The access token is valid for 3600 seconds (60 minutes), so cache it in memory and refresh just before expiry rather than calling the auth endpoint on every request.

// lib/jumio/auth.ts  – runs ONLY on the server
import { AUTH_HOST } from "./config";

let cached: { token: string; expiresAt: number } | null = null;

export async function getAccessToken(): Promise<string> {
  // Reuse until 60s before expiry
  if (cached && cached.expiresAt > Date.now() + 60_000) return cached.token;

  const basic = Buffer.from(
    `${process.env.JUMIO_CLIENT_ID}:${process.env.JUMIO_CLIENT_SECRET}`,
  ).toString("base64");

  const res = await fetch(`${AUTH_HOST}/oauth2/token`, {
    method: "POST",
    headers: {
      Authorization: `Basic ${basic}`,
      "Content-Type": "application/x-www-form-urlencoded",
      "User-Agent": "youroperator-kyc/1.0",
    },
    body: "grant_type=client_credentials",
    cache: "no-store",
  });

  if (!res.ok) throw new Error(`Jumio auth failed: ${res.status}`);
  const json = (await res.json()) as { access_token: string; expires_in: number };
  cached = { token: json.access_token, expiresAt: Date.now() + json.expires_in * 1000 };
  return cached.token;
}

The mandatory User-Agent header is easy to forget; Jumio rejects requests without a meaningful one. Keep this module out of any file that carries "use client" – if the secret ends up in a client bundle, rotate it immediately in the Portal.

Step 4 – Create the Account and Workflow Execution

An SDK session in the KYX model is an account plus a workflow execution. You POST to /api/v1/accounts with your internal player reference and the workflow key; the response contains the account id, the workflow execution id, and the short-lived sdk.token the browser will consume.

// lib/jumio/account.ts
import { ACCOUNT_HOST } from "./config";
import { getAccessToken } from "./auth";

export interface KycSession {
  accountId: string;
  workflowExecutionId: string;
  sdkToken: string;
}

export async function createKycSession(playerId: string): Promise<KycSession> {
  const token = await getAccessToken();

  const res = await fetch(`${ACCOUNT_HOST}/api/v1/accounts`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${token}`,
      "Content-Type": "application/json",
      "User-Agent": "youroperator-kyc/1.0",
    },
    body: JSON.stringify({
      customerInternalReference: playerId,         // your player id
      userReference: playerId,
      workflowDefinition: {
        key: Number(process.env.JUMIO_WORKFLOW_KEY), // 10013 = ID + Identity
        credentials: [{ category: "ID" }, { category: "SELFIE" }],
      },
      callbackUrl: process.env.JUMIO_CALLBACK_URL,
    }),
    cache: "no-store",
  });

  if (!res.ok) throw new Error(`Jumio account create failed: ${res.status}`);
  const data = await res.json();

  return {
    accountId: data.account.id,
    workflowExecutionId: data.workflowExecution.id,
    sdkToken: data.sdk.token,
  };
}

Setting customerInternalReference to your own player id is what makes the asynchronous callback resolvable later – without it you cannot reliably map a Jumio result back to a player.

Step 5 – Expose a Session Route Handler to the Client

The browser must never see your OAuth token or client secret. It only needs the sdk.token and the dc code. This Route Handler authenticates your own player session first, creates the Jumio session, persists the mapping, and returns the minimum surface area.

// app/api/kyc/session/route.ts
import { NextRequest, NextResponse } from "next/server";
import { createKycSession } from "@/lib/jumio/account";
import { getPlayerFromSession } from "@/lib/auth-session";
import { savePendingKyc } from "@/lib/db";
import { dc } from "@/lib/jumio/config";

export const runtime = "nodejs"; // Buffer + secrets → Node runtime

export async function POST(req: NextRequest) {
  const player = await getPlayerFromSession(req);
  if (!player) {
    return NextResponse.json({ error: "unauthenticated" }, { status: 401 });
  }

  try {
    const session = await createKycSession(player.id);
    await savePendingKyc(player.id, session.accountId, session.workflowExecutionId);
    // Return ONLY what the browser needs – never the OAuth token
    return NextResponse.json({ sdkToken: session.sdkToken, dc });
  } catch {
    return NextResponse.json({ error: "kyc_init_failed" }, { status: 502 });
  }
}

This is the single most important security boundary in the integration. If you are still deciding which provider to standardize on across your stack, our hands-on walkthrough of the Onfido identity verification SDK for casino onboarding shows the same server-mints-the-token pattern with a different vendor, which is useful when you are running a multi-provider fallback.

Step 6 – Type and Build the React 19 Client Wrapper

React 19 renders unknown lowercase-hyphenated tags as custom elements and passes string attributes through cleanly, but TypeScript’s JSX checker still needs a declaration for <jumio-sdk>. Add a module augmentation so the element and its attributes are typed.

// types/jumio-sdk.d.ts
import "react";

declare module "react" {
  namespace JSX {
    interface IntrinsicElements {
      "jumio-sdk": React.DetailedHTMLProps<
        React.HTMLAttributes<HTMLElement>,
        HTMLElement
      > & { dc?: string; token?: string; locale?: string };
    }
  }
}

Now the wrapper. We set dc declaratively but apply the fetched token imperatively through a ref – this matches Jumio’s documented setAttribute("token", …) pattern and avoids a race where React re-renders before the token arrives.

// components/JumioKyc.tsx
"use client";
import { useEffect, useRef, useState } from "react";

type Status = "loading" | "ready" | "started" | "finished" | "failed";

export default function JumioKyc({ dc }: { dc: "us" | "eu" | "sgp" }) {
  const ref = useRef<HTMLElement | null>(null);
  const [status, setStatus] = useState<Status>("loading");

  // 1. Register the custom element (client only) then fetch a token
  useEffect(() => {
    let active = true;
    import("@jumio/websdk").then(async () => {
      const res = await fetch("/api/kyc/session", { method: "POST" });
      if (!res.ok) { setStatus("failed"); return; }
      const { sdkToken } = await res.json();
      if (active && ref.current) {
        ref.current.setAttribute("token", sdkToken);
        setStatus("ready");
      }
    });
    return () => { active = false; };
  }, []);

  return (
    <div className="kyc-shell">
      <jumio-sdk ref={ref} dc={dc}></jumio-sdk>
      {status === "finished" && <p>Verifying your documents…</p>}
      {status === "failed" && <p>Something went wrong. Please retry.</p>}
    </div>
  );
}

Step 7 – Mount the SDK Without Breaking SSR

The @jumio/websdk import has side effects: it calls customElements.define and touches browser-only APIs like the camera. That code must never run on the server. The clean App Router answer is a "use client" component whose SDK import lives inside useEffect – exactly what Step 6 does. The page that hosts it can stay a server component.

// app/kyc/page.tsx  – server component
import JumioKyc from "@/components/JumioKyc";

export default function KycPage() {
  const dc = (process.env.NEXT_PUBLIC_JUMIO_DC ?? "us") as "us" | "eu" | "sgp";
  return (
    <main className="kyc-page">
      <h1>Verify your identity</h1>
      <p>Required before your first deposit or withdrawal.</p>
      <JumioKyc dc={dc} />
    </main>
  );
}

Why not next/dynamic with ssr: false?

A common reflex is to wrap the component in dynamic(() => import(...), { ssr: false }). In the Next.js App Router that throws if called inside a server component, and it is unnecessary here: the empty <jumio-sdk> tag renders harmlessly during SSR, and the element only “upgrades” once the client-side import registers it. Keeping the import in useEffect is simpler and avoids the error entirely. For broader market context, see our coverage of best sweepstakes casino sites.

Step 8 – Wire the workflow:* Event Lifecycle

The element dispatches DOM events on the jumio-sdk node. React 19 does not auto-bind non-standard custom events, so attach listeners with addEventListener against the ref and clean them up on unmount. The events let you show progress, but – and this matters – none of them is authoritative.

// add inside JumioKyc.tsx
useEffect(() => {
  const el = ref.current;
  if (!el) return;

  const onStarted  = () => setStatus("started");
  const onFinished = () => setStatus("finished");  // capture done, NOT verified
  const onFailed   = (e: Event) => {
    console.error("jumio workflow:failed", (e as CustomEvent).detail);
    setStatus("failed");
  };

  el.addEventListener("workflow:loaded", onStarted);
  el.addEventListener("workflow:started", onStarted);
  el.addEventListener("workflow:finished", onFinished);
  el.addEventListener("workflow:failed", onFailed);

  return () => {
    el.removeEventListener("workflow:loaded", onStarted);
    el.removeEventListener("workflow:started", onStarted);
    el.removeEventListener("workflow:finished", onFinished);
    el.removeEventListener("workflow:failed", onFailed);
  };
}, []);
Event Fires when Carries Authoritative?
workflow:loaded SDK UI is ready – No
workflow:started User begins capture – No
workflow:finished Documents submitted workflow ids No – confirm server-side
workflow:failed Capture/SDK error detail error (since 0.5.0) No

Step 9 – Handle the Callback (the Authoritative Result)

When the workflow execution reaches a terminal state, Jumio POSTs to the callbackUrl you supplied. Treat the callback body as a notification, not as truth: re-fetch the decision from the Retrieval API using the account and workflow-execution ids. This defends against forged callbacks and against partial bodies.

// app/api/kyc/callback/route.ts
import { NextRequest, NextResponse } from "next/server";
import { retrieveDecision } from "@/lib/jumio/retrieve";
import { applyKycDecision } from "@/lib/db";

export const runtime = "nodejs";

export async function POST(req: NextRequest) {
  const body = await req.json().catch(() => null);
  const accountId = body?.account?.id ?? body?.accountId;
  const wfeId = body?.workflowExecution?.id ?? body?.workflowExecutionId;
  if (!accountId || !wfeId) {
    return NextResponse.json({ ok: false }, { status: 400 });
  }

  // Never trust the POST body alone – re-fetch from Jumio
  const decision = await retrieveDecision(accountId, wfeId);
  await applyKycDecision(accountId, wfeId, decision);

  return NextResponse.json({ ok: true });
}

Add the public callback URL to your Jumio Portal allow-list and lock the route down with an IP allow-list or a shared secret in the path. During local development, expose the route with an HTTPS tunnel – Jumio will not call an http://localhost URL.

Make the callback idempotent. Jumio may retry delivery, so key your decision write on the workflowExecutionId and treat repeat deliveries as no-ops rather than re-running side effects such as crediting a welcome bonus. If you protect the route with a path secret, validate it before doing any work, and return a fast 200 so a slow handler does not trigger a retry storm. Persisting both the decision and the raw callback timestamp also gives your compliance team an audit trail when a regulator later asks when and how a given player was verified.

Step 10 – Retrieve and Interpret the Decision

The Retrieval API returns a decision.type of PASSED, REJECTED, WARNING, or NOT_EXECUTED. Map each to a concrete operator action; do not collapse WARNING into a pass.

// lib/jumio/retrieve.ts
import { ACCOUNT_HOST } from "./config";
import { getAccessToken } from "./auth";

export type DecisionType = "PASSED" | "REJECTED" | "WARNING" | "NOT_EXECUTED";

export async function retrieveDecision(
  accountId: string,
  wfeId: string,
): Promise<DecisionType> {
  const token = await getAccessToken();
  const res = await fetch(
    `${ACCOUNT_HOST}/api/v1/accounts/${accountId}/workflow-executions/${wfeId}`,
    { headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
      cache: "no-store" },
  );
  if (!res.ok) throw new Error(`Jumio retrieve failed: ${res.status}`);
  const data = await res.json();
  return data.decision?.type as DecisionType;
}
decision.type Meaning Operator action
PASSED Identity verified Unlock deposits and play
WARNING Verified with risk flags Hold; route to manual review / EDD
REJECTED Failed (mismatch or fraud) Block; offer one supervised retry
NOT_EXECUTED Flow abandoned/expired Re-prompt KYC on next login

Step 11 – Gate Deposits and Play on Server-Verified Status

The frontend should read KYC status only from your own backend, which in turn reflects the Jumio decision you persisted in Step 9. A lightweight status route lets the client poll or revalidate after the user returns from the SDK flow.

// app/api/kyc/status/route.ts
import { NextRequest, NextResponse } from "next/server";
import { getPlayerFromSession } from "@/lib/auth-session";
import { getKycStatus } from "@/lib/db";

export async function GET(req: NextRequest) {
  const player = await getPlayerFromSession(req);
  if (!player) return NextResponse.json({ error: "unauthenticated" }, { status: 401 });
  const status = await getKycStatus(player.id); // "PASSED" | "WARNING" | ...
  return NextResponse.json({ status });
}

Enforce the gate on the server side of every money movement – deposit, wager, withdrawal – not just in the UI. A hidden “Deposit” button is not a compliance control. In US markets you should pair this identity gate with location enforcement; our production guide to setting up GeoComply gambling geolocation in production covers how to combine the two checks into a single “may this player transact” decision.

Step 12 – Content-Security-Policy and Camera Permissions

The SDK needs the camera and must talk to *.jumio.ai. A permissive * policy is a finding waiting to happen in a gambling audit, so scope the CSP to Jumio origins and grant the camera through Permissions-Policy only on the KYC route.

// middleware.ts
import { NextResponse, NextRequest } from "next/server";

export function middleware(_req: NextRequest) {
  const res = NextResponse.next();
  res.headers.set(
    "Content-Security-Policy",
    [
      "default-src 'self'",
      "script-src 'self' https://*.jumio.ai",
      "connect-src 'self' https://*.jumio.ai",
      "frame-src https://*.jumio.ai",
      "img-src 'self' data: blob: https://*.jumio.ai",
      "media-src 'self' blob:",
      "worker-src 'self' blob:",
    ].join("; "),
  );
  res.headers.set("Permissions-Policy", 'camera=(self "https://*.jumio.ai")');
  return res;
}

export const config = { matcher: "/kyc/:path*" };

If you serve a strict nonce-based CSP elsewhere, keep the KYC route on this dedicated, slightly looser policy rather than weakening your global one. The blob: allowances in media-src and worker-src are what let the camera stream and the on-device liveness worker run.

Step 13 – Data Minimization and Regulatory Mapping

Document images and biometric selfies are special-category data. The React layer should never store them: the <jumio-sdk> element streams captures straight to Jumio, and your backend only ever persists the decision and the workflow ids. That keeps raw PII out of your logs, your analytics, and your error tracker.

Match the datacenter to the license

For a UK-licensed brand, run in the eu datacenter and verify age and identity before the customer can gamble, per the UK Gambling Commission rules on customer identification. For US states, route to us; New Jersey’s Division of Gaming Enforcement requires age-21 verification and identity proofing before real-money play. Malta-licensed operators under the Malta Gaming Authority follow the EU AML regime.

Retention and deletion

Honor data-subject deletion with the Retrieve & Delete permission on your API token, and set retention to the minimum your AML obligations allow (commonly five years post-relationship in the EU). Surface a clear privacy notice on the KYC page before the camera ever activates. When you are scoping vendors, our side-by-side of the leading iGaming KYC vendors compared head-to-head breaks down where each one stores data and how deletion requests are handled. For broader market context, see our coverage of leading online casino sites.

Step 14 – Test in Sandbox, Then Go Live

Run the entire flow against your sandbox credentials first. The fastest smoke test is to mint a token straight from the shell and confirm your region and credentials are wired correctly before any React code is involved.

# Smoke-test auth + region from the terminal
curl -s -X POST "https://auth.amer-1.jumio.ai/oauth2/token" \
  -u "$JUMIO_CLIENT_ID:$JUMIO_CLIENT_SECRET" \
  -H "User-Agent: youroperator-kyc/1.0" \
  -d "grant_type=client_credentials" | jq -r .access_token

Use Jumio’s sandbox test documents to drive each decision branch – a clean pass, a deliberate mismatch for REJECTED, and an abandoned session for NOT_EXECUTED. Verify the callback lands and that applyKycDecision flips your stored status. Only then swap to production credentials, point callbackUrl at your live domain, and re-add it to the Portal allow-list.

Production go-live checklist

  • Secrets in a manager, not .env.local; NEXT_PUBLIC_ limited to dc.
  • Callback URL allow-listed and protected by a shared secret or IP filter.
  • Access-token caching verified under load (no auth call per request).
  • CSP scoped to *.jumio.ai; camera granted only on /kyc.
  • Server-side gate on deposit, wager, and withdrawal endpoints.
  • Alerting on workflow:failed rates and 5xx from the session route.

Complete Project Example

Assembled, the integration is six small server files plus one client component and a middleware. The data flow is linear and auditable: the browser asks your server for a session, your server asks Jumio, the browser runs the SDK, Jumio calls your server back, and your server re-confirms the decision before unlocking anything.

Browser (React 19)
   │  POST /api/kyc/session            (no secrets cross this line)
   ▼
Route Handler ──► lib/jumio/auth.ts ──► auth.{region}.jumio.ai/oauth2/token
   │                                     (Bearer, cached 60 min)
   └──────────► lib/jumio/account.ts ──► account.{region}.jumio.ai/api/v1/accounts
                                          └─ returns { accountId, wfeId, sdkToken }
   ◄── { sdkToken, dc } ──┘
   │
Browser sets <jumio-sdk token dc>  →  user captures ID + selfie
   │
Jumio  ── POST callbackUrl ──►  /api/kyc/callback
                                   └─► lib/jumio/retrieve.ts (authoritative)
                                        └─► persist decision  →  gate unlocks

Because @jumio/websdk carries zero dependencies and your server logic is four tiny modules, the whole feature adds a remarkably small footprint to a Next.js 16 app – and every byte of PII flows to Jumio, never through your own tables.

5 Critical Pitfalls and How to Avoid Them

Most failed integrations fail in the same handful of places. Each of these has bitten real operators.

  1. Trusting workflow:finished to unlock funds. The client event means “documents submitted,” not “identity verified.” Unlock only after the server-side Retrieval API returns PASSED. This is the difference between a demo and a compliant system.
  2. Leaking the OAuth token or secret to the browser. Anything with a NEXT_PUBLIC_ prefix, or any import of lib/jumio/auth.ts into a "use client" file, ships your secret to every visitor. Keep the auth and account modules server-only.
  3. Mismatched datacenter codes. The SDK uses eu/us/sgp while the API host uses emea-1/amer-1/apac-1. Mixing them yields confusing 401/404 errors. The resolver in Step 2 prevents this.
  4. Calling the auth endpoint on every request. Tokens last an hour; minting a new one per request will rate-limit you and add latency. Cache and refresh just before expiry.
  5. Forgetting the HTTPS requirement. Browser camera APIs and Jumio callbacks both require HTTPS. http://localhost will fail silently in subtle ways – tunnel it.

8-Item Troubleshooting Guide

  1. 401 from /oauth2/token: wrong region host or bad Basic credentials. Confirm dc maps to the right auth.{region} host and that the client is enabled in the Portal.
  2. <jumio-sdk> renders empty: the SDK import never ran (it is inside useEffect) or the token attribute was set before the element registered. Check the import resolves and the ref is non-null.
  3. Camera prompt never appears: missing Permissions-Policy camera grant, a too-strict media-src CSP, or a non-HTTPS origin.
  4. Callback never arrives: URL not allow-listed in the Portal, pointing at localhost, or blocked by your WAF. Test with an HTTPS tunnel.
  5. Decision always NOT_EXECUTED: users abandon before submitting, or the workflow key does not include the credentials you expect. Confirm key 10013 for ID + selfie.
  6. Hydration warning around the element: ensure the host page stays a server component and the wrapper carries "use client"; do not force ssr: false via next/dynamic in a server component.
  7. TypeScript error on <jumio-sdk>: the JSX augmentation in types/jumio-sdk.d.ts is missing from tsconfig include.
  8. 403 on Retrieval: your API token lacks the Retrieve & Delete permission. Re-issue it in the Portal with both Initialize and Retrieve & Delete.

Advanced Tips: Continuous Monitoring and Deepfake Defense

Once the basic flow is live, the 2026 differentiators are about what happens after onboarding.

Re-KYC and continuous monitoring

With injection attacks up 700% year over year, a single onboarding check ages quickly. Jumio Watch’s premise – up to 25% more risk visible after onboarding – argues for periodic re-verification triggered by risk signals: a new device, an impossible-travel login, or a large withdrawal. Architect your status store so a PASSED player can be flipped back to PENDING_REKYC and routed through the same <jumio-sdk> component without code changes.

Liveness, NFC and locale

The selfie credential runs liveness on-device; for chipped passports and national IDs you can add NFC reading where the workflow supports it, which raises assurance against printed-photo and screen-replay attacks. Set the element’s locale attribute to localize the capture UI for international players – a small change that measurably improves completion rates in non-English markets.

Theming with slots and CSS variables

The SDK exposes slots for high-level customization (theme, navigation, logging) and CSS variables for fonts and colors. Match the verification screen to your brand so the handoff from your React UI to the SDK feels seamless rather than like a redirect to a third party – a subtle but real driver of drop-off reduction.

Observability and drop-off metrics

Instrument the funnel: count workflow:loaded against workflow:finished to measure abandonment, and alert when the gap widens – a spike usually means a camera-permission or CSP regression, not a change in user behavior. Track the distribution of decision.type over time; a sudden rise in REJECTED or WARNING can be an early signal of a coordinated fraud ring rather than a code bug. Jumio’s risk signals return in roughly five seconds for the bulk of traffic, so a climbing decision latency is itself worth paging on. Feed these metrics into the same dashboard your AML team watches, so identity friction and transaction risk are read together rather than in separate silos.

Beyond the Standard Coverage: Gaps in Competitor Docs

Search the primary keyword and the top results are thin in predictable ways. Jumio’s own npm README stops at “set dc and token on an index.html and run npx serve.” The cross-platform documentation page only covers React Native, Cordova and Flutter – not React for the web. Older forum answers still reference the legacy netverify.com/api/v4/initiate endpoint, which predates the KYX token model entirely. None of them close the loop that an actual iGaming operator needs.

What none of them explain

  • Server-authoritative decisions. Every quick-start ends at “the SDK finished.” None show the callback-plus-Retrieval pattern that prevents a tampered client from unlocking funds.
  • App Router and SSR reality. The official samples are plain index.html; they say nothing about "use client", the useEffect import, or why next/dynamic with ssr: false throws in a server component.
  • React 19 custom-element typing. The TSX declaration that stops the compiler from rejecting <jumio-sdk> appears nowhere in vendor docs.
  • The two-token mental model. Beginners conflate the OAuth Bearer token (server-only, 60 minutes) with the per-session SDK token (browser-facing, short-lived). The distinction is the whole security story.
  • Regulatory routing. No quick-start ties the dc value to UKGC, NJ DGE or MGA obligations, even though picking the wrong region can breach data-residency terms.

This guide treats those gaps as the main event, because they are exactly where a working prototype diverges from a production iGaming KYC system.

NetVerify v4 vs the KYX Token Flow: What Changed

If you are maintaining an older Jumio integration, it almost certainly speaks to the legacy NetVerify Web v4 API – a single initiate call against a regional netverify.com host that returned a redirect URL and a transaction reference. That model still functions for existing customers, but it predates everything this guide relies on: OAuth2 client-credentials auth, the account-plus-workflow-execution object model, the <jumio-sdk> Web Component, and the workflow:* event stream.

The migration is mostly a re-think of where state lives. Under v4 you tracked a scanReference and polled a retrieval endpoint for a status string. Under KYX you track an accountId and a workflowExecutionId, you receive a structured decision.type, and you drive the UI from real DOM events instead of an opaque iframe. The data you persist shrinks to a couple of identifiers and a decision – never the document images themselves.

Port in this order: stand up the OAuth token service (Step 3), swap the initiate call for account creation (Step 4), replace the iframe with the Web Component (Steps 6–7), and only then retire the old polling loop in favor of the callback-plus-Retrieval pattern (Steps 9–10). Run both paths in parallel behind a feature flag until your callback success rate on the KYX path matches the legacy one, then cut over a percentage of traffic at a time so a regression never takes the whole onboarding funnel down at once.

Frequently Asked Questions

Is @jumio/websdk free to use?

The npm package itself is free and carries no runtime dependencies. What you pay for is verification volume on your Jumio KYX contract; Jumio prices per check via sales rather than a public MSRP, and pricing depends on workflow, region and volume.

Does the Web SDK work with React 19 and Next.js 16?

Yes. React 19 renders the <jumio-sdk> custom element natively, and Next.js 16’s App Router hosts it through a "use client" component that imports the SDK inside useEffect. Add a small TSX type declaration and the integration is fully typed.

Can I trust the workflow:finished event to unlock a player’s account?

No. Client events indicate the user submitted their documents, not that verification passed. Always confirm the result server-side via the callback and the Retrieval API, which returns PASSED, WARNING, REJECTED or NOT_EXECUTED.

Which datacenter should an iGaming operator choose?

Match it to your license and data-residency rules: eu (emea-1) for UK/EU brands, us (amer-1) for US states, and sgp (apac-1) for Asia-Pacific. The dc attribute and the API hostname must use the same region.

What is the difference between NetVerify and the KYX Platform?

NetVerify is Jumio’s identity-verification product; the KYX Platform is the current delivery model with OAuth2, workflow executions and the Web SDK. The legacy NetVerify v4 API still functions, but new builds should use the KYX token flow shown here.

How do I handle a WARNING decision?

Treat it as a hold, not a pass. Route the player to manual review or enhanced due diligence, keep deposits locked, and only release after a human or an additional check clears the flags.

Does the SDK detect deepfakes and injection attacks?

The selfie credential includes liveness, and Jumio applies risk signals on top. For post-onboarding threats – injection attacks rose 700% year over year in 2026 – Jumio Watch adds continuous monitoring that re-assesses identity over time rather than only at signup.

How long is the SDK token valid?

The browser-facing SDK token is short-lived and tied to a single workflow execution, so mint it per session from your Route Handler. The separate server-side OAuth access token lasts 3600 seconds (60 minutes) and should be cached and reused until just before it expires.

Casey Mitchell

Casey Mitchell

Crypto Casino Editor

Casey Mitchell tracks crypto casinos, on-chain gaming, provably fair RNG, and the regulatory gray zones around US-facing crypto gambling platforms. Reviews wallets, deposit flows, and KYC policies across BTC, ETH, and stablecoin operators.

View all articles