Veriff SDK Sportsbook KYC: 12-Step Integration Guide [2026]

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 Veriff for Sportsbook KYC in 2026

Online sportsbooks operate inside one of the tightest regulatory envelopes in consumer tech. Thirty-eight US states now license sports wagering, each with its own age-verification threshold, geolocation requirement, and self-exclusion registry. The UK Gambling Commission’s 2025 Affordability Checks rollout, Australia’s National Self-Exclusion Register (BetStop), and the EU’s revised Anti-Money Laundering Directive (AMLD6) have collectively raised the cost of a weak KYC pipeline from an inconvenience to a licence-threatening event.

Veriff entered 2026 as one of the most complete choices for regulated operators. As of May 20, 2026, its developer documentation added three new verification modalities–Selfie2Selfie Biometric Verification, Match Database Verification, and Metadata Database Verification–filling gaps that previously required third-party bolt-ons. Its SDK supports 12,000+ government-issued documents, covers 230+ countries and territories in 48 languages, and is independently certified to iBeta Level 2 Presentation Attack Detection (passive liveness). The internal benchmark Veriff cites for end-to-end verification is 6 seconds–inside the sub-30-second window that iGaming UX research says keeps registration abandonment below 15 percent.

This tutorial builds a production-grade Veriff Web SDK integration for a Node.js and React sportsbook. You will create sessions on the backend, render the hosted verification flow in your React app, receive and validate signed webhooks, map Veriff decision codes to player account states, add AML screening, and trigger re-verification at deposit thresholds. Every code block is runnable against Veriff’s staging environment before you flip to production keys.

Competitor Gap: What Generic KYC Guides Miss

Most 2026 KYC SDK articles on the web cover the generic happy path–create a session, redirect the user, handle the callback–and stop there. They omit sportsbook-specific requirements: state-by-state re-verification at deposit milestones, OFAC Specially Designated Nationals (SDN) list checks, webhook idempotency under high-concurrency registration spikes, and the Veriff SDK’s bi-weekly update cadence that can silently change frame dimensions if you pin a major version incorrectly. This guide addresses all of those gaps directly.

Prerequisites and Versions

  • Node.js 20 LTS – Veriff’s Node helper library uses native fetch; Node 18 works but Node 20 is the recommended minimum for 2026 deployments.
  • React 18.3+ or Next.js 14.2+ – The Veriff Web SDK injects an iframe; React 18’s concurrent renderer handles the mount timing cleanly.
  • Veriff Business or Enterprise account – Free-trial accounts have rate limits incompatible with load testing. Request sandbox credentials from your Veriff account manager.
  • npm 10+ or pnpm 9+ – package-lock integrity changed in npm 9; use npm 10 to avoid lockfile conflicts.
  • PostgreSQL 16 – Session and decision storage. The schema in Step 5 requires JSONB columns.
  • ngrok 3.x – Local webhook tunnelling during development. Create a free account at ngrok.com; the static domain feature (available on the free tier since 2025) gives you a stable URL without regenerating your Veriff webhook config each session.
  • dotenv 16 – Environment variable loading. Never hardcode API keys.

API Credential Locations

After logging into the Veriff Station, navigate to Integrations → API keys. You need two values: the VERIFF_API_KEY (public, used in session-creation requests) and the VERIFF_SECRET_KEY (private, used for HMAC-SHA256 webhook signature validation). Both are generated per environment–create separate pairs for staging and production. Store them in environment-specific .env files and never commit either to version control.

Step 1 – Create Your Veriff Account and Project

In Veriff Station (station.veriff.com), create a new Project for your sportsbook. Projects define which document types to accept, which countries to permit, and which data fields to return in the decision payload. For a US-licensed sportsbook, configure the following at minimum:

  • Document types: PASSPORT, DRIVERS_LICENSE, ID_CARD, RESIDENCE_PERMIT
  • Countries: All 50 US states plus territories if your licence covers them, plus any international markets
  • Data fields: firstName, lastName, dateOfBirth, documentNumber, address, idExpiry
  • Liveness: Required (Passive Level 2)
  • AML screening: Enabled (sanctions + PEP)

Under Webhooks, add your endpoint URL. In development this will be your ngrok static domain; in production it will be your hardened HTTPS endpoint. Veriff signs every webhook with HMAC-SHA256; record the secret shown at webhook creation–it is displayed once.

Step 2 – Install the Veriff Web SDK

Install the SDK and supporting packages:

npm install @veriff/js-sdk @veriff/incontext-sdk dotenv express pg crypto
# or with pnpm:
pnpm add @veriff/js-sdk @veriff/incontext-sdk dotenv express pg crypto

