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.
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 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
- Create the Veriff integration and copy the API key plus shared secret.
- Build the Node.js session endpoint.
- Sign the session request with HMAC-SHA256.
- Install
@veriff/[email protected]. - Configure iOS (CocoaPods, camera and NFC usage strings).
- Configure Android (Veriff Maven repo) or the Expo config plugin.
- Launch Veriff from the app with the session URL.
- Handle the in-app result (done, canceled, error).
- Verify the decision webhook signature.
- Map Veriff decisions to account states.
- Gate deposits and bets on approved status.
- 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.







