Veriff React Native SDK: Mobile Sportsbook KYC, 12 Steps [2026]

Mobile is where US sports betting actually happens. The DraftKings and FanDuel apps process the overwhelming majority of legal wagers, and every one of those accounts has to clear identity verification before the first dollar moves. If you are building a native sportsbook app in React Native, embedding a browser-based identity widget is the wrong tool: it fights the navigation stack, mishandles the camera, and breaks NFC passport reads. The right tool is the native Veriff React Native SDK, which wraps Veriff’s iOS and Android verification flows behind a single JavaScript call.

This tutorial walks through a production-grade integration of @veriff/react-native-sdk version 13.2.0 (published June 15, 2026) for a real-money sportsbook, in 12 concrete steps and roughly 35 minutes of hands-on work. You will build a Node.js session backend, wire the native SDK into iOS and Android, verify HMAC-signed decision webhooks, and gate deposits and bets on verified status – all mapped to the requirements of US state regulators. It is the mobile companion to our web build with the Veriff InContext SDK for browser sportsbooks.

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 Mobile Sportsbooks Need a Native Veriff Flow in 2026

A sportsbook KYC flow is not a sign-up nicety – it is a regulatory gate. Under every state’s online gaming rules, an operator must confirm a player’s identity and age before accepting a deposit or a bet. On the web you can drop Veriff’s InContext modal into the page and be done. On a native app, the constraints are different and harsher: you are inside a compiled binary, the camera is a native resource, and Apple and Google both scrutinize how identity data is captured.

Where the web SDK falls short on a native app

Loading Veriff’s JavaScript or InContext SDK inside a React Native WebView technically works, but it degrades the experience in ways that hurt conversion. The WebView negotiates camera permissions separately from the host app, autofocus and torch control are unreliable, and the NFC passport read – which is what drops manual-review rates from under 5% to under 2% – is simply unavailable to web code on most devices. The native SDK talks directly to the platform camera and NFC stack, so the capture quality that drives Veriff’s reported 99.6% decision accuracy is actually reachable on a phone.

What you are going to build

By the end you will have three components working together: a Node.js endpoint that mints Veriff sessions with your secret key, a React Native screen that launches the native verification flow with one launchVeriff call, and a webhook listener that validates Veriff’s signed decision and flips the player’s account from pending to approved. Deposits and bets stay blocked until that webhook says the player is who they claim to be.

The 12 steps at a glance

  1. Create the Veriff integration and copy the API key plus shared secret.
  2. Build the Node.js session endpoint.
  3. Sign the session request with HMAC-SHA256.
  4. Install @veriff/[email protected].
  5. Configure iOS (CocoaPods, camera and NFC usage strings).
  6. Configure Android (Veriff Maven repo) or the Expo config plugin.
  7. Launch Veriff from the app with the session URL.
  8. Handle the in-app result (done, canceled, error).
  9. Verify the decision webhook signature.
  10. Map Veriff decisions to account states.
  11. Gate deposits and bets on approved status.
  12. Handle re-verification, resubmission, and source-of-funds triggers.

Veriff React Native SDK 13.2.0: What’s New and How It Differs

Veriff ships several SDKs, and choosing the wrong one is the most common early mistake. The React Native package is a thin JavaScript bridge over Veriff’s compiled native iOS and Android SDKs – it is not the same code path as the browser SDKs. That distinction matters for permissions, app-store review, and the capture quality you can achieve.

The Veriff SDK family, by version

All three JavaScript-facing packages are verified against the public npm registry as of June 2026. The React Native package is the freshest of the set, reflecting active mobile investment.

Package Latest version Released Use case
@veriff/react-native-sdk 13.2.0 Jun 15, 2026 Native iOS + Android apps (this guide)
@veriff/incontext-sdk 2.5.0 Aug 8, 2025 Embedded browser modal (web)
@veriff/js-sdk 2.0.0 Sep 10, 2025 Drop-in web form / redirect

What the 13.x line requires