The @veriff/js-sdk package contains the redirect flow (opens Veriff in a new tab), while @veriff/incontext-sdk renders the verification in an iframe embedded in your page. For sportsbook registration, the in-context SDK delivers higher completion rates because the user never leaves your funnel. Veriff releases SDK updates on a two-week cadence; pin to a minor version in package.json to avoid unannounced UI changes breaking your layout:

{
  "dependencies": {
    "@veriff/incontext-sdk": "~2.9.0",
    "@veriff/js-sdk": "~2.9.0"
  }
}

Environment File

# .env.staging
VERIFF_API_KEY=sandbox_api_key_here
VERIFF_SECRET_KEY=sandbox_secret_key_here
VERIFF_BASE_URL=https://stationapi.veriff.com
DATABASE_URL=postgres://sportsbook:password@localhost:5432/sportsbook_staging
WEBHOOK_BASE_URL=https://your-static.ngrok-free.app

Step 3 – Build the Backend Session-Creation Endpoint

Veriff sessions are created server-side using your private API key. The client never touches the API key. Create src/routes/kyc.js:

// src/routes/kyc.js
import express from 'express';
import crypto from 'crypto';
import { pool } from '../db.js';

const router = express.Router();

const VERIFF_BASE_URL = process.env.VERIFF_BASE_URL;
const VERIFF_API_KEY  = process.env.VERIFF_API_KEY;
const VERIFF_SECRET   = process.env.VERIFF_SECRET_KEY;

// POST /api/kyc/session
// Called by the React frontend immediately after account creation.
router.post('/session', async (req, res) => {
  const { userId, firstName, lastName, dateOfBirth } = req.body;

  if (!userId || !firstName || !lastName || !dateOfBirth) {
    return res.status(400).json({ error: 'Missing required fields' });
  }

  // Check for an active in-progress session to avoid duplicate billing
  const existing = await pool.query(
    `SELECT session_id, session_url FROM veriff_sessions
     WHERE user_id = $1
       AND status = 'created'
       AND created_at > NOW() - INTERVAL '30 minutes'
     LIMIT 1`,
    [userId]
  );

  if (existing.rows.length > 0) {
    return res.json({
      sessionId: existing.rows[0].session_id,
      sessionUrl: existing.rows[0].session_url,
    });
  }

  // Create a new Veriff session
  const payload = {
    verification: {
      callback: `${process.env.WEBHOOK_BASE_URL}/api/kyc/webhook`,
      person: { firstName, lastName, dateOfBirth },
      document: { type: 'PASSPORT' },
      vendorData: String(userId), // echoed back in webhook payload
    },
  };

  const bodyStr = JSON.stringify(payload);
  // HMAC-SHA256 signature required by Veriff API
  const signature = crypto
    .createHmac('sha256', VERIFF_SECRET)
    .update(bodyStr)
    .digest('hex');

  const response = await fetch(`${VERIFF_BASE_URL}/v1/sessions/`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-AUTH-CLIENT': VERIFF_API_KEY,
      'X-HMAC-SIGNATURE': signature,
    },
    body: bodyStr,
  });

  if (!response.ok) {
    const err = await response.text();
    console.error('Veriff session creation failed:', err);
    return res.status(502).json({ error: 'Verification service unavailable' });
  }

  const data = await response.json();
  const { id: sessionId, url: sessionUrl } = data.verification;

  // Persist session for idempotency and audit trail
  await pool.query(
    `INSERT INTO veriff_sessions (session_id, user_id, session_url, status, created_at)
     VALUES ($1, $2, $3, 'created', NOW())
     ON CONFLICT (session_id) DO NOTHING`,
    [sessionId, userId, sessionUrl]
  );

  return res.json({ sessionId, sessionUrl });
});

export default router;

PostgreSQL Schema

-- migrations/001_veriff_sessions.sql
CREATE TABLE veriff_sessions (
  session_id   TEXT PRIMARY KEY,
  user_id      BIGINT NOT NULL,
  session_url  TEXT NOT NULL,
  status       TEXT NOT NULL DEFAULT 'created',
  decision     JSONB,
  created_at   TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  decided_at   TIMESTAMPTZ
);

CREATE INDEX idx_veriff_sessions_user ON veriff_sessions (user_id, status);
CREATE INDEX idx_veriff_sessions_created ON veriff_sessions (created_at);

Step 4 – Launch the Verification Flow in React

The in-context SDK renders an iframe inside a container element you provide. Create src/components/VeriffKYC.jsx:

// src/components/VeriffKYC.jsx
import { useEffect, useRef, useState } from 'react';
import { createVeriffFrame, MESSAGES } from '@veriff/incontext-sdk';

