Most sportsbook KYC tutorials still send the bettor away from your site. They create a Veriff session, redirect the browser to a hosted verification page, and pray the user comes back to finish the deposit. In 2026 that redirect is a liability: the UK Gambling Commission has scrapped the old 72-hour grace period, deepfake-driven fraud is up sharply year over year, and every bounce between your sportsbook and an external domain is a chance to lose a regulated, ready-to-deposit customer.
This tutorial takes the opposite approach. We embed the verification flow inside your own page using the Veriff InContext SDK (@veriff/incontext-sdk v2.5.0) together with the Veriff JS SDK (@veriff/js-sdk v2.0.0), wired to a server-authoritative Next.js App Router backend in TypeScript. You will create sessions on the server, render the Veriff iframe modal in-context, verify decision webhooks with HMAC-SHA256, and map every Veriff decision to a sportsbook account state – in 14 steps, in roughly 35 minutes of hands-on work. Every version number and statistic below is drawn from official Veriff documentation, the npm registry, the UK Gambling Commission, and named 2025–2026 market research.
Don't miss new tech stories on Google
Add Tech Insider once in the Google app and our stories appear in your news suggestions.
Why Embedded KYC Beats the Redirect Flow for Sportsbooks in 2026
The identity verification market is no longer a niche line item. Future Market Insights values the global identity verification market at roughly USD 14.1 billion in 2026, growing to USD 42.8 billion by 2036 at a 13.1% CAGR, and the KYC/KYB/onboarding segment alone accounted for the largest single share – 43.2% in 2025. Gaming and gambling is the fastest-growing vertical inside that market, expanding at roughly an 11.24% CAGR through 2031. For a sportsbook, KYC is not a cost center to minimize; it is the gate that every depositing customer must pass, so the gate’s user experience directly drives revenue.
The 72-Hour Grace Period Is Gone – Verification Must Be Instant
Under LCCP Condition 17.1.1, UKGC-licensed operators must verify a customer’s name, address, and date of birth – and confirm they are 18 or older – before that customer deposits, bets, or accesses any gambling product. In 2025 the Commission eliminated the legacy 72-hour grace period that once let operators take a deposit first and verify later. That single regulatory change is why the redirect flow is now risky: if a user bounces to an external page and abandons, you cannot legally let them deposit, and you have lost the customer. An embedded flow keeps them on your domain through the entire check.
Conversion Math: Why an In-Context Iframe Outperforms a Redirect
The InContext SDK renders Veriff’s verification application in an iframe modal layered over your own sportsbook page. The bettor never sees a different domain, never loses your branding, and never has to re-find your deposit screen afterward. Veriff’s own integration guidance positions the InContext approach as the “frictionless web flow” specifically because keeping users in-context reduces abandonment. When a verification finishes, your single-page app reacts to an event in milliseconds and can move the user straight to the cashier – no return redirect, no orphaned tabs.
The 2026 Fraud Landscape Raises the Stakes
Embedded UX is not only about conversion; it is about control. Veriff’s 2025–2026 Identity Fraud Report found global fraud attempts up 21% year over year, with impersonation accounting for more than 82% of all fraudulent attempts and deepfakes now behind roughly 1 in every 20 verification failures. Adversary-in-the-middle attacks – many of them real-time deepfakes – rose 46%, and digitally presented media was 300% more likely to be entirely AI-generated or altered than the year before. On the consumer side, 78.65% of survey respondents said they had been targeted by deepfake or AI-generated fraud at least once in the prior 12 months, and the share of US consumers suffering a financial loss to fraud jumped from about a third in 2024 to 66% in 2025. Keeping verification in-context, on infrastructure you control, removes a class of redirect-based interception that attackers increasingly exploit – and gives you a single, signed audit artifact per check.
Where the InContext SDK Fits in the Veriff Stack
Veriff ships three browser-relevant building blocks: the JS SDK (@veriff/js-sdk) to create a session and obtain a session URL, the InContext SDK (@veriff/incontext-sdk) to render that URL as an embedded iframe, and a React Native SDK for native mobile apps. This tutorial uses the JS SDK pattern on the server (via the raw REST API for security) and the InContext SDK on the client. Veriff is headquartered in Tallinn, Estonia, was founded in 2015 by CEO Kaarel Kotkas, reached a USD 1.5 billion valuation as Estonia’s ninth unicorn, and crossed roughly USD 110 million in ARR in 2025 – so this is a vendor with the scale to back a regulated sportsbook.
Prerequisites: Versions, Accounts, and Environment
Pin every version before you start. Embedded KYC breaks in subtle ways when the SDK and runtime drift, so this stack is what the rest of the tutorial assumes.
Software Versions
| Component | Version | Notes |
|---|---|---|
| Node.js | 20.14.0 LTS or 22.x | Native fetch and crypto.timingSafeEqual required |
| Next.js | 15.x (App Router) | Route Handlers for server-side session + webhook |
| TypeScript | 5.4+ | Strict mode on |
| React | 18.3 or 19.x | Client component for the frame trigger |
@veriff/incontext-sdk |
2.5.0 (8 Aug 2025) | Deps: [email protected], [email protected] |
@veriff/js-sdk |
2.0.0 (10 Sep 2025) | Optional client helper; we call the REST API server-side |
Veriff Station Account, API Key, and Shared Secret
Sign up for the Veriff Customer Portal (Station) and create an integration. Each integration exposes two credentials you will need: an API key (sent as the X-AUTH-CLIENT header when creating sessions) and a shared secret key (used to compute and verify the X-HMAC-SIGNATURE on webhooks). Treat the shared secret like a password – it never touches the browser. The official Veriff developer documentation is the canonical reference for endpoint behavior and decision codes.
Regulatory Prerequisites
Decide your licensing jurisdiction before writing code, because it dictates which data you collect and how long you keep it. For UKGC you must verify identity at onboarding under LCCP 17.1.1; for Malta you follow the Malta Gaming Authority risk-based framework. If you are still choosing a vendor, our earlier teardown that compared Veriff against Sumsub, Onfido and Jumio for iGaming KYC is the fastest way to confirm Veriff is the right fit for your risk profile.
Architecture: Server-Authoritative Embedded KYC
The single most important design decision in this build is that the server, not the browser, is the source of truth. The InContext SDK fires friendly client-side events such as FINISHED, but those events only tell you the user clicked through the UI – they are not a compliance decision. The real decision arrives later, signed, on a webhook. Build for that asymmetry from line one.
The Four Moving Parts
- Session endpoint – a Next.js Route Handler that calls Veriff’s
POST /v1/sessionswith your API key and returns only the session URL to the browser. - Client frame – a React client component that passes that URL to
createVeriffFrame()and listens for lifecycle events. - Webhook endpoint – a Route Handler that receives Veriff’s signed decision, verifies the HMAC, and updates your store.
- Decision store – your users table (or a dedicated KYC table) that records the authoritative verification state.
Why the Client Never Holds Your API Key
If you call POST /v1/sessions from the browser, your API key ships in client JavaScript where any attacker can scrape it and mint sessions against your account. Creating sessions server-side keeps the key on the server, lets you attach a tamper-proof vendorData (your internal user ID) to each session, and gives you a natural place to enforce rate limits and per-user de-duplication.
The Data Flow, End to End
Browser asks your server for a session → server calls Veriff and returns verification.url → browser opens the InContext iframe → user completes capture → Veriff runs automated checks → Veriff POSTs a signed decision webhook to your server → server verifies HMAC and flips the account to approved/declined → browser polls or subscribes to your own status endpoint and unlocks the cashier. The same skeleton underpins other vendors too; if you have already wired an Onfido identity verification SDK for casino onboarding, the webhook-as-source-of-truth pattern will feel familiar.
Steps 1–2: Scaffold Next.js and Install the Veriff SDKs
Step 1 – scaffold a TypeScript Next.js app with the App Router, then add the two Veriff packages at the exact versions we pinned.
# Step 1: scaffold
npx create-next-app@15 sportsbook-kyc --typescript --app --eslint
cd sportsbook-kyc
# Step 2: install the Veriff SDKs (pinned)
npm install @veriff/[email protected] @veriff/[email protected]
# verify what npm actually resolved
npm ls @veriff/incontext-sdk @veriff/js-sdk
Expected output from npm ls:
[email protected]
├── @veriff/[email protected]
└── @veriff/[email protected]
Project Structure
app/
api/
veriff/
session/route.ts # POST: create a Veriff session
webhook/route.ts # POST: receive + verify decisions
status/route.ts # GET: client polls KYC state
kyc/page.tsx # the embedded KYC screen
lib/
veriff.ts # server-side Veriff client + HMAC helpers
store.ts # decision store (swap for your DB)
.env.local
Environment Variables
Step 2 (cont.) – never hardcode credentials. Put them in .env.local, which Next.js keeps server-only unless prefixed with NEXT_PUBLIC_.
# .env.local
VERIFF_API_KEY=pk_live_xxxxxxxxxxxxxxxx
VERIFF_SHARED_SECRET=sk_live_yyyyyyyyyyyyyyyy
VERIFF_BASE_URL=https://stationapi.veriff.com/v1
APP_BASE_URL=https://sportsbook.example.com
Steps 3–4: Build the Server-Side Session Endpoint
Step 3 – create a small server module that knows how to talk to Veriff and how to sign payloads. Centralizing this keeps your Route Handlers thin and your HMAC logic in exactly one place.
// lib/veriff.ts
import crypto from 'crypto';
const BASE = process.env.VERIFF_BASE_URL!;
const API_KEY = process.env.VERIFF_API_KEY!;
const SECRET = process.env.VERIFF_SHARED_SECRET!;
// HMAC-SHA256 of a raw string payload, hex-encoded (lowercase).
export function sign(payload: string): string {
return crypto.createHmac('sha256', SECRET).update(payload, 'utf8').digest('hex');
}
export interface VeriffSession {
id: string;
url: string;
sessionToken: string;
}
export async function createSession(vendorData: string): Promise<VeriffSession> {
const body = JSON.stringify({
verification: {
vendorData, // your internal user id
callback: `${process.env.APP_BASE_URL}/kyc/return`,
},
});
const res = await fetch(`${BASE}/sessions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-AUTH-CLIENT': API_KEY, // API key only for session creation
},
body,
});
if (!res.ok) {
throw new Error(`Veriff session failed: ${res.status} ${await res.text()}`);
}
const json = await res.json();
return {
id: json.verification.id,
url: json.verification.url,
sessionToken: json.verification.sessionToken,
};
}
The Veriff Session Payload
The only field you truly need is vendorData – set it to your own user ID so that when the decision webhook returns, you know exactly which sportsbook account to update. Avoid pushing personally identifiable information into the session body unless your flow requires pre-filled data; data minimization is both a GDPR best practice and an MGA expectation.
The Route Handler
Step 4 – expose the session creator over HTTP. In production, replace the inline user ID with the authenticated session user.
// app/api/veriff/session/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createSession } from '@/lib/veriff';
export async function POST(req: NextRequest) {
const { userId } = await req.json();
if (!userId) return NextResponse.json({ error: 'userId required' }, { status: 400 });
try {
const session = await createSession(userId);
// Return ONLY the URL to the browser – never the API key or session id internals.
return NextResponse.json({ url: session.url });
} catch (err) {
return NextResponse.json({ error: 'session_failed' }, { status: 502 });
}
}
Steps 5–6: Render the Embedded Veriff Frame
Step 5 – build the client component. It fetches a session URL from your endpoint, then hands it to createVeriffFrame(). This is the exact API surface documented for @veriff/incontext-sdk v2.5.0.
// app/kyc/VeriffButton.tsx
'use client';
import { useState } from 'react';
import { createVeriffFrame, MESSAGES } from '@veriff/incontext-sdk';
export default function VeriffButton({ userId }: { userId: string }) {
const [state, setState] = useState<'idle' | 'open' | 'submitted' | 'closed'>('idle');
async function startKyc() {
const res = await fetch('/api/veriff/session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId }),
});
const { url } = await res.json();
setState('open');
createVeriffFrame({
url,
onEvent(msg: MESSAGES) {
switch (msg) {
case MESSAGES.STARTED: setState('open'); break;
case MESSAGES.SUBMITTED: setState('submitted'); break; // docs uploaded
case MESSAGES.FINISHED: setState('submitted'); break; // UI flow done
case MESSAGES.CANCELED: setState('closed'); break; // user closed modal
case MESSAGES.RELOAD_REQUEST: window.location.reload(); break;// Safari camera re-prompt
}
},
});
}
return (
<button onClick={startKyc} disabled={state === 'open'}>
{state === 'submitted' ? 'Verification submitted – checking…' : 'Verify my identity'}
</button>
);
}
Handling the MESSAGES Lifecycle
The SDK emits five events: STARTED, SUBMITTED, FINISHED, CANCELED, and RELOAD_REQUEST. Use SUBMITTED/FINISHED only to update the UI (“checking…”), never to unlock deposits. The RELOAD_REQUEST case exists for an iOS Safari edge case where the user denied camera access and the parent window must reload to re-prompt – handle it or mobile bettors will silently stall.
Modal vs Embedded Mode
By default createVeriffFrame() renders a centered modal over your page. If you want the flow inline (for example, inside a dedicated onboarding panel), set embedded: true and point embeddedOptions.rootElementID at a container that is at least 600×680 px (800×800 is optimal):
createVeriffFrame({
url,
embedded: true,
embeddedOptions: { rootElementID: 'kyc-container' },
});
// <div id="kyc-container" style="width:800px;height:800px" />
Steps 7–8: Secure the Decision Webhook with HMAC-SHA256
Step 7 – this is the compliance backbone. Veriff signs every webhook by computing an HMAC-SHA256 of the raw request body using your shared secret and sends it in the X-HMAC-SIGNATURE header. You must recompute that signature over the untouched raw bytes and compare in constant time. If the signatures differ, drop the request – it is forged or corrupted.
// app/api/veriff/webhook/route.ts
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';
import { sign } from '@/lib/veriff';
import { applyDecision } from '@/lib/store';
export async function POST(req: NextRequest) {
// 1) Read the RAW body – do not JSON.parse before verifying.
const raw = await req.text();
const received = req.headers.get('x-hmac-signature') ?? '';
const expected = sign(raw);
// 2) Constant-time compare; guard equal length first (timingSafeEqual throws otherwise).
const a = Buffer.from(received, 'utf8');
const b = Buffer.from(expected, 'utf8');
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return NextResponse.json({ error: 'bad_signature' }, { status: 401 });
}
// 3) Only now is it safe to parse and act.
const payload = JSON.parse(raw);
await applyDecision(payload);
// 4) Always 200 quickly so Veriff does not retry a processed event.
return NextResponse.json({ received: true });
}
Why Client Events Are Not Compliance Evidence
A bettor can open the browser console and dispatch a fake FINISHED event, or sit on a modified front end entirely. The webhook is signed with a secret only your server and Veriff hold, so it is the only artifact you can defend to an auditor. Make it structurally impossible to grant deposit rights from the client: the client UI may say “submitted”, but the deposit button stays locked until the webhook flips your stored state.
Constant-Time Comparison Matters
Comparing signatures with === leaks timing information that, at scale, can help an attacker forge a valid signature byte by byte. crypto.timingSafeEqual compares in constant time, but it throws if the two buffers differ in length – hence the explicit length guard before the call.
Steps 9–10: Map Veriff Decisions to Account States
Step 9 – Veriff’s decision webhook carries a verification.status and a numeric code. Translate each into a concrete sportsbook account action. Never treat anything except an explicit positive decision as permission to deposit.
| Veriff status | Code | Sportsbook account action |
|---|---|---|
approved |
9001 | Mark KYC passed; unlock deposit + betting |
declined |
9102 | Block account; route to manual review/SAR queue |
resubmission_requested |
9103 | Prompt user to re-run KYC (poor image, glare) |
review |
– | Hold; await human compliance decision |
expired / abandoned |
9104 | Keep locked; allow a fresh session |
The Decision Handler
// lib/store.ts (swap the map for your real database)
type KycState = 'pending' | 'approved' | 'declined' | 'resubmit' | 'review' | 'expired';
const db = new Map<string, { state: KycState; updatedAt: string; eventId?: string }>();
export async function applyDecision(payload: any) {
const v = payload.verification;
if (!v) return; // event webhook, not a decision
const userId: string = v.vendorData;
const eventId: string = payload.id ?? v.id;
// Idempotency: ignore a decision we have already processed.
const current = db.get(userId);
if (current?.eventId === eventId) return;
const map: Record<string, KycState> = {
approved: 'approved',
declined: 'declined',
resubmission_requested: 'resubmit',
review: 'review',
expired: 'expired',
abandoned: 'expired',
};
db.set(userId, {
state: map[v.status] ?? 'pending',
updatedAt: new Date().toISOString(),
eventId,
});
}
export async function getState(userId: string) {
return db.get(userId)?.state ?? 'pending';
}
Idempotency and Replay Protection
Step 10 – Veriff retries webhooks until it receives a 2xx, so the same decision can land more than once. Key your writes on the event ID and short-circuit duplicates, exactly as above. Then expose a tiny GET /api/veriff/status endpoint the client polls every few seconds to learn when the cashier should unlock – the browser asks your server, never Veriff directly. For broader market context, see our coverage of best sweepstakes casino sites.
The Status Endpoint the Client Polls
This endpoint is intentionally dumb: it reads the authoritative state your webhook wrote and returns a single boolean the front end can act on. Because the deposit gate is derived here and re-checked again at the cashier API, there is no single client value an attacker can flip to unlock funding.
// app/api/veriff/status/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getState } from '@/lib/store';
export async function GET(req: NextRequest) {
const userId = req.nextUrl.searchParams.get('userId');
if (!userId) return NextResponse.json({ error: 'userId required' }, { status: 400 });
const state = await getState(userId);
return NextResponse.json({ state, depositUnlocked: state === 'approved' });
}
On the client, poll this on an interval after SUBMITTED and stop once depositUnlocked is true or a decline arrives. For higher-traffic books, swap polling for a server-sent events stream so the cashier unlocks the instant the webhook lands rather than on the next poll tick.
Steps 11–12: AML Screening and Age Gating
Identity proofing answers “is this person who they claim to be?” Compliance also demands “should this person be allowed to gamble, and are they who we must report on?” Those are separate questions, and Veriff deliberately does not answer all of them.
Veriff Is Screening-Only – Add Transaction Monitoring
Step 11 – Veriff’s AML capability is screening (sanctions, PEP, adverse media) at onboarding. It is not a transaction-monitoring engine, so a complete sportsbook AML program layers ongoing monitoring on top. If you need real-time post-onboarding surveillance and Travel Rule handling, our walkthrough of real-time AML transaction monitoring with Sumsub Travel Rule shows how to bolt that second layer onto a KYC-verified user base. Treat a positive PEP or sanctions hit as a hold-and-escalate event, never an auto-block, because false positives are common.
Age Gating and the 18+ Rule
Step 12 – UKGC requires confirmed 18+ before any gambling activity. Veriff extracts date of birth from the document, and an optional age-estimation add-on can pre-screen lower-risk users. Implement age as a hard server-side gate keyed off the verified DOB returned with an approved decision – do not trust a self-declared birthday from a form.
Layering Geolocation
For US-facing sportsbooks, identity is only half the puzzle; you must also prove the bettor is physically inside a legal state at bet time. Pair this KYC build with a geolocation check – our guide to a production GeoComply gambling geolocation setup covers that adjacent control without duplicating any of the identity work here.
Steps 13–14: Production Hardening and Go-Live
Step 13 – the happy path works; now make it survive the real internet. Veriff’s 2025 Identity Fraud Report found global fraud attempts up 21% year over year, with deepfakes now behind 1 in 20 verification failures and adversary-in-the-middle attacks up 46% – so your integration is an active target, not a formality.
Rate Limiting, Retries, and Observability
- Rate-limit session creation per user and per IP to stop session-minting abuse.
- Make the webhook idempotent and fast – return 200 in well under a second and do heavy work asynchronously, so Veriff does not retry.
- Log every decision with its event ID, status, and timestamp for audit reconstruction.
- Alert on signature failures; a spike means either a secret rotation gone wrong or an attack.
GDPR Data Minimization and Retention
Step 14 – store the verification outcome and the minimum identifiers you are legally required to keep; let Veriff retain the biometric and document media under its own retention controls. Define a retention clock that matches your license conditions, and make sure your privacy notice tells bettors a third-party processor handles their documents. A practical pattern is to persist only the verified DOB, a pass/fail flag, the decision event ID, and the timestamp – everything an auditor needs to confirm the gate fired, and nothing more. Avoid copying document images into your own logs, since that turns every log backup into a high-risk biometric data store.
Rotating the Shared Secret Safely
Secrets leak, staff churn, and good programs rotate credentials on a schedule. The trap is that webhooks in flight during a rotation are signed with the old secret. Support a brief overlap window where your verifier accepts a signature matching either the current or previous secret, then retire the old key once the queue drains. Log which key validated each webhook so you can confirm the old secret is genuinely unused before deleting it – and alert immediately if a signature matches neither, because that is your earliest signal of a forged request or a botched rotation.
Go-Live Checklist
- Switch
VERIFF_BASE_URLand keys from sandbox to production. - Confirm the production webhook URL is registered in Station and reachable over HTTPS.
- Verify HMAC validation rejects a deliberately tampered payload (write a test for it).
- Confirm the deposit button cannot unlock without a stored
approvedstate. - Load-test the session and webhook endpoints at your expected peak sign-up rate.
UKGC and MGA Compliance Mapping for Embedded Veriff
Auditors do not care that your code is elegant; they care that each control maps to a named obligation. This table makes the mapping explicit so you can hand it to a compliance reviewer. For the deeper regulatory context, our breakdown of the 2026 MGA and UKGC casino API audit requirements details the documentation auditors now expect.
| Obligation | Regulator | How this build satisfies it |
|---|---|---|
| Verify identity before deposit/bet | UKGC LCCP 17.1.1 | Deposit locked until webhook stores approved |
| No 72-hour grace period | UKGC (2025) | Embedded flow runs at sign-up, pre-deposit |
| Confirm 18+ | UKGC / MGA | Hard server gate on verified DOB |
| Risk-based onboarding | MGA | Step-up/age-estimation per user risk tier |
| AML screening + ongoing monitoring | UKGC / MGA | Veriff screening + external transaction monitoring |
| Audit trail | Both | Signed webhook log keyed on event ID |
LCCP 17.1.1 in Practice
The condition is prescriptive: name, address, and date of birth must be verified before the customer gambles. Your enforcement point is the cashier, not the KYC screen – gate the deposit API server-side on the stored state, so even a bettor who tampers with the front end cannot fund an account.
MGA’s Risk-Based Approach
Malta gives operators more design latitude than the UKGC’s rulebook but expects you to justify your risk tiers. Document why a given user got a lighter or heavier check, and keep that logic versioned alongside your code so the rationale is reproducible at audit time.
InContext vs JS SDK Redirect vs React Native: 2026 Comparison
Choosing the right Veriff surface is the difference between a smooth onboarding and a leaky funnel. Here is how the three browser/mobile approaches compare for a sportsbook in 2026.
| Dimension | InContext SDK (this guide) | JS SDK redirect | React Native SDK |
|---|---|---|---|
| Package | @veriff/[email protected] |
@veriff/[email protected] |
Native module |
| User stays on your domain | Yes (iframe modal) | No (full redirect) | Yes (native screen) |
| Best for | Web sportsbooks, SPAs | Simplest legacy web | Native iOS/Android apps |
| Camera UX | In-page, branded | Veriff-hosted page | Native permissions |
| Return-trip risk | None | High (lost tabs) | None |
| Setup effort | Moderate | Lowest | Highest |
When to Use Each
Use the InContext SDK for any modern web sportsbook where conversion matters – it is the focus of this tutorial precisely because it removes the redirect bounce. Reach for the plain JS SDK redirect only when you cannot run an iframe (rare). Use the React Native SDK when your primary product is a native app, where an embedded web flow would feel out of place. For broader market context, see our coverage of leading online casino sites.
Why We Did Not Reuse the Redirect Pattern
An earlier Tech Insider tutorial covered the redirect-style @veriff/js-sdk integration on a Node/Express backend. This guide deliberately moves to the embedded InContext model on Next.js because in-context verification is now Veriff’s recommended frictionless web pattern and the better fit for 2026 conversion and compliance pressure.
6 Pitfalls That Break Embedded Veriff Integrations
Most failed launches die on the same handful of mistakes. Avoid these and you avoid roughly 90% of support tickets.
Trusting the Client Event
Pitfall 1: unlocking deposits on the FINISHED event. It is forgeable and only means the UI flow ended. Always gate on the signed webhook.
Parsing the Body Before Verifying
Pitfall 2: calling req.json() before computing the HMAC. Re-serializing changes byte order and whitespace, so your signature will never match. Read the raw text first, verify, then parse.
Length-Mismatch Crashes
Pitfall 3: passing unequal-length buffers to crypto.timingSafeEqual, which throws and 500s your webhook – causing Veriff to retry forever. Guard the length first.
Shipping the API Key to the Browser
Pitfall 4: creating sessions client-side. Keep X-AUTH-CLIENT server-only. A leaked key lets anyone burn your Veriff quota.
Ignoring Resubmission
Pitfall 5: treating resubmission_requested as a decline. It is a recoverable “try again” – surface a clear prompt and create a fresh session, or you will reject good customers over a glare-spoiled photo.
No Idempotency
Pitfall 6: double-processing retried webhooks, which can flip a user back and forth. Key writes on the event ID and ignore duplicates.
Troubleshooting: 8 Errors and Their Fixes
| Symptom | Likely cause | Fix |
|---|---|---|
401 on POST /v1/sessions |
Wrong/missing X-AUTH-CLIENT |
Send the API key; confirm sandbox vs prod |
| Webhook always 401s locally | Body re-parsed before HMAC | Read raw text first; verify, then parse |
timingSafeEqual throws |
Buffers differ in length | Guard length before comparing |
| Modal never opens | Empty/invalid session URL | Log the server response; check verification.url |
| Camera blocked on iOS Safari | Permission denied mid-flow | Handle RELOAD_REQUEST to re-prompt |
| Decision never arrives | Webhook URL unreachable | Use a public HTTPS URL; check Station config |
| Duplicate state flips | No idempotency key | De-dupe on event ID |
| Deposit unlocks too early | Gating on client event | Gate only on stored approved state |
Reproducing a Signature Failure Safely
To test rejection, send your webhook endpoint a valid-looking body with a deliberately wrong X-HMAC-SIGNATURE header; you should get a 401 and no state change. Keep this as an automated test so a future refactor cannot silently disable verification.
Sandbox vs Production Drift
The most common “it worked yesterday” failure is mixing sandbox keys with the production base URL (or vice versa). Centralize both in .env.local and assert at boot that the key prefix matches the environment.
Advanced Tips: Reverification, Step-Up, and Conversion Telemetry
Once the core flow is solid, these patterns separate a compliant integration from a competitive one.
In-Play Reverification and Step-Up
High-risk triggers – a large withdrawal, a device change, a dormant account waking up – should fire a fresh InContext session rather than relying on the original onboarding check. Because session creation is server-side, you can mint a re-verification session from any backend trigger and surface the same embedded modal, keeping the bettor on-site.
Conversion Telemetry
Instrument the STARTED, SUBMITTED, and CANCELED events into your analytics to measure exactly where bettors drop. A high CANCELED rate after STARTED usually means a camera or lighting friction point; a high gap between SUBMITTED and an approved webhook points to document-quality issues worth addressing with better in-flow guidance.
Graceful Degradation
If the InContext iframe cannot load (corporate firewall, exotic browser), fall back to the JS SDK redirect using the same session URL. You already have the URL server-side, so the fallback is a one-line branch – and no bettor is ever stranded.
Beyond the Standard Coverage: What the Official Docs and GitHub README Miss
The top-ranking resources for “veriff sdk integration” are Veriff’s own developer docs (a ~23,000-word reference) and the npm package README. They are accurate but generic – they document the SDK in isolation, not a regulated sportsbook end to end. Here is what they leave out and this tutorial fills in.
No Sportsbook Compliance Mapping
The official SDK guide explains createVeriffFrame and the MESSAGES enum, but it never maps those mechanics to LCCP 17.1.1, the eliminated 72-hour grace period, or MGA’s risk-based expectations. A developer following the docs alone can ship a technically working iframe that is still non-compliant. The compliance-mapping table above is the missing bridge.
No Server-Authoritative Webhook Pattern
The README’s quick start ends at “render an iframe.” It does not warn that the client FINISHED event is not a decision, nor does it show HMAC verification with a constant-time, length-guarded comparison. That gap is exactly where insecure integrations are born – and where an auditor will push hardest.
No Complete, Runnable Project
Neither source ships a full Next.js App Router project with session, webhook, status, idempotency, and a decision-to-account-state machine in one place. This guide does, so you can lift the structure wholesale rather than stitching fragments from a dozen doc pages. It also situates Veriff within the broader stack – AML monitoring and geolocation – that the single-vendor docs never mention.
Frequently Asked Questions
What is the difference between the Veriff InContext SDK and the JS SDK?
The JS SDK (@veriff/js-sdk v2.0.0) creates a verification session and, in its simplest form, redirects the user to a Veriff-hosted page. The InContext SDK (@veriff/incontext-sdk v2.5.0) renders that same session inside an iframe modal on your own page via createVeriffFrame(), so the bettor never leaves your domain.
Which Veriff package versions should I pin in 2026?
As of mid-2026 the current releases are @veriff/[email protected] (published 8 August 2025) and @veriff/[email protected] (published 10 September 2025). Pin exact versions and test before bumping, since SDK changes can alter event behavior.
Can I unlock deposits when the client fires the FINISHED event?
No. The FINISHED event only means the UI flow ended and is forgeable from the browser. Unlock deposits only after your server receives and HMAC-verifies the decision webhook with a status of approved.
How does Veriff sign its webhooks?
Veriff computes an HMAC-SHA256 of the raw request body using your integration’s shared secret and sends it in the X-HMAC-SIGNATURE header. Recompute the HMAC over the untouched raw body and compare it in constant time.
Does Veriff handle full AML compliance for a sportsbook?
Only partly. Veriff provides AML screening (sanctions, PEP, adverse media) at onboarding but is not a transaction-monitoring engine. A complete program adds ongoing post-onboarding monitoring as a separate layer.
Does using Veriff make my sportsbook UKGC compliant automatically?
No tool grants automatic compliance. Veriff verifies identity, but you must enforce the controls: gate deposits server-side on a verified state, confirm 18+ from the returned DOB, keep audit logs, and meet LCCP 17.1.1 at onboarding.
What happens if a user denies camera access on iOS Safari?
The SDK emits a RELOAD_REQUEST event. Handle it by reloading the parent window so the browser re-prompts for camera permission; otherwise mobile users can stall mid-flow without any error.
How fast is a typical Veriff decision and how big is this market?
Veriff runs automated checks and returns most decisions via webhook within seconds of submission. The surrounding identity verification market is sizable – roughly USD 14.1 billion in 2026 per Future Market Insights, with KYC/KYB/onboarding the largest segment at about 43.2% share in 2025 and gaming among the fastest-growing verticals.