Version 13.2.0 declares peer dependencies of react ^19.1.0 and react-native ^0.81.0. If your app is still on React 18 or an older React Native, the install will warn and the native build can fail at link time. The package ships with no runtime JavaScript dependencies – the weight is in the bundled native artifacts pulled from Veriff’s Maven and CocoaPods feeds.

The two-key model

Every Veriff integration has two credentials: a public API key (sent as the X-AUTH-CLIENT header) and a shared secret that signs every request and verifies every webhook. The golden rule for a mobile build: the shared secret never ships in the app binary. The app only ever receives a short-lived session URL minted by your backend. Anyone who decompiles your APK or IPA should find no Veriff secret at all.

Prerequisites: Versions, Accounts, and API Keys

Pin your toolchain before you write a line of code. Mismatched React Native, Xcode, or Android SDK versions cause the majority of “it builds on my machine” failures in a native KYC integration.

Local toolchain

Tool Minimum version Notes
Node.js 20 LTS Backend session + webhook service
React Native 0.81.0 Peer dependency of the SDK
React 19.1.0 Peer dependency of the SDK
Xcode 16 iOS 14+ deployment target
Android compileSdk 34 minSdk 24 or higher
CocoaPods 1.15+ Resolves the native iOS pod

Veriff dashboard setup (Step 1)

In Veriff Station, create an integration of type Self-service. Under the integration’s settings, copy the API key and the shared secret key. Set the decision webhook URL to a server endpoint you control – for example https://api.yoursportsbook.com/kyc/webhook. Veriff signs both API responses and webhooks with the secret, so treat it like a database password: store it in your secrets manager, not in source control.

Backend assumptions

The examples use Express 5 on Node 20, but the logic ports to any framework. You need exactly two routes: one authenticated route that mints a session for a logged-in player, and one public route that receives Veriff’s signed webhook. Keep them in the same service so they share the secret and your player database.

Architecture: Backend Sessions, On-Device Capture, Webhook Decisions

The flow has a strict separation of duties. Your backend owns the secret and talks to Veriff’s REST API. The device owns the camera and runs the native capture flow. Veriff owns the decision and pushes it back to your backend asynchronously. Getting this separation right is what keeps you both secure and compliant.

The end-to-end sequence

The session lifecycle is deterministic: your app asks your backend for a session; your backend calls POST https://stationapi.veriff.com/v1/sessions with an HMAC-signed body and gets back a verification.url; your backend returns only that URL to the device; the SDK opens the URL and guides the user through document and selfie capture; Veriff runs its checks and posts a signed decision to your webhook; your backend flips the account state. The in-app result tells you the user finished capturing – the webhook tells you whether they passed.

App                 Your backend            Veriff
 | POST /kyc/session     |                     |
 |---------------------->| POST /v1/sessions   |
 |                       | (X-HMAC-SIGNATURE)  |
 |                       |-------------------->|
 |                       |   verification.url  |
 |     { sessionUrl }    |<--------------------|
 |<----------------------|                     |
 | launchVeriff(url)     |                     |
 |------ capture document + selfie + NFC ----->|
 |     statusDone        |                     |
 |<----------------------|                     |
 |                       | POST /kyc/webhook   |
 |                       | (signed decision)   |
 |                       |<--------------------|
 |                       | 200 OK              |
 |                       |-------------------->|
 |  account: approved    |                     |
 |<----------------------|                     |

Why decisions come by webhook, not by return value

A frequent rookie error is treating statusDone as “approved.” It is not. statusDone only means the user submitted their images. The actual approve/decline decision arrives seconds later via the webhook, after Veriff runs document authentication, face matching, liveness, and sanctions screening. Build your account-state machine around the webhook; use the in-app result purely for UX (“Thanks, we’re reviewing your documents”).

Idempotency and matching

Each session carries a vendorData field you set to your internal player ID. Veriff echoes it back in the webhook, so you can match the decision to the player without exposing your database keys in the URL. Store the returned sessionId against the player when you mint the session, and treat repeat webhooks for the same session ID as idempotent – Veriff retries on any non-2xx response.

Step 2–3: Build the Node.js Session Backend