export default function VeriffKYC({ userId, firstName, lastName, dateOfBirth, onComplete }) {
  const containerRef = useRef(null);
  const frameRef     = useRef(null);
  const [loading, setLoading]   = useState(true);
  const [error,   setError]     = useState(null);

  useEffect(() => {
    let cancelled = false;

    async function initVeriff() {
      try {
        const res = await fetch('/api/kyc/session', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ userId, firstName, lastName, dateOfBirth }),
        });

        if (!res.ok) throw new Error('Failed to create verification session');
        const { sessionUrl } = await res.json();

        if (cancelled) return;

        frameRef.current = createVeriffFrame({
          url: sessionUrl,
          onEvent(msg) {
            switch (msg) {
              case MESSAGES.STARTED:
                setLoading(false);
                break;
              case MESSAGES.FINISHED:
                onComplete({ status: 'submitted' });
                break;
              case MESSAGES.CANCELED:
                onComplete({ status: 'canceled' });
                break;
              default:
                break;
            }
          },
        });
      } catch (err) {
        if (!cancelled) setError(err.message);
      }
    }

    initVeriff();

    return () => {
      cancelled = true;
      frameRef.current?.close();
    };
  }, [userId]); // re-init only if the user changes

  if (error) {
    return (
      <div className="kyc-error">
        <p>Unable to start identity verification. Please try again or contact support.</p>
      </div>
    );
  }

  return (
    <div className="kyc-wrapper">
      {loading && <div className="kyc-loading">Loading secure verification…</div>}
      <div ref={containerRef} id="veriff-mount" style={{ minHeight: 600 }} />
    </div>
  );
}

The MESSAGES enum includes STARTED, FINISHED, CANCELED, and RELOAD_REQUEST. Handle all four to avoid silent failures in your UI state machine. The RELOAD_REQUEST event means the SDK detected a browser capability issue and wants to open the flow in a new tab–call window.open(sessionUrl) in that handler rather than leaving the user stranded.

If you are evaluating multiple verification providers and want to benchmark Veriff against the Sumsub workflow covered in our Sumsub KYC API for iGaming integration guide, the session-creation patterns are nearly identical but the webhook schema and decision codes differ substantially–plan your abstraction layer accordingly.

Step 5 – Set Up the Webhook Receiver

Veriff pushes decision events to your webhook URL within seconds of a verification completing. The endpoint must:

  1. Return HTTP 200 immediately (within 5 seconds) before doing any heavy processing
  2. Validate the HMAC-SHA256 signature before acting on any payload
  3. Be idempotent–Veriff will retry delivery on non-200 responses
// src/routes/webhook.js
import express from 'express';
import crypto from 'crypto';
import { pool } from '../db.js';
import { processVeriffDecision } from '../services/kycService.js';

const router = express.Router();

// Use raw body parser for signature validation
router.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
  // Respond 200 first – Veriff retries on timeout
  res.status(200).json({ received: true });

  // Validate signature asynchronously
  const sigHeader   = req.headers['x-hmac-signature'];
  const rawBody     = req.body; // Buffer
  const expected    = crypto
    .createHmac('sha256', process.env.VERIFF_SECRET_KEY)
    .update(rawBody)
    .digest('hex');

  if (!sigHeader || sigHeader !== expected) {
    console.warn('Veriff webhook: invalid signature, discarding');
    return;
  }

  const event = JSON.parse(rawBody.toString());

  // Idempotency: skip if already processed
  const { rows } = await pool.query(
    'SELECT id FROM veriff_webhook_log WHERE event_id = $1',
    [event.id]
  );
  if (rows.length > 0) return;

  await pool.query(
    `INSERT INTO veriff_webhook_log (event_id, payload, received_at)
     VALUES ($1, $2, NOW())`,
    [event.id, JSON.stringify(event)]
  );

  await processVeriffDecision(event);
});

export default router;

Webhook Log Table