The backend’s first job is minting sessions. This is the only place your shared secret is used to sign an outgoing request. The session body is small: a callback URL, your player ID as vendorData, and a timestamp. Everything else (which documents to accept, branding, supported countries) is configured on the integration in Veriff Station.

The signing helper and the create-session route

HMAC signing is identical in both directions: hex-encoded HMAC-SHA256 of the exact payload string, keyed with the shared secret. Sign the serialized body you actually send – if you re-stringify the object, key order can change and the signature will not match.

// server/kycSession.js – Node.js 20+, Express 5
import express from 'express';
import crypto from 'node:crypto';

const VERIFF_BASE = 'https://stationapi.veriff.com';
const API_KEY = process.env.VERIFF_API_KEY;        // public: X-AUTH-CLIENT
const SECRET  = process.env.VERIFF_SHARED_SECRET;  // private: signs everything

// hex HMAC-SHA256 of a string payload, keyed with the shared secret
function sign(payload) {
  return crypto.createHmac('sha256', SECRET).update(payload, 'utf8').digest('hex');
}

const router = express.Router();

router.post('/kyc/session', requireAuth, async (req, res) => {
  const userId = req.user.id;                       // your internal player id

  // Build the body ONCE and sign exactly these bytes
  const body = JSON.stringify({
    verification: {
      callback: 'https://api.yoursportsbook.com/kyc/webhook',
      vendorData: String(userId),                   // echoed back in the webhook
      timestamp: new Date().toISOString(),
    },
  });

  const veriffRes = await fetch(`${VERIFF_BASE}/v1/sessions`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-AUTH-CLIENT': API_KEY,
      'X-HMAC-SIGNATURE': sign(body),
    },
    body,
  });

  const data = await veriffRes.json();
  if (data.status !== 'success') {
    return res.status(502).json({ error: 'veriff_session_failed', detail: data });
  }

  await db.kyc.upsert({ userId, sessionId: data.verification.id, status: 'created' });

  // ONLY the session URL crosses the wire to the device
  res.json({ sessionUrl: data.verification.url });
});

export default router;

Expected output

A successful call returns a compact JSON object to the device. The session URL is single-use and time-limited:

{
  "sessionUrl": "https://alchemy.veriff.com/v/eyJhbGciOiJIUzI1Ni␣..."
}

On Veriff’s side, the response body is also signed, so a defensive backend can verify the X-HMAC-SIGNATURE on the response before trusting it. At minimum, check that data.status === 'success' and that a verification.url is present before returning anything to the app.

Step 4–6: Install and Wire the React Native SDK

With the backend minting sessions, install the SDK and configure each platform. The package re-exports a single native module, so the JavaScript surface is tiny – the work is in the native build settings.

Install (Step 4)

# React Native 0.81+, React 19.1+
npm install @veriff/[email protected]

# iOS: resolve the native pod
cd ios && pod install && cd ..

iOS configuration (Step 5)

Veriff needs the camera always and the NFC reader for passport chips. Both require usage-description strings in Info.plist, or iOS will crash the flow the moment it requests the resource. App Store review also rejects builds that access the camera without a clear purpose string.

<!-- ios/YourSportsbook/Info.plist -->
<key>NSCameraUsageDescription</key>
<string>We use the camera to verify your identity, as required for legal betting.</string>
<key>NFCReaderUsageDescription</key>
<string>We read your passport chip to speed up identity verification.</string>

Android configuration (Step 6)

The native Android artifacts live in Veriff’s own Maven repository, so add it to the project-level build.gradle alongside mavenCentral():

// android/build.gradle (project level)
allprojects {
  repositories {
    google()
    mavenCentral()
    maven { url 'https://cdn.veriff.me/android/' } // Veriff native artifacts
  }
}

If you use Expo with the managed workflow, skip the manual Gradle edit. The package ships an Expo config plugin that injects the Maven URL during prebuild – just register it in app.json and supply your iOS strings:

{
  "expo": {
    "plugins": ["@veriff/react-native-sdk"],
    "ios": {
      "infoPlist": {
        "NSCameraUsageDescription": "We use the camera to verify your identity for legal betting.",
        "NFCReaderUsageDescription": "We read your passport chip to speed up verification."
      }
    }
  }
}

Then run npx expo prebuild to generate the native projects with Veriff already wired in. This is the cleanest path for teams that do not want to maintain hand-edited native folders.

Step 7–8: Launch Verification and Handle the Result In-App

Now the payoff: a single screen that fetches a session URL from your backend and hands it to the native SDK. The SDK takes over the screen, runs the full capture flow, and resolves a promise with a status. Your job is to translate that status into UX and never into a final compliance decision.

The verification screen (Steps 7 and 8)

// src/screens/VerifyIdentity.tsx – React 19 + RN 0.81
import React, { useState } from 'react';
import { View, Button, Text, Alert } from 'react-native';
import VeriffSdk from '@veriff/react-native-sdk';

const API = 'https://api.yoursportsbook.com';

export default function VerifyIdentity({ authToken }: { authToken: string }) {
  const [busy, setBusy] = useState(false);
  const [outcome, setOutcome] = useState<string | null>(null);

  async function startKyc() {
    setBusy(true);
    try {
      // 1) Ask OUR backend to mint a Veriff session (secret stays server-side)
      const r = await fetch(`${API}/kyc/session`, {
        method: 'POST',
        headers: { Authorization: `Bearer ${authToken}` },
      });
      const { sessionUrl } = await r.json();

      // 2) Hand the URL to the native SDK – it opens camera/NFC capture
      const result = await VeriffSdk.launchVeriff({ sessionUrl });

      switch (result.status) {
        case VeriffSdk.statusDone:
          // Images submitted – NOT a final decision. Wait for the webhook.
          setOutcome('submitted');
          break;
        case VeriffSdk.statusCanceled:
          setOutcome('canceled');
          break;
        case VeriffSdk.statusError:
          setOutcome('error');
          Alert.alert('Verification error', result.error ?? 'Unknown error');
          break;
      }
    } finally {
      setBusy(false);
    }
  }

  return (
    <View>
      <Button title="Verify my identity" onPress={startKyc} disabled={busy} />
      {outcome === 'submitted' && <Text>Thanks – we're reviewing your documents.</Text>}
      {outcome === 'canceled' && <Text>Verification canceled. You can try again anytime.</Text>}
    </View>
  );
}

Reading the result correctly

The SDK resolves with one of three statuses: statusDone, statusCanceled, or statusError. Only statusError carries an error string worth logging. Crucially, none of these three is an approval. A user can reach statusDone and still be declined a moment later for a tampered document. Your screen should move the player to a neutral “under review” state and let the webhook drive the rest.

Step 9–10: Verify Webhook Decisions with HMAC-SHA256

The webhook is the security-critical part of the whole integration. If you trust an unsigned POST to /kyc/webhook, anyone who learns the URL can mark themselves approved and start depositing. Veriff signs every webhook with your shared secret, and you must verify that signature against the raw request bytes before believing a word of the payload. For broader market context, see our coverage of best sweepstakes casino sites.

Verify the signature against the raw body (Step 9)

The single most common webhook bug is verifying against re-serialized JSON. Express’s JSON parser reorders nothing, but it does drop the exact byte sequence Veriff signed. Capture the raw buffer for this route only, then compute the hex HMAC-SHA256 and compare in constant time.

// server/kycWebhook.js – verify BEFORE parsing
import express from 'express';
import crypto from 'node:crypto';

const SECRET  = process.env.VERIFF_SHARED_SECRET;
const API_KEY = process.env.VERIFF_API_KEY;
const router  = express.Router();

// express.raw gives us the exact bytes Veriff signed
router.post('/kyc/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.get('x-hmac-signature') || '';
  const client    = req.get('x-auth-client') || '';

  const expected = crypto.createHmac('sha256', SECRET)
    .update(req.body)            // req.body is a Buffer here
    .digest('hex');

  const sigOk = signature.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));

  if (client !== API_KEY || !sigOk) {
    return res.status(401).send('bad signature');
  }

  const event = JSON.parse(req.body.toString('utf8'));
  const v = event.verification;          // { id, status, vendorData, code, ... }
  handleDecision(v.id, v.status, v.vendorData);

  res.sendStatus(200);                   // ACK fast – Veriff retries non-2xx
});