-- migrations/002_veriff_webhook_log.sql
CREATE TABLE veriff_webhook_log (
  id          BIGSERIAL PRIMARY KEY,
  event_id    TEXT UNIQUE NOT NULL,
  payload     JSONB NOT NULL,
  received_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

Step 6 – Validate Webhook Signatures

Webhook signature validation is the single most important security step in this integration. Without it, any party who discovers your webhook URL can inject fake “approved” decisions and unlock betting accounts for unverified players–a direct regulatory violation under FinCEN’s Customer Identification Program rules and UKGC Social Responsibility Code provision 3.4.1.

Veriff sends the signature in the X-HMAC-SIGNATURE header as a hex-encoded HMAC-SHA256 of the raw request body computed with your webhook secret. The critical implementation detail is that you must read the raw byte buffer–not a parsed JSON object–because any whitespace normalisation by your JSON middleware will change the computed hash. That is why the webhook route uses express.raw() rather than express.json().

Use a timing-safe comparison function (crypto.timingSafeEqual in Node.js) to prevent timing-oracle attacks:

// Timing-safe signature comparison
function isValidSignature(rawBody, receivedSig, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  const expectedBuf = Buffer.from(expected, 'hex');
  const receivedBuf = Buffer.from(receivedSig || '', 'hex');

  if (expectedBuf.length !== receivedBuf.length) return false;

  return crypto.timingSafeEqual(expectedBuf, receivedBuf);
}

Step 7 – Map Decision Codes to Player Account States

Veriff’s webhook payload includes a verification.status field and a verification.reason code. Your sportsbook account state machine must respond to each combination correctly. The table below maps the most common combinations to recommended actions:

Veriff Status Reason Code Sportsbook Action Regulatory Note
approved – Activate account, enable deposits up to tier-1 limit Store document expiry; schedule re-verification 30 days before expiry
declined 1001 Document expired Block deposits; prompt user to re-submit with valid document Log reason; UKGC requires you to record why verification failed
declined 1002 Document not supported Request alternative document type Maintain a list of accepted document types in your T&Cs
declined 1006 Suspected identity fraud Suspend account; escalate to compliance team File SAR with FinCEN / submit to UKGC if warranted
declined 1007 Person underage Permanent account block; refund any deposits immediately Mandatory in all jurisdictions; UKGC fines for underage access are unlimited
resubmission_requested – Notify user; limit account to free-to-play until resolved Allow up to 72 hours before escalating to manual review
review – Hold account in pending state; Veriff human reviewer is assessing Typical review SLA: <24 h business hours
expired – Generate a fresh session; notify user the previous session timed out Sessions expire after 7 days by default; configure in Veriff Station

The processVeriffDecision service function implements these mappings. For players evaluating their options between providers, a direct cost benchmark is available in our iGaming KYC provider comparison for 2026–Veriff sits at a higher per-check price point than Sumsub but lower than Jumio for high-volume sportsbook use cases.

Step 8 – Add AML Screening

In regulated sports-betting markets, KYC and AML are not the same thing. KYC confirms identity; AML screening checks that identity against OFAC’s SDN list, FATF high-risk jurisdictions, Politically Exposed Persons (PEP) databases, and adverse media feeds. Veriff’s optional AML module runs this screening automatically when enabled in your project settings, returning results in the same webhook payload under the amlAlerts key.

If your AML results require more granular control–for example, you want to apply different risk thresholds for deposits above $5,000 versus deposits above $10,000–you can call Veriff’s AML endpoint directly with the personId returned in the initial decision:

// src/services/amlService.js
export async function runAMLCheck(personId) {
  const res = await fetch(
    `${process.env.VERIFF_BASE_URL}/v2/persons/${personId}/aml-alerts`,
    {
      headers: {
        'X-AUTH-CLIENT': process.env.VERIFF_API_KEY,
        'Authorization': `Bearer ${process.env.VERIFF_SECRET_KEY}`,
      },
    }
  );

  if (!res.ok) {
    throw new Error(`AML check failed: ${res.status} ${await res.text()}`);
  }

  const { alerts } = await res.json();

  const highRisk = alerts.filter(a =>
    a.matchType === 'SANCTIONS' || a.matchType === 'PEP_CLASS_1'
  );

  return {
    clear:    alerts.length === 0,
    highRisk: highRisk.length > 0,
    alerts,
  };
}

Call runAMLCheck inside processVeriffDecision whenever the verification status is approved. If highRisk is true, route the account to your compliance queue rather than auto-activating it. Document every alert and the action taken–FATF Recommendation 12 requires records to be kept for at least five years.

AML Screening Cost and Frequency

Trigger Veriff AML Module Typical Interval Regulatory Basis
Account creation Automatic (included in KYC check) Once at registration UKGC LCCP, FinCEN CIP
First deposit Manual API call recommended Per event EU AMLD6 Art. 14
Cumulative deposits >$10,000/month Re-screen via Ongoing Monitoring Monthly FinCEN SAR thresholds
Periodic refresh Continuous Monitoring add-on Quarterly for PEP, annually for standard MGA PMDR, UKGC AML Policy

Step 9 – Configure Liveness Detection

Veriff’s passive liveness detection is certified to iBeta Level 2 PAD (Presentation Attack Detection), the highest commercially available biometric liveness standard. For sportsbooks, this matters because bonus-abuse syndicates routinely attempt to register multiple accounts using printed photos or pre-recorded video loops.

Passive liveness is the default when you enable Video Verification in your Veriff project settings. No additional SDK configuration is required on your end–the Veriff SDK captures a brief face video during the document-scan step and runs server-side liveness analysis. The result appears in the webhook payload as verification.person.liveness:

// Inside processVeriffDecision
const { decision, verification } = event;

if (verification?.person?.liveness?.status === 'FAILED') {
  // Liveness check did not pass – treat as declined
  await pool.query(
    `UPDATE veriff_sessions SET status = 'declined', decision = $1 WHERE session_id = $2`,
    [JSON.stringify(event), verification.id]
  );
  await notifyComplianceTeam(verification.vendorData, 'LIVENESS_FAIL');
  return;
}

Veriff also collects device fingerprint and network metadata during the verification session–browser user-agent, IP address, TLS fingerprint, and device sensor data. This intelligence is returned in the technicalData block of the decision webhook and can be fed into your internal risk-scoring model to flag accounts where the device profile changes between registration and first deposit, a common fraud indicator.

Step 10 – Implement Re-Verification at Deposit Thresholds

Regulators increasingly treat KYC as a continuous obligation rather than a one-time signup gate. The UK Gambling Commission’s 2025 strengthened LCCP requires operators to re-verify customer identity when cumulative net deposits reach £2,000 in a rolling 90-day window for customers who have not already completed enhanced due diligence. Similar thresholds apply in New Jersey (AML filing requirements above $3,000 single transaction), Malta (MGA PMDR), and Australia (AUSTRAC). For broader market context, see our coverage of best sweepstakes casino sites.

Implement re-verification triggers as a database-driven rule engine rather than hardcoded thresholds, so compliance teams can adjust limits without code deployments:

// src/services/depositGuard.js
export async function checkReVerificationRequired(userId, depositAmount) {
  const { rows } = await pool.query(
    `SELECT
       SUM(amount)     AS rolling_90d,
       MAX(kyc_tier)   AS current_tier
     FROM player_deposits
     WHERE user_id = $1
       AND created_at > NOW() - INTERVAL '90 days'`,
    [userId]
  );

  const rolling90d   = parseFloat(rows[0]?.rolling_90d || 0);
  const currentTier  = rows[0]?.current_tier || 'tier1';
  const newTotal     = rolling90d + depositAmount;

  const { rows: rules } = await pool.query(
    `SELECT threshold, required_tier FROM kyc_deposit_rules
     WHERE jurisdiction = (SELECT jurisdiction FROM players WHERE id = $1)
     ORDER BY threshold ASC`,
    [userId]
  );

  for (const rule of rules) {
    if (newTotal >= rule.threshold && currentTier !== rule.required_tier) {
      return { required: true, requiredTier: rule.required_tier };
    }
  }

  return { required: false };
}

When required: true, trigger a new Veriff session (Step 3) with a vendorData value that includes reason: DEPOSIT_THRESHOLD so you can distinguish threshold-triggered re-verifications from initial registrations in your audit logs. Block the deposit until the new session returns an approved decision.

For React iGaming frontend implementations that need to embed the re-verification modal without disrupting an active checkout flow, the patterns in our React iGaming verification setup guide are directly transferable to the Veriff in-context SDK.

Step 11 – Sandbox Testing

Veriff’s staging environment (stationapi.veriff.com with sandbox API keys) accepts synthetic test cases via the Veriff Station web interface. Use the Magic Values feature–specific first-name strings that trigger predetermined outcomes without requiring a real document scan:

Magic First Name Triggered Outcome Use Case
Approved status: approved Happy-path account activation
Declined status: declined, reason 1001 Document expired flow
Resubmission status: resubmission_requested Retry flow and email notification
Review status: review Pending state, compliance queue
Expired status: expired Session timeout regeneration

Run your full end-to-end test suite against all five magic values before requesting production keys. Write integration tests that assert:

  • The correct player account state after each decision type
  • Webhook idempotency: delivering the same event twice should not change state twice
  • Signature rejection: a tampered webhook body should be discarded silently
  • Session reuse: a second call to POST /api/kyc/session within 30 minutes should return the existing session URL, not create a billable new one

Load Testing the Webhook Endpoint

Sports-betting registration spikes happen at predictable moments: when a major tournament opens markets, when a promotional free-bet offer goes live, and when a competing operator suffers an outage and their players migrate to you. Use k6 or Artillery to simulate 500 concurrent webhook deliveries and verify your endpoint returns 200 inside 5 seconds under load. Veriff’s retry policy delivers failed webhooks at 1 min, 5 min, 30 min, and 2 h intervals–a slow webhook will trigger four additional deliveries per event, multiplying load during a spike.

Step 12 – Production Go-Live Checklist

Before switching to production API keys, complete every item on this checklist:

  1. Swap environment variables – Replace all staging values with production equivalents. Audit every .env file and CI/CD secret store.
  2. Update webhook URL in Veriff Station – Point to your production HTTPS endpoint. Verify TLS certificate is valid and not self-signed.
  3. Enable rate-limit headers – Veriff’s API returns X-RateLimit-Remaining and X-RateLimit-Reset. Log them; alert if Remaining drops below 100.
  4. Configure session expiry – Default is 7 days. For sportsbooks, 72 hours is a better UX balance–long enough for a player to come back and complete verification, short enough to limit open-session cost exposure.
  5. Enable AML Continuous Monitoring – This Veriff add-on automatically re-screens your active player base when sanctions lists update, without requiring a new verification session.
  6. Test magic values against production project – Veriff’s magic values work in production sandbox mode; confirm all decision flows work end-to-end.
  7. Verify document retention policy – Under GDPR Art. 17 and US state privacy laws, you must be able to delete verification data on request. Veriff’s data retention settings in Station control how long Veriff stores raw images; configure these to match your data processing agreement.
  8. Confirm iBeta Level 2 liveness is active – Check Project Settings → Liveness shows Passive Mode (iBeta Level 2), not the legacy active-liveness option.
  9. Set up monitoring alerts – Alert on >5% of sessions entering declined or resubmission_requested state within any 1-hour window; this pattern indicates a configuration issue (e.g., a document type was inadvertently excluded from your project settings).
  10. Brief your support team – Provide agents with the Veriff session ID lookup process in Station so they can pull a verification audit trail for customer disputes without needing database access.
  11. Register with self-exclusion registries – GamStop (UK), BetStop (AU), and state-level ISRS (US) must be queried before or alongside KYC, not after. If your Veriff decision returns approved but the self-exclusion check returns a match, the account must not be activated.
  12. Complete a penetration test of the webhook endpoint – At minimum, verify that the endpoint rejects: missing X-HMAC-SIGNATURE headers, malformed JSON, excessively large payloads (>1 MB), and replayed events (use the event ID idempotency check from Step 5).

5 Critical Pitfalls to Avoid

Pitfall 1: Using express.json() on the Webhook Route

The most common integration mistake. express.json() parses the body and may normalise whitespace, changing the HMAC computation. Use express.raw({ type: 'application/json' }) on the webhook route specifically, and parse with JSON.parse(rawBody.toString()) after signature validation.

Pitfall 2: Pinning to a Major SDK Version

Veriff ships SDK updates every two weeks. Major version locks prevent you from receiving security patches. Conversely, @veriff/incontext-sdk: "*" can pull in a breaking change on your next npm install. Pin to the minor version (~2.9.0) and schedule a monthly dependency-review task to apply minor updates deliberately.

Pitfall 3: Ignoring the RELOAD_REQUEST Message

Safari on iOS 16 and earlier, and some WebView environments inside native sportsbook apps, cannot render the in-context iframe reliably. Veriff sends MESSAGES.RELOAD_REQUEST in these cases. If you do not handle this message, the user sees a blank box and abandons the flow. The handler must open the sessionUrl in a new tab or switch to the redirect-flow SDK.

Pitfall 4: Not Storing vendorData Before Session Creation

Veriff echoes vendorData back in every webhook event. This is your only reliable way to map a Veriff session back to a player account without a database lookup, which matters when your webhook handler runs in a stateless Lambda function. Store the player’s internal user ID as vendorData, not an email address (PII minimisation).

Pitfall 5: Treating review as approved

The review status means a Veriff human agent is assessing the session–it is NOT a positive outcome. Accounts in review status must remain in a restricted state (free-to-play only, no deposit) until a final approved or declined webhook arrives. Operators that auto-activate on review have failed UKGC audits for enabling deposits before KYC completion.

Troubleshooting: 8 Common Issues

1. Webhook signature mismatch (400 Bad Request from your endpoint)

Check that your endpoint reads the raw byte buffer, not a JSON-parsed object. Also verify you are using the webhook secret (separate from the API secret) shown in Veriff Station → Webhooks. A common mistake is using the API secret key for webhook validation–they are different values. For broader market context, see our coverage of leading online casino sites.

2. Session creation returns 401 Unauthorized

The HMAC signature for session creation uses the request body string, not the raw URL. Ensure you are signing JSON.stringify(payload) with the same encoding Node uses to send the request body. If you use Axios, note that Axios serialises JSON slightly differently from JSON.stringify in some edge cases–use the fetch API directly as shown in Step 3.

3. In-context iframe renders blank

Veriff’s iframe requires allow="camera; microphone" permissions on the parent frame. If your React app is embedded inside a native app’s WebView, the host app must grant camera permissions to the WebView. For Safari on iOS, the iframe must be inside a user-initiated gesture handler–do not auto-open it on page load.

4. Webhook events arrive but decisions are never processed

You responded 200 but then threw an unhandled exception in the async processing code. Because the 200 response already went out, Veriff considers the webhook delivered and will not retry. Wrap the entire processing block in a try/catch and log all errors to a persistent error table so you can replay missed events.

5. Magic values not triggering in sandbox

Magic values are case-sensitive and must match exactly (Approved, not approved). They also require the project to be in Test mode in Veriff Station. If your project is set to Live mode with sandbox API keys, magic values are ignored and Veriff will attempt real document analysis.

6. AML alerts not appearing in the webhook payload

AML screening must be explicitly enabled per-project in Veriff Station → Project Settings → Add-ons. It is not active by default even if your contract includes it. After enabling, sessions created before the change do not retroactively include AML data; you must create fresh sessions.

7. Duplicate approved webhooks activating accounts twice

Veriff guarantees at-least-once delivery, not exactly-once. Your veriff_webhook_log idempotency table (Step 5) prevents double-processing, but only if you check it before updating account state. If your processing pipeline fans out to multiple async workers (e.g., a message queue), ensure only one worker processes each event ID.

8. Player completes verification but account stays in pending state

The most common cause is a mismatch between the session’s vendorData and the expected format in processVeriffDecision. Add structured logging at the decision-processing step that outputs vendorData, decision.status, and the resulting account state update. Also verify your webhook endpoint URL in Veriff Station exactly matches your deployed URL–a trailing-slash mismatch will cause Veriff to return 404s and stop delivering events.

Beyond Standard Coverage: What Competitors Don’t Address

Most KYC integration articles in 2026 stop at the happy path: create session, launch SDK, handle approval. The gaps they leave are exactly where regulated sportsbook operators get fined. This section covers the areas absent from the top-ranking competitors for this keyword.

State-by-State Deposit Re-Verification Thresholds

US sportsbook operators serving multiple state markets face a patchwork of re-verification triggers that generic KYC guides ignore entirely. New Jersey’s Division of Gaming Enforcement (NJDGE) requires enhanced due diligence for cumulative transactions above $3,000 in any 24-hour period. Pennsylvania’s Gaming Control Board uses a $5,000 threshold. Colorado and Virginia have no specific thresholds but require operators to demonstrate a risk-based AML program that covers large-transaction monitoring. The database-driven rule engine in Step 10 is designed specifically for this multi-jurisdiction complexity.

Veriff SDK Bi-Weekly Update Management

Competitor articles recommend installing the latest SDK version without discussing the operational risk. Veriff’s two-week release cadence means that an unpinned dependency can change the iframe dimensions, alter the liveness frame sequence, or modify the event message schema between your CI runs and your production deployment. The minor-version pinning strategy in Step 2 and the scheduled monthly dependency-review task in Step 12 close this gap.

Selfie2Selfie and Match Database Verification (May 2026)

Veriff’s documentation index, updated May 20, 2026, added three new verification modalities that no competitor article covers yet: Selfie2Selfie Biometric Verification (match a new selfie against a previously verified document photo), Match Database Verification (check a document against Veriff’s proprietary fraud registry), and Metadata Database Verification (cross-reference document metadata against issuer databases). For sportsbooks with existing player databases, Selfie2Selfie enables re-verification without requiring players to re-submit documents–a significant UX improvement for deposit-threshold checks in Step 10.

Device and Network Intelligence in Risk Scoring

Veriff’s SDKs collect device fingerprint, IP geolocation, browser canvas fingerprint, and TLS fingerprint data during the verification session. This data appears in the technicalData block of the decision webhook and is almost universally ignored by integration guides. For sportsbooks, correlating the device fingerprint at KYC time against the device fingerprint at first deposit is a powerful fraud signal: a mismatch suggests the verified person and the depositing person are not the same, a common money-laundering pattern in the sports-betting context.

Advanced Tips for Production Operations

Async Decision Processing with a Message Queue

For high-traffic sportsbooks processing thousands of registrations per hour, synchronous database writes inside the webhook handler create a bottleneck. Move the processing logic to a Redis Stream or a managed queue (AWS SQS, Google Pub/Sub) by publishing the validated webhook payload and processing it in a separate consumer. Keep the idempotency check in the webhook handler itself, before enqueueing, to avoid flooding the queue with duplicates.

Document Expiry Re-Verification Scheduling

Veriff returns document.validUntil in the approved decision payload. Store this date and schedule a re-verification job to run 30 days before expiry. Players whose documents expire without re-verification should be downgraded to a restricted account state automatically–UKGC compliance auditors have specifically checked for expired-document handling in recent operator audits.

Webhook Replay for Missed Events

Veriff provides a session lookup API at GET /v1/sessions/{sessionId} that returns the current decision state for any session. If your webhook endpoint was down during a delivery window, use this endpoint to backfill decisions for sessions in created state that are older than 24 hours. Run this as a scheduled job every hour in production.

GDPR Right-to-Erasure Handling

When a player submits a right-to-erasure request, you must delete verification records from both your database and Veriff’s storage. Call DELETE /v1/persons/{personId} in Veriff’s API to delete their stored media. Log the deletion with a timestamp and the player’s internal ID (not PII) for your compliance audit trail. This is a step that generic KYC guides entirely omit.

Veriff SDK Capability Matrix: Web vs iOS vs Android

Feature Web SDK (incontext) iOS SDK Android SDK
iBeta Level 2 Liveness Yes Yes Yes
NFC Chip Reading No Yes (iOS 13+) Yes (Android 9+)
Selfie2Selfie (May 2026) Yes Yes Yes
Device Intelligence Collection Partial (browser only) Full (sensor data) Full (sensor data)
In-Context / In-App Flow iframe embed Native UIViewController Native Fragment/Activity
Update Cadence Every 2 weeks Every 2 weeks Every 2 weeks
Offline Document Scan No Yes (queued upload) Yes (queued upload)
AML Screening Via webhook (server-side) Via webhook (server-side) Via webhook (server-side)

For sportsbooks that operate native iOS and Android apps alongside a web product, the native SDKs deliver higher verification completion rates because they access the device’s native camera stack directly, avoiding the browser permission-request flow that causes 8–12% of mobile web users to abandon. NFC chip reading (available on iOS 13+ and Android 9+) allows passport verification that is cryptographically unforgeable–relevant for sportsbooks that accept players from high-risk jurisdictions where document forgery rates are elevated.

Frequently Asked Questions

How long does a Veriff verification take for a sportsbook player?

Veriff’s internal benchmark for automated decisions is 6 seconds end-to-end. The vast majority of verifications complete within this window. Sessions routed to human review (the review status) have an SLA of under 24 business hours. For your signup funnel, communicate to the player that verification typically completes “in seconds” and provide a clear status page so they know where they are in the process.

What document types does Veriff accept for US sportsbook players?

Veriff supports 12,000+ document types across 230+ countries. For US players, the most common accepted documents are state-issued driver’s licenses, state ID cards, US passports, and US Passport Cards. Your Veriff project configuration controls which document types are accepted–configure this in Veriff Station to match your sportsbook’s Terms and Conditions and jurisdiction-specific requirements.

Is Veriff compliant with US state sportsbook regulations?

Veriff is a tool that enables KYC compliance–it is not itself a licensed KYC provider under US gaming law. Your sportsbook’s compliance with NJDGE, PGCB, CGCB, or other state regulators depends on how you configure Veriff’s output (age verification, document validation, AML screening) and how you integrate it with self-exclusion registries and responsible-gambling controls. Veriff’s iBeta Level 2 liveness certification is accepted by most US state regulators as sufficient for biometric age-verification requirements.

How does Veriff handle failed verifications for underage players?

When Veriff’s automated system or a human reviewer determines the player is underage, the webhook delivers status: declined, reason: 1007. Your integration must respond by permanently blocking the account (not just restricting it) and immediately refunding any funds that were deposited before verification completed. Under UKGC Social Responsibility Code and all US licensed-state requirements, underage gambling is a zero-tolerance breach–there is no appeal process for the operator.

Can I use Veriff for ongoing player re-verification without the player re-submitting documents?

Yes–Veriff’s Selfie2Selfie modality (added May 2026) allows you to verify that a player performing a re-verification selfie matches the document photo from their initial KYC session, without requiring them to re-present their document. This is the recommended approach for deposit-threshold re-verification triggers because it has a significantly higher completion rate than asking players to re-scan documents.

What is the difference between the Veriff Web SDK and the API integration?

The Web SDK (@veriff/incontext-sdk or @veriff/js-sdk) handles the front-end capture flow–the camera UI, document scanning frame, and liveness animation. The API (REST) handles session creation, decision retrieval, and AML screening. In a production sportsbook integration, you use both: the API on your backend to create sessions and receive webhook decisions, and the SDK on your frontend to render the verification experience. You cannot bypass the SDK and send document images directly via the API on a standard commercial plan.

How do I handle players who abandon the verification flow mid-session?

Veriff marks sessions as expired after 7 days (configurable). When a player returns to complete a previously abandoned verification, check for an active session in your database (the idempotency logic in Step 3 handles this) and return the existing session URL rather than creating a new billable session. If the session has expired, create a fresh one. Send a re-engagement email after 24 hours of inactivity with a direct link to resume verification–operators who implement this recover 20–35% of abandoned verifications.

Does Veriff support real-time OFAC sanctions screening?

Veriff’s AML module includes sanctions screening against the OFAC SDN list, FATF-listed jurisdictions, and EU/UN consolidated sanctions lists. Screening runs at session decision time and returns results in the amlAlerts array in the webhook payload. For operators who need continuous monitoring between KYC events–for example, when a player’s name appears on an SDN list six months after registration–Veriff offers a Continuous Monitoring add-on that rescreens your active player base whenever the OFAC list is updated (typically weekly).

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