export default router;

Map decisions to account states (Step 10)

Veriff’s verification.status is a small enum. Translate it once, in one place, so your sportsbook never has to interpret raw Veriff strings deeper in the codebase.

// server/decision.js
async function handleDecision(sessionId, status, vendorData) {
  const userId = vendorData;
  const map = {
    approved:               'approved',  // unlock deposits + betting
    declined:               'blocked',   // hard fail – route to AML review
    resubmission_requested: 'resubmit',  // ask the player to recapture
    expired:                'expired',
    abandoned:              'expired',
    review:                 'pending',   // Veriff escalated to a human analyst
  };
  const state = map[status] ?? 'pending';
  await db.kyc.upsert({ userId, sessionId, status: state, decidedAt: new Date() });

  if (status === 'resubmission_requested') notifyPlayerToRetry(userId);
  if (status === 'declined') flagForAmlReview(userId, sessionId);
}
Veriff status Account state Sportsbook action
approved approved Unlock deposits, withdrawals, betting
resubmission_requested resubmit Prompt a new capture; keep funds locked
declined blocked Block account; open AML/SAR review
review pending Hold; wait for the human decision webhook
expired / abandoned expired Re-issue a session on next login

Step 11–12: Gate Deposits, Bets, and Re-Verification

Verification is worthless if it does not actually block money movement. The compliance requirement is explicit: no deposit and no wager until identity and age are confirmed. Enforce it server-side with middleware on every money path, and never rely on the app to hide a button.

Deposit and bet gating (Step 11)

// server/requireVerified.js
const ALLOWED = new Set(['approved']);

export async function requireVerified(req, res, next) {
  const kyc = await db.kyc.findByUser(req.user.id);
  if (!kyc || !ALLOWED.has(kyc.status)) {
    return res.status(403).json({
      error: 'kyc_required',
      state: kyc?.status ?? 'none',
      message: 'Identity verification must be approved before depositing or betting.',
    });
  }
  next();
}

app.post('/wallet/deposit', requireVerified, depositHandler);
app.post('/bets',           requireVerified, placeBetHandler);
app.post('/wallet/withdraw', requireVerified, withdrawHandler);

This pairs naturally with location enforcement: a player must be both verified and physically inside a legal state. If you have not wired the geofence yet, our walkthrough of GeoComply geolocation gating in production slots in next to this middleware as a second mandatory check before any bet is accepted.

Event-driven re-verification (Step 12)

KYC is not one-and-done. State AML programs expect ongoing monitoring, and Veriff supports re-running a session when risk signals fire. Trigger a fresh session – minted by the exact same backend route – on events such as a large single or cumulative deposit, an unusually fast withdrawal, a device or location change, or a chargeback spike. For high-value triggers, escalate to a source-of-funds check rather than a simple re-capture, and keep the account in resubmit until the new decision clears.

US Sportsbook Compliance: Mapping Veriff to State Regulators

Veriff handles the identity science; you handle the regulatory mapping. US online sports betting is licensed state by state, and each regulator publishes its own identity and AML expectations. The verification primitives are the same everywhere – confirm a real person, confirm their age, screen them against sanctions lists – but the documentation you must retain differs.

State-by-state requirements

State Regulator Core KYC expectation
New Jersey Division of Gaming Enforcement (DGE) Identity + age verified before first deposit; geolocation on every bet
Pennsylvania Gaming Control Board (PGCB) Documented identity proofing; ongoing AML monitoring
Michigan Gaming Control Board (MGCB) Age and identity confirmation pre-play; retained verification records

Authoritative requirements live with the regulators themselves – see the New Jersey DGE, the Pennsylvania Gaming Control Board, and the Michigan Gaming Control Board. For the AML layer, screening obligations trace back to OFAC sanctions lists and the Bank Secrecy Act administered by FinCEN.

Where Veriff fits the requirement

Veriff’s session returns document authentication, face match, passive liveness (iBeta Level 2), and optional sanctions screening against OFAC, UN, EU, and HMT lists, with the screening step reportedly adding under 250 ms to the call. That single decision satisfies the “confirm a real person of legal age” obligation in every listed state. Age is derived from the document’s date of birth, so you do not need a separate age-gate vendor – but you must still record and retain the decision artifacts the regulator can audit. If you are still choosing a provider, our side-by-side of Sumsub, Onfido, Jumio, and Veriff for iGaming KYC breaks down accuracy and pricing across the field before you commit.

Retention and auditability

Store the Veriff sessionId, the final decision status, and the decision timestamp against every player, and keep them for the retention window your license demands (commonly five years after account closure). Do not store the raw document images yourself – Veriff is the system of record for the biometric data, which keeps your own breach surface smaller.

Testing and Going Live: A Pre-Launch Checklist

Identity verification is not something you debug in production with real players’ documents. Veriff supports a full staging workflow, and a regulated sportsbook should exercise every decision branch before a single live wager is possible. The reference quick-starts show the happy path and stop; your QA plan has to cover the unhappy ones, because those are the states your compliance team will be audited on.

Test in a staging integration first

Create a separate test integration in Veriff Station with its own API key and shared secret, and point your staging backend at those credentials. Run the flow on a physical device – the iOS Simulator and most Android emulators expose no usable camera, so capture silently fails there. Tunnel your local webhook over HTTPS so you receive real signed decisions against test data, and log the computed-versus-received signature on every call until you are confident the verification passes.

Simulate every decision status

Drive each branch of your decision map deliberately. Submit a clean document to get approved; submit a deliberately blurry or glare-heavy capture to trigger resubmission_requested; let a session time out to produce expired. For each, confirm the account lands in the right state and that the deposit endpoint returns 403 for everything except approved. A decision map that is never exercised is a decision map that is wrong.

The go-live checklist

  • Shared secret is loaded from a secrets manager, never hard-coded or shipped in the app.
  • Webhook route verifies the raw-body HMAC in constant time and returns 200 on success.
  • Every money path – deposit, withdraw, place bet – is behind requireVerified.
  • iOS usage strings for camera and NFC are present; Android Maven repo resolves.
  • Resubmission and re-verification flows are reachable from the app UI.
  • Decision records (session ID, status, timestamp) are persisted for your retention window.
  • Geolocation enforcement runs alongside KYC on every bet.
  • Production integration uses production keys, separate from staging.

Five Pitfalls That Break Mobile KYC in Production

These are the failures that pass QA and then surface at scale, when real users on real devices hit edge cases your test account never did. For broader market context, see our coverage of leading online casino sites.

1. Treating statusDone as approval

The most damaging mistake. If you unlock deposits when the SDK returns statusDone, you have unlocked the account before Veriff decided anything. A fraudster submits a doctored document, sees the success screen, and deposits while the decline is still in flight. Always gate on the webhook.

2. Shipping the shared secret in the app

If your app calls stationapi.veriff.com directly, the shared secret is in the binary, and a five-minute decompile exposes it. Mint every session on the backend. The device should never hold anything but a single-use session URL.

3. Verifying the webhook against parsed JSON

Re-stringifying req.body changes the bytes and your signature check fails – or worse, you disable the check to “fix” it. Use express.raw for the webhook route and HMAC the exact buffer Veriff sent.

4. Missing iOS usage strings

No NSCameraUsageDescription means an instant crash when the SDK opens the camera, and an App Store rejection on top of it. NFC needs its own string. Add both before your first TestFlight build.

5. Ignoring resubmission

A large share of first attempts come back as resubmission_requested – glare, a cropped document, a blurry selfie. If your app has no resubmit path, those legitimate players are stuck in limbo and churn. Handle the status explicitly and prompt a clean recapture.

Troubleshooting: 8 Errors and Their Fixes

When the integration misbehaves, the symptom is almost always one of these eight. Work top to bottom.

Symptom Likely cause Fix
Session create returns non-success HMAC over a different body than sent Sign the exact serialized string; do not re-stringify
Android build fails to resolve Veriff classes Maven repo missing Add cdn.veriff.me/android to project build.gradle or use the Expo plugin
iOS app crashes on launch flow Missing camera/NFC usage string Add NSCameraUsageDescription + NFCReaderUsageDescription
Webhook returns 401 from your own check Verifying parsed JSON, not raw bytes Use express.raw and HMAC the Buffer
launchVeriff rejects immediately Expired or reused session URL Mint a fresh session per attempt
Peer dependency warnings on install React < 19.1 or RN < 0.81 Upgrade to the SDK’s peer versions
No webhook ever arrives Callback URL unreachable or non-2xx Expose a public HTTPS endpoint; ACK with 200
Decision never matches a player vendorData not set on session Set vendorData to your player ID at create time

Reproducing webhook failures locally

Use a tunneling tool to expose your local webhook over HTTPS, then run a real session in a staging integration. Log the raw header values for x-auth-client and x-hmac-signature alongside your computed expected signature – a single mismatched character almost always means you parsed before verifying.

When Veriff escalates to manual review

The review status means a human analyst is looking. Do not poll the API for it; the final decision will arrive as a second webhook. Keep the account in pending and surface a neutral message to the player rather than implying failure.

Advanced Tips: Branding, NFC, Localization, and Conversion

Once the core loop works, these refinements lift completion rates and shrink your manual-review bill. Each one is optional, but together they are the difference between a flow that converts and one that bleeds users at the document step.

Cut manual review with NFC

Veriff reports manual-review rates under 5% for standard captures, dropping under 2% when the passport NFC chip is read and combined with biometric liveness. On a native app you actually have NFC access – so for passport holders, prompt the chip read. It is the single highest-leverage quality improvement available to a mobile build, and it directly reduces the per-verification cost of human review.

Branding and localization

The native flow can be themed to match your app and localized to the player’s language – Veriff’s flow supports dozens of UI languages and dark-mode theming. Pass branding and locale options through to launchVeriff per Veriff’s current SDK reference, and set them from the device locale so a Spanish-speaking player in Arizona sees the flow in Spanish. A flow that looks like a third-party pop-up converts worse than one that feels native to your brand.

Understand the pricing model

Veriff is priced per verification, reported in the range of roughly $1.50 to $3.00 depending on volume, with premium AML monitoring as a per-verification add-on (around $0.25 to $0.50). Because every resubmission can incur cost, quality matters to your unit economics: better capture means fewer retries means a lower effective price per approved player. NFC and good in-app guidance pay for themselves here.

Metric (Veriff-reported) Figure
Decision accuracy ~99.6% globally
Average decision time under 10 seconds
Manual-review rate (standard) under 5%
Manual-review rate (with NFC + liveness) under 2%
Supported document types 12,000+ across 230+ countries
Sanctions-screening latency under 250 ms added

Beyond the Reference Docs: What Veriff’s Guide Doesn’t Cover

Veriff’s developer documentation is excellent as a reference, but it is written for a generic identity use case, not a regulated US sportsbook. The official SDK guide, the JS SDK GitHub README, and the npm package page all show you how to call the SDK – none of them tells you how to build a compliant betting flow around it. Here is what you have to supply yourself.

The compliance wiring the docs omit

The reference never mentions deposit gating, age-before-play enforcement, retention windows, or the mapping from a Veriff status to a regulator-defensible account state. It shows launchVeriff and stops. The entire middle of this tutorial – the requireVerified middleware, the decision map, the state-regulator table – is the part that turns an SDK call into a licensable sportsbook, and it is exactly the part the docs leave to you.

The production hardening they skip

Official quick-starts routinely verify the webhook against parsed JSON, omit constant-time signature comparison, and never discuss idempotent retries, the difference between statusDone and an approval, or event-driven re-verification. Those omissions are harmless in a demo and dangerous in production. This guide treats the webhook as the security boundary it actually is.

The mobile-specific gaps

Because the docs are platform-agnostic, they gloss over the App Store usage-string rejections, the Veriff Maven repository that Android builds need, and the Expo config-plugin path. Those are the things that actually block a React Native release – and they get a single line in the reference, if any. Pair this with our web Veriff InContext build and you have both surfaces of a sportsbook covered.

Complete Project Example: Minimal Sportsbook KYC App

Here is how the pieces fit into a runnable shape. The backend is one Express app with two routes; the app is one screen plus your existing navigation. Everything below has appeared in full earlier – this is the assembly map.

Folder structure

sportsbook-kyc/
├── server/
│   ├── index.js            // wires routers, loads secrets from env
│   ├── kycSession.js       // POST /kyc/session  (Steps 2–3)
│   ├── kycWebhook.js       // POST /kyc/webhook   (Step 9)
│   ├── decision.js         // status -> account state (Step 10)
│   └── requireVerified.js  // money-path gate     (Step 11)
└── app/
    ├── App.tsx
    └── src/screens/VerifyIdentity.tsx  // launchVeriff (Steps 7–8)

Server entry point

// server/index.js
import express from 'express';
import session from './kycSession.js';
import webhook from './kycWebhook.js';

const app = express();

// IMPORTANT: mount the raw webhook BEFORE express.json()
app.use(webhook);
app.use(express.json());
app.use(session);

app.listen(8080, () => console.log('KYC service on :8080'));

Run it

Export VERIFF_API_KEY and VERIFF_SHARED_SECRET, start the server with node server/index.js, point your integration’s webhook at the public URL, and run the app on a device (the simulator has no camera). Tap Verify my identity, complete the capture, and watch the webhook flip the account to approved – at which point the deposit endpoint stops returning 403. That round trip, from button tap to unlocked wallet, is the whole integration.

Frequently Asked Questions

Is the Veriff React Native SDK the same as the InContext SDK?

No. @veriff/react-native-sdk (13.2.0) wraps Veriff’s native iOS and Android SDKs for compiled mobile apps, while @veriff/incontext-sdk (2.5.0) embeds a browser modal for web pages. Native apps should use the React Native package to get real camera and NFC access.

Does the shared secret ever go in the app?

Never. The app receives only a single-use session URL minted by your backend. The shared secret signs API requests and verifies webhooks server-side, and should live in a secrets manager – not in the app binary.

What does statusDone mean?

It means the user finished submitting their document and selfie – not that they passed. The approve or decline decision arrives seconds later by webhook. Gate deposits and bets on the webhook decision, never on statusDone.

How do I verify the webhook is really from Veriff?

Compute a hex HMAC-SHA256 of the raw request body using your shared secret and compare it in constant time to the X-HMAC-SIGNATURE header, and confirm X-AUTH-CLIENT equals your API key. Verify against the raw bytes, before parsing JSON.

Which React Native and React versions are required?

SDK 13.2.0 declares peer dependencies of React Native 0.81.0 and React 19.1.0 or newer. Older versions will throw peer-dependency warnings and can fail the native build.

Can I use it with Expo?

Yes. The package ships an Expo config plugin. Add "@veriff/react-native-sdk" to your app.json plugins, supply the iOS usage strings, and run npx expo prebuild – the plugin injects the Veriff Maven repository for Android automatically.

What does Veriff cost per verification?

Veriff is priced per verification, reported in the roughly $1.50–$3.00 range depending on volume, with premium AML monitoring as an add-on of about $0.25–$0.50 each. Reducing resubmissions with NFC and good capture guidance lowers your effective cost per approved player.

Does this satisfy US state KYC rules on its own?

The Veriff decision satisfies the identity and age-confirmation requirement, but you must also enforce geolocation, retain the decision records your license requires, and run AML screening and ongoing monitoring. Veriff is the identity layer of a compliant stack, not the entire stack.

This tutorial reflects package versions and Veriff-reported figures verified as of June 30, 2026. Always confirm current SDK versions on npm and current requirements with your state regulator before going live.

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