Identity fraud is no longer a back-office nuisance for online gambling operators – it is the single fastest-moving operational risk of 2026. Sumsub’s Identity Fraud Report 2025–2026, built on more than four million analysed fraud attempts, found that while the average identity fraud rate actually fell to 2.2%, the most sophisticated attacks jumped 180% and deepfake-driven fraud surged a staggering 1,100%. For a licensed casino or sportsbook, every one of those attempts that slips through is a chargeback, a money-mule account, or a regulatory penalty waiting to happen.
This tutorial is a production-grade walkthrough for engineering teams: 14 steps, roughly three hours of build time, and a complete Node.js + React microservice that delivers a production Sumsub KYC API integration for an iGaming platform the way a real compliance stack needs it – signed requests, a verification gate, securely verified webhooks, AML screening, idempotency, and a reconciliation job. It is deliberately deeper than a quickstart. If you only need the bare 12-step happy path, our condensed Sumsub integration guide covers that. Here we build the parts that survive contact with production traffic and an auditor.
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 Sumsub Owns the iGaming KYC Conversation in 2026
Sumsub was ranked the No.1 iGaming Fraud Prevention Provider in Gambling IQ’s 2026 industry report, and the reason is structural: gambling is now the most heavily targeted vertical for AI-assisted identity fraud, and a single SDK that fuses document verification, liveness, AML screening, and transaction monitoring is far easier to defend in front of a regulator than four bolted-together point tools. Sumsub serves more than 4,000 clients and verifies identities in 240+ countries and territories, which matters when your player base is global and your licence is not.
The 2026 fraud numbers operators cannot ignore
The headline statistics frame the entire build. The table below is drawn directly from Sumsub’s 2025–2026 report and is the business case you will hand to your compliance lead when they ask why the integration deserves engineering time this quarter.
| Metric (Sumsub Identity Fraud Report 2025–2026) | Figure |
|---|---|
| Clients served | 4,000+ |
| Fraud attempts analysed (2024–2025) | 4,000,000+ |
| Global identity fraud rate, 2025 | 2.2% (2024: 2.6%, 2023: 2.0%) |
| Rise in the most sophisticated fraud attempts | +180% |
| Deepfake fraud growth | +1,100% |
| Synthetic identity-document fraud growth | +300% |
| Largest regional deepfake spike (Maldives) | +2,100% YoY |
Where Sumsub fits in the player lifecycle
KYC is not a one-time gate. A modern operator runs identity verification at registration, re-runs liveness on risky logins, screens against watchlists continuously, and monitors transactions for AML patterns. Sumsub exposes each of these as a distinct product – User Verification, Liveness & Deepfake Detection, Watchlists and PEPs, Transaction Monitoring, and Travel Rule – but they share one applicant object, so the integration you build now becomes the spine for everything that follows.
That single-applicant model is also why Sumsub’s 2026 platform additions matter to engineers, not just compliance officers. Workflow Orchestration lets you branch the verification path by country, risk score, or product – a new player in a high-risk market can be routed to enhanced due diligence automatically, while a low-risk returning user breezes through in seconds. Case Management and the Summy AI Copilot give your review team a single console for the applicantOnHold cases your code escalates, and Risk Scoring assigns a live risk value you can read back through the same API. You are not integrating a document checker; you are integrating the backbone of a fraud-and-compliance operation, which is exactly why getting the applicant object and its webhooks right at the very start pays dividends for years.
The compliance clock you are coding against
The reason this is urgent in 2026 is regulatory. Great Britain’s Gambling Commission tightened its rules so that verification must complete before a customer deposits or bets, and the old grace period is gone. Build the gate wrong and you are not merely exposed to fraud – you are non-compliant from the first deposit.
The Regulatory Map: Verify Before Deposit, or Don’t Take the Bet
Before a single line of code, you need to know when verification has to succeed, because that timing dictates your architecture. The answer differs by market, and getting it wrong is the most expensive mistake in this entire build.
UKGC, MGA, Mexico and Curaçao at a glance
| Regulator / Market | When ID must be verified | Age rule | Key 2026 note |
|---|---|---|---|
| UKGC (Great Britain) | Before first deposit, bet, or access to any gambling product | 18+ confirmed pre-play | LCCP Condition 17.1.1; 72-hour grace period removed; AI-generated documents explicitly flagged |
| MGA (Malta) | Identity established at onboarding under a risk-based AML framework | 18+ | More operator discretion, but more internal judgement required |
| Mexico (2026) | At onboarding, with source-of-funds checks for higher tiers | 18+ | Sumsub’s 2026 Mexico iGaming KYC guide flags age plus ongoing AML monitoring |
| Curaçao (LOK) | At registration under the National Ordinance on Games of Chance | 18+ | The 2024 LOK reform tightened operator KYC/AML duties and direct licensing |
The UKGC rule that shapes your code
Under LCCP Condition 17.1.1, a licensee must establish a customer’s identity – name, address and date of birth at minimum – and confirm they are 18 or older before that customer is permitted to gamble. Practically, this means your deposit and bet endpoints must check a server-side verification flag that only turns true after Sumsub returns a positive review. A client-side “looks verified” signal is not enough.
Why the risk-based markets are not easier
The Malta Gaming Authority’s risk-based approach looks more forgiving, but it shifts the burden onto your own documented risk assessment. You still need the same technical plumbing; you simply gain discretion over when to escalate to enhanced due diligence. Either way, the integration below – gate, webhook, AML screening, audit trail – is the common denominator across every licence you are likely to hold.
Architecture: The Verification Microservice You Are Building
The integration is a small, self-contained service that sits between your player platform and Sumsub. Keeping it isolated means your monolith never holds Sumsub secrets, your verification logic is independently deployable, and your auditor has one place to look. The service does four things: it signs and sends API calls, it mints short-lived SDK tokens for the browser, it receives and verifies webhooks, and it exposes a single source-of-truth verification status to the rest of your stack.
Components and data flow
The flow is deliberately linear. A new player triggers createApplicant on your backend. Your frontend requests a short-lived access token and launches the WebSDK, which handles document capture and liveness entirely inside Sumsub’s iframe – your servers never touch raw passport images, which keeps your GDPR data-minimisation posture clean. When Sumsub finishes its review it fires a webhook to your service; you verify the signature, update the player’s status, and only then unlock deposits.
Keep the trust boundary crisp: the browser holds only a short-lived access token, your microservice holds the Secret Key and the webhook secret, and your core platform holds neither – it simply reads a verification flag the microservice owns. That separation means a compromise of your public web tier never leaks Sumsub credentials, and it gives auditors one place to look for every identity decision. It also makes the service trivially horizontal to scale: the API calls are stateless, and the only shared state is the idempotency set and your applicant-status table.
The applicant lifecycle and review answers
Every applicant moves through a small state machine you must model in your own database. The key terminal signal is reviewResult.reviewAnswer: GREEN means approved, RED means rejected. A RED answer carries a reviewRejectType of either RETRY (the user can resubmit – often a blurry photo) or FINAL (permanent, do not let them back in). Conflating those two is one of the most common and most damaging integration bugs, because treating a RETRY as FINAL locks out paying customers.
Sandbox versus production tokens
Sumsub issues separate App Tokens for sandbox and production, prefixed sbx: and prd: respectively. The entire build below runs against sandbox first. Treat the two environments as fully separate deployments with separate secrets, separate webhook endpoints, and separate verification levels – drift between them is a pitfall we return to later.
Prerequisites and Exact Versions
Pin your toolchain. The Sumsub API itself is stable, but the surrounding stack is where version surprises bite. This is the environment the example project was built and tested against.
Accounts, tokens and a verification level
- A Sumsub account with dashboard access (start in the sandbox environment).
- An App Token and Secret Key generated under Dev Space → App Tokens.
- At least one verification level defined in Workflow Orchestration – for example
basic-kyc-levelrequiring an identity document plus a liveness selfie. - A webhook secret configured under the Webhook Manager so deliveries are signed.
Backend dependencies
- Node.js 20 LTS (or 22 LTS) – the example uses only the built-in
cryptomodule plus Express. - Express 4.19+ for routing and the raw-body webhook parser.
- axios 1.7+ as the HTTP client (any client works; axios keeps the example compact).
- A datastore for applicant status – Postgres 16 in the example; Redis 7 for the idempotency set.
Frontend dependencies
- React 18.3+.
@sumsub/websdk-react2.x – the official React wrapper around the WebSDK (the vanilla@sumsub/websdkpackage is the alternative for non-React frontends).
Steps 1–3: Credentials, Verification Levels and the Signing Layer
Everything starts in the Sumsub dashboard. These two steps are clicks, not code, but they define the contract your code depends on, so do them carefully and write the values into your environment configuration immediately.
Step 1 – Generate the App Token and Secret Key
In the sandbox dashboard, open Dev Space → App Tokens and create a token. You receive two values: the App Token (sent on every request) and the Secret Key (shown once – used to sign requests and never transmitted). Store both as environment variables. Never commit them, and never ship the Secret Key to the browser.
# .env (sandbox)
SUMSUB_APP_TOKEN=sbx:xxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxx
SUMSUB_SECRET_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
SUMSUB_LEVEL_NAME=basic-kyc-level
SUMSUB_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxx
Step 2 – Build a verification level in Workflow Orchestration
A “level” is the named set of checks a user must pass. In Workflow Orchestration, create a level – call it basic-kyc-level – that requires an identity document (passport, national ID, driver’s licence, or residence permit) plus a liveness selfie. For higher-risk markets you can branch the workflow to demand proof of address or source-of-funds, and since June 2026 Sumsub also lets you insert paid Questionnaire steps directly into the flow to capture structured risk-assessment answers – billed from first use, so budget for it if you switch it on. The level name is the single string that ties your backend, your access tokens, and your dashboard configuration together, so keep it identical across all three.
Step 3 – Build the HMAC signing layer
Every Sumsub API call is authenticated with three headers: X-App-Token, X-App-Access-Ts (a Unix timestamp in seconds), and X-App-Access-Sig (an HMAC-SHA256 signature). The signature is computed over the exact string ts + HTTP_METHOD + path + body using your Secret Key, hex-encoded. Get this layer right once and every other call becomes trivial.
How the signature string is assembled
The subtle part is that the signed body must byte-for-byte match what actually goes on the wire. If your HTTP client re-serialises the JSON after you sign it, the signature breaks and you get a 401 that looks like a credential problem but is not. The client below signs the exact serialised payload and then disables any further transformation.
// sumsubClient.js
const crypto = require('crypto');
const axios = require('axios');
const BASE_URL = 'https://api.sumsub.com';
const APP_TOKEN = process.env.SUMSUB_APP_TOKEN;
const SECRET_KEY = process.env.SUMSUB_SECRET_KEY;
async function signedRequest(method, path, body = null) {
const ts = Math.floor(Date.now() / 1000);
const payload = body ? JSON.stringify(body) : '';
const signature = crypto
.createHmac('sha256', SECRET_KEY)
.update(ts + method.toUpperCase() + path + payload)
.digest('hex');
const res = await axios({
method,
url: BASE_URL + path,
data: payload || undefined,
transformRequest: [(d) => d], // do NOT let axios re-stringify
headers: {
'X-App-Token': APP_TOKEN,
'X-App-Access-Ts': ts,
'X-App-Access-Sig': signature,
'Content-Type': 'application/json',
},
});
return res.data;
}
module.exports = { signedRequest };
A reusable signed-request client
This single signedRequest function is the foundation for the rest of the tutorial. Notice it signs path including any query string – so /resources/applicants?levelName=basic-kyc-level is signed in full. A frequent mistake is signing the path without its query parameters; the request then fails verification on Sumsub’s side.
Steps 4–6: Applicants, Access Tokens and the WebSDK
With signing in place, the two core API calls are short. An applicant is Sumsub’s record of one person; an access token is a short-lived credential the browser uses to launch the WebSDK without ever seeing your Secret Key. If your platform already authenticates players through Auth0, note that Sumsub launched a native KYC integration with Auth0 on 29 May 2026, embedding identity verification directly into the Auth0 login flow – worth evaluating before you hand-roll the token plumbing below if your stack is already standardised on it.
Step 4 – POST /resources/applicants
Create the applicant when a user registers on your platform, tying it to your own user identifier via externalUserId. That field is your join key forever after – make it your stable internal user ID, not an email that can change. Since June 2026, Sumsub also exposes a dedicated Check Applicant Existence by externalUserId method, so instead of firing a blind createApplicant call and catching the resulting 409, you can check first and branch cleanly between creating a new applicant and reusing an existing one.
// applicants.js
const { signedRequest } = require('./sumsubClient');
const LEVEL = process.env.SUMSUB_LEVEL_NAME;
async function createApplicant(externalUserId, email, phone) {
const path = `/resources/applicants?levelName=${encodeURIComponent(LEVEL)}`;
return signedRequest('POST', path, {
externalUserId,
email,
phone,
type: 'individual',
});
}
async function createAccessToken(externalUserId, ttlInSecs = 600) {
const path =
`/resources/accessTokens?userId=${encodeURIComponent(externalUserId)}` +
`&levelName=${encodeURIComponent(LEVEL)}&ttlInSecs=${ttlInSecs}`;
const data = await signedRequest('POST', path);
return data.token; // '_act-...'
}
module.exports = { createApplicant, createAccessToken };
Expected output from createApplicant is a JSON applicant object whose id you store against your user:
{
"id": "65f3c1e2a4b1c20001a2b3c4",
"createdAt": "2026-06-25 09:12:44",
"externalUserId": "user_10293",
"review": { "reviewStatus": "init" },
"type": "individual"
}
Step 5 – POST /resources/accessTokens
The access token is intentionally short-lived (600 seconds is a sensible default). Your frontend asks for a fresh one each time it launches or refreshes the SDK; you never cache it server-side beyond the request that created it. The returned token looks like _act-sbx-xxxx... and is safe to hand to the browser.
Step 6 – Embed the WebSDK in a React frontend
The browser side is mostly Sumsub’s responsibility. The WebSDK renders the document-capture and liveness flow inside a sandboxed iframe, so your application never handles biometric data directly. Your job is to supply an access token and to refresh it when it expires.
The verification gate component
The official @sumsub/websdk-react component takes an accessToken plus an expirationHandler callback that returns a fresh token whenever the current one lapses. Wire the handler to a small backend endpoint that calls createAccessToken. For broader market context, see our coverage of best sweepstakes casino sites.
// VerificationGate.jsx
import { useEffect, useState } from 'react';
import SumsubWebSdk from '@sumsub/websdk-react';
export default function VerificationGate({ userId }) {
const [token, setToken] = useState(null);
const fetchToken = async () => {
const res = await fetch('/api/kyc/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId }),
});
const data = await res.json();
return data.token;
};
useEffect(() => { fetchToken().then(setToken); }, [userId]);
if (!token) return <p>Loading verification…</p>;
return (
<SumsubWebSdk
accessToken={token}
expirationHandler={fetchToken}
config={{ lang: 'en', theme: 'dark' }}
options={{ adaptIframeHeight: true }}
onMessage={(type, payload) => console.log('sns:', type, payload)}
onError={(error) => console.error('sns error:', error)}
/>
);
}
Token refresh and expiration handling
Because every call to fetchToken mints a brand-new token, an expired token never blocks a user mid-flow – the SDK silently swaps it. The most common failure here is an expirationHandler that returns the same stale token; always issue a fresh one with a TTL of at least 600 seconds.
Reusable KYC and Non-Doc Verification
Two 2026 features dramatically cut friction and cost, though Reusable KYC’s own roadmap moved fast this year. Reusable KYC lets a user who has already verified elsewhere on the Sumsub network confirm with a tap instead of re-uploading documents, and Sumsub kept extending it through the first half of the year – expanding the API/SDK in March 2026 to reuse more applicant data steps during verification, then adding a new sourceKey parameter for applicant creation in May 2026 to support more complex integrations. As of August 2026, though, Sumsub has marked Reusable KYC via API as legacy, moving it to sustaining support only with no further feature development planned, so treat it as a stable building block rather than one still actively evolving. Non-Doc Verification checks a user against authoritative government and credit databases in supported countries without any document upload at all – faster, cheaper, and far harder to deepfake. Both are toggled at the workflow level, so enabling them is a dashboard change, not a code change.
Steps 7–9: Webhooks, Signatures and AML Screening
This is the step most tutorials get dangerously wrong. The webhook is your only trustworthy signal that verification succeeded. The WebSDK’s client-side success message is a UX convenience and can be spoofed; the server-to-server webhook, with its signature, is the source of truth that gates real money.
Step 7 – verify the x-payload-digest signature
Sumsub signs each webhook with an HMAC of the raw request body, sent in the x-payload-digest header. The algorithm is named in x-payload-digest-alg (for example HMAC_SHA256_HEX). You must compute the HMAC over the raw bytes – if your framework parses the JSON first, re-serialising it will change the bytes and the signature will never match.
// webhook.js
const crypto = require('crypto');
const express = require('express');
const router = express.Router();
const SECRET = process.env.SUMSUB_WEBHOOK_SECRET;
const ALGO = { HMAC_SHA1_HEX: 'sha1', HMAC_SHA256_HEX: 'sha256', HMAC_SHA512_HEX: 'sha512' };
router.post('/sumsub',
express.raw({ type: 'application/json' }), // keep the raw Buffer
(req, res) => {
const alg = ALGO[req.get('x-payload-digest-alg')] || 'sha256';
const expected = crypto.createHmac(alg, SECRET).update(req.body).digest('hex');
const received = Buffer.from(req.get('x-payload-digest') || '', 'utf8');
const expectedBuf = Buffer.from(expected, 'utf8');
if (received.length !== expectedBuf.length ||
!crypto.timingSafeEqual(expectedBuf, received)) {
return res.status(401).send('invalid signature');
}
const event = JSON.parse(req.body.toString('utf8'));
enqueue(event); // process async; ack fast
res.status(200).send('ok');
}
);
module.exports = router;
Step 8 – handle applicantReviewed and gate the account
The webhook type values you will handle most are applicantCreated, applicantPending, applicantOnHold, and the decisive applicantReviewed. A representative applicantReviewed payload looks like this:
{
"applicantId": "65f3c1e2a4b1c20001a2b3c4",
"inspectionId": "65f3c1e2a4b1c20001a2b3c5",
"correlationId": "req-7f1c9e",
"externalUserId": "user_10293",
"type": "applicantReviewed",
"reviewResult": {
"reviewAnswer": "GREEN",
"reviewRejectType": null
},
"reviewStatus": "completed",
"createdAtMs": "2026-06-25 09:18:02.114"
}
Only a GREEN answer should flip your internal kyc_verified flag to true and unlock deposits. A RED with RETRY should prompt the user to resubmit; a RED with FINAL should block the account and, depending on your AML policy, trigger a suspicious-activity review. This branching is exactly where many operators leak both revenue (false locks) and risk (false unlocks).
Mapping Sumsub statuses to player account states
The cleanest integrations keep an explicit translation table from Sumsub signals to internal account states, so the gating logic lives in one place rather than scattered across deposit, withdrawal, and bet endpoints. This is the exact mapping the example service implements, and it is worth pinning to a wall during go-live testing.
| Sumsub signal | reviewAnswer / reject type | Player account state | Deposit allowed? |
|---|---|---|---|
applicantCreated |
– | onboarding | No |
applicantPending |
– | pending | No |
applicantOnHold |
– | manual_review | No |
applicantReviewed |
GREEN | verified | Yes |
applicantReviewed |
RED + RETRY | resubmit_required | No |
applicantReviewed |
RED + FINAL | blocked | No |
Step 9 – AML screening, Travel Rule and ongoing monitoring
Verifying identity once is table stakes; staying compliant means screening that identity against sanctions and PEP lists continuously and watching the money move. Sumsub bundles these as Watchlists and PEPs, Transaction Monitoring, and Travel Rule, all keyed off the same applicant you already created.
Watchlists, PEP and adverse-media screening
When you build your verification level, enable the AML screening check so that every applicant is matched against global sanctions, PEP, and adverse-media databases at onboarding and re-screened on a schedule. Since an April 2026 partnership with ComplyAdvantage, that screening is powered in part by the ComplyAdvantage Mesh network inside Sumsub’s KYC/KYB APIs, widening the underlying watchlist and adverse-media coverage without requiring any change on your side of the integration. A hit still surfaces as an applicantOnHold webhook, which should route the case to a human reviewer rather than auto-approving. For operators who want the full monitoring build – Travel Rule messaging, threshold rules, and case management – our deep dive on real-time AML transaction monitoring with Sumsub’s Travel Rule picks up exactly where this section ends.
Configure the screening cadence deliberately. A daily re-screen against updated sanctions and PEP lists is the common baseline for licensed operators, because a player who was clean at onboarding can appear on a watchlist weeks later. Sumsub re-runs the match automatically and fires a fresh applicantOnHold event if a new hit surfaces, which means your only job is to ensure that event reliably pauses withdrawals and routes the case to a human reviewer. Treat a missed re-screen webhook with the same seriousness as a missed onboarding decision – both are reportable AML control failures, not merely bugs in a queue.
Travel Rule for crypto cashiers
If your platform accepts crypto deposits, the FATF Travel Rule (Recommendation 16) requires originator and beneficiary information to travel with transfers above the applicable threshold. Sumsub’s Travel Rule product, including unhosted-wallet attestation, plugs into the same applicant graph, so a player verified for fiat play does not need a second identity record for crypto. Treat the Travel Rule layer as an extension of KYC, not a separate project.
Pairing KYC with geolocation
Identity alone does not satisfy US state regulators, who also require the operator to confirm the player is physically inside a legal jurisdiction at the moment of play. Most teams run Sumsub for identity and a dedicated geolocation provider in parallel; if you are evaluating that layer, our guide to US gambling geolocation with GeoComply and its rivals covers how the two checks compose at the deposit gate.
Step 10–11: Idempotency, Retries and Status Reconciliation
Webhooks are delivered at-least-once, not exactly-once. Sumsub will retry a delivery if your endpoint is slow or returns a non-2xx, which means the same applicantReviewed event can land twice. Without idempotency you risk double-crediting a welcome bonus or thrashing account state. And occasionally a webhook is missed entirely – your service was deploying – so you also need a poll-based safety net. For broader market context, see our coverage of leading online casino sites.
Step 10 – idempotent event handling
Deduplicate on a stable key. Use the correlationId (or a composite of applicantId + type + the review timestamp) and record processed keys in a fast store with a TTL. Process the event only if the key is new.
// handleEvent.js
const { signedRequest } = require('./sumsubClient');
const redis = require('./redis');
async function handleEvent(event) {
const key = `sns:${event.correlationId || event.applicantId}:${event.type}`;
const fresh = await redis.set(key, '1', 'EX', 172800, 'NX'); // 48h, only-if-absent
if (fresh !== 'OK') return; // already processed
const { type, externalUserId, reviewResult } = event;
if (type === 'applicantReviewed') {
const answer = reviewResult?.reviewAnswer; // GREEN | RED
const reject = reviewResult?.reviewRejectType; // FINAL | RETRY
if (answer === 'GREEN') await markVerified(externalUserId);
else if (reject === 'RETRY') await requestResubmission(externalUserId);
else await blockAccount(externalUserId);
} else if (type === 'applicantOnHold') {
await flagForManualReview(externalUserId);
}
}
module.exports = { handleEvent };
Step 11 – the reconciliation poll
For any user stuck in pending longer than your SLA – say 30 minutes – poll Sumsub directly and reconcile. Query the applicant by your own identifier so you never have to store Sumsub’s internal ID just to look a user up.
// reconcile.js
const { signedRequest } = require('./sumsubClient');
async function reconcile(externalUserId) {
const path =
`/resources/applicants/-;externalUserId=${encodeURIComponent(externalUserId)}/one`;
const applicant = await signedRequest('GET', path);
return applicant?.review?.reviewResult?.reviewAnswer || 'pending';
}
module.exports = { reconcile };
Run this on a schedule for the small set of pending users; it is your defence against the silent missed-webhook failure mode that otherwise produces angry “I verified but can’t deposit” support tickets.
Step 12–14: Sandbox Testing, Go-Live and Observability
The last three steps turn a working prototype into something you can put a licence behind. Do not skip them; the gap between “it worked once in sandbox” and “it survives Friday-night traffic” is exactly here.
Step 12 – simulate GREEN and RED in sandbox
Sumsub’s sandbox lets you force outcomes. Upload one of the provided test documents to trigger a GREEN, and use the dashboard’s simulation controls to push an applicant to RED with both RETRY and FINAL reject types. Verify that each path drives the correct internal state and that a replayed webhook is ignored by your idempotency layer.
Step 13 – the production go-live checklist
- Swap
sbx:tokens forprd:tokens and point at the production webhook endpoint. - Confirm the production verification level matches sandbox exactly – same required documents, same AML checks.
- Serve the webhook over HTTPS with a valid certificate; Sumsub will not deliver to an unreachable or untrusted endpoint.
- Load the production webhook secret and re-test the signature path with a live event.
- Gate every deposit and bet endpoint on the server-side
kyc_verifiedflag.
Step 14 – observability and alerting
Instrument three things: webhook delivery (alert on a drop in volume), the pending-user backlog (alert when reconciliation has to rescue more than a handful), and the GREEN-rate (a sudden swing signals either a fraud wave or a broken level). Log applicantId and inspectionId for every decision – that pairing is your audit trail when a regulator asks why a specific player was approved.
The Complete Project Example
Here is the full microservice wired together. It is intentionally small – four route handlers and the shared client – because a KYC service should be boring, legible, and easy to audit.
Project structure
kyc-service/
├── .env
├── server.js # Express app, mounts routes
├── sumsubClient.js # signed request helper (Step 3)
├── applicants.js # createApplicant, createAccessToken (Steps 4–5)
├── webhook.js # signature verification (Step 7)
├── handleEvent.js # idempotent event processing (Step 10)
├── reconcile.js # pending-user safety net (Step 11)
└── redis.js # idempotency store
The Express entrypoint
// server.js
const express = require('express');
const { createApplicant, createAccessToken } = require('./applicants');
const webhook = require('./webhook');
const app = express();
app.use('/webhooks', webhook); // raw body – mount BEFORE json()
app.use(express.json());
// frontend asks for a launch token
app.post('/api/kyc/token', async (req, res) => {
const { userId } = req.body;
try {
await createApplicant(userId, req.body.email, req.body.phone)
.catch((e) => { if (e.response?.status !== 409) throw e; }); // ignore 'already exists'
const token = await createAccessToken(userId);
res.json({ token });
} catch (e) {
res.status(500).json({ error: 'kyc_token_failed' });
}
});
app.listen(8080, () => console.log('kyc-service on :8080'));
Note the ordering: the webhook router with its raw-body parser is mounted before express.json(), so the signature check still sees raw bytes while the rest of your API enjoys parsed JSON. That single ordering detail prevents the most common webhook-401 of all.
6 Critical Pitfalls That Break iGaming KYC Integrations
Each of these has cost a real operator either money or a compliance finding. They are ordered by how often they appear in production incident reports.
Pitfalls one through three: the signature traps
- Verifying parsed JSON instead of raw bytes. Re-serialising the webhook body changes whitespace and key order, so the HMAC never matches. Always HMAC the raw
Buffer. - Re-stringifying the request body after signing. If your HTTP client serialises the payload a second time, the API request fails with a misleading 401. Sign the exact string you send.
- Signing the path without its query string.
?levelName=...is part of the signed path. Drop it and every applicant call fails authentication.
Pitfalls four through six: the logic traps
- Trusting the client-side success message. The WebSDK’s
onMessagecompletion event is a UX hint, not authorisation. Only a signedapplicantReviewedwebhook should unlock money. - Treating every RED as FINAL. A
RETRYrejection is recoverable – usually a poor photo. Lock those users out and you are rejecting paying customers for a blurry selfie. - No idempotency. Because webhooks are retried, a non-idempotent handler can double-credit bonuses or corrupt account state. Deduplicate on a stable key with a TTL.
Troubleshooting: 10 Common Sumsub Errors and Fixes
When the integration misbehaves, the symptom rarely names the real cause. This lookup maps what you see to what is actually wrong.
| Symptom / error | Likely cause | Fix |
|---|---|---|
| HTTP 401, “app token invalid” | Wrong or expired App Token, or a sandbox token used against production | Regenerate the token; confirm the sbx: vs prd: prefix matches the host |
| HTTP 401 but the token is valid | Signature string mismatch (body re-stringified, or wrong timestamp) | Sign the exact wire body; send the pre-serialised payload; use seconds, not milliseconds, for ts |
| HTTP 403 on /resources/applicants | The token lacks permission for that verification level | Enable the level for the token in the dashboard |
| HTTP 409, “applicant already exists” | Duplicate externalUserId |
Look the user up by externalUserId and reuse the applicant; treat 409 as success |
| WebSDK stuck in a “token expired” loop | expirationHandler returns a stale token |
Mint a fresh token each call with TTL ≥ 600s |
| Webhook returns 401 in your logs | Verifying parsed JSON, not the raw body | Use express.raw and HMAC the raw bytes |
| Webhook signature never matches | Wrong hash algorithm assumed | Read x-payload-digest-alg and map to sha1 / sha256 / sha512 |
applicantReviewed never arrives |
Webhook URL unreachable or not HTTPS | Expose a valid HTTPS endpoint; check the Webhook Manager delivery log |
| GREEN in sandbox, RED in production | Production level requires different documents | Align the production level config with sandbox |
| Users stranded in “pending” | A webhook delivery was missed | Run the reconciliation poll (Step 11) |
Advanced Tips for High-Volume iGaming Deployments
Once the basics are live, these techniques separate a deployment that scrapes by from one that runs clean at six-figure monthly verification volumes.
Cut friction with Reusable KYC and Non-Doc Verification
For repeat players and supported markets, enabling Reusable KYC and Non-Doc Verification can remove the document-upload step entirely. That both lifts your conversion rate at the registration funnel – where operators lose the most users – and lowers your per-verification cost, because a database check is cheaper than a full document-plus-liveness review.
Layer the fraud signals
Identity verification is one signal among several. Sumsub’s Device Intelligence, Risk Scoring, and Fraud Networks products correlate a verification against device fingerprints and known fraud rings, which is how you catch the synthetic-identity and money-mule schemes that pass a single document check. Given that the most sophisticated attacks rose 180% in 2025, a single-layer defence is no longer enough.
Make webhooks asynchronous and queued
Acknowledge every webhook in under a second by writing it to a queue and processing it in a worker. Synchronous processing inside the HTTP handler invites timeouts, which trigger Sumsub retries, which amplify load exactly when you are already struggling. Add a dead-letter queue so a poison event never blocks the stream.
Beyond the Standard Coverage: What the Vendor Docs and Tutorials Miss
The official Sumsub reference and integration write-ups such as the widely-cited sdk.finance walkthrough do an excellent job of the happy path – create an applicant, embed the SDK, receive a webhook. What they consistently leave out is precisely what an iGaming operator gets audited on. Those gaps are the reason this guide exists.
The operational realities nobody documents
Vendor docs assume webhooks always arrive, never repeat, and are always processed in order – none of which is true at scale. They rarely show idempotency, almost never show a reconciliation poll, and gloss over the RETRY-versus-FINAL distinction that directly governs whether a legitimate customer can recover. They also stop at “verification succeeded” and say nothing about gating deposits server-side, which is the actual UKGC requirement. Each of those omissions is a production incident waiting to happen, and each is covered above.
Cost and throughput modeling
The other blind spot is economics, and here Sumsub has become considerably more transparent than the old contact-sales model suggested. KYC is billed per verification, and as of May 2026 Sumsub publishes two self-serve tiers: Basic runs $1.35 per verification with a $149/month minimum, while Compliance – which layers in AML screening – runs $1.85 per verification with a $299/month minimum. A public pricing review on 11 July 2026 confirmed both figures and noted that Sumsub bills only for successful verifications, not failed or abandoned attempts, which meaningfully improves the retry-rate math for the operator. The number that matters is still not the sticker price but the loaded cost: list price multiplied by your retry rate, plus the manual-review minutes that applicantOnHold cases consume. If you are still choosing a provider on those economics, our side-by-side comparison of Sumsub against Veriff, Onfido and Jumio for iGaming KYC breaks down where each one wins on price and pass-rate. Model your monthly volume against that loaded cost before you commit, and revisit it after go-live when your real retry rate is known.
A concrete model makes the point. Take an operator running 50,000 new verifications a month at an assumed blended rate of $1.50 per completed check. A naive estimate is $75,000, but the real figure includes retries and manual review: if 12% of users need a second attempt and 4% land in manual review at roughly three reviewer-minutes each, the loaded monthly cost climbs meaningfully above the sticker total once you price in both the extra checks and the reviewer hours. Reusable KYC and Non-Doc Verification attack exactly those two cost drivers – fewer uploads means fewer retries, and authoritative database checks rarely need human eyes. Re-run this model every quarter, because your retry rate is the single variable that moves the bill the most.
The 2026 fraud posture
Finally, standard guides predate the deepfake surge. With AI-generated documents and face swaps now mainstream – and synthetic identity the single largest first-party fraud type – your integration should default to liveness plus deepfake detection on every flow, not just high-risk ones. The marginal cost is small; the cost of a deepfake-enabled mule network on your books is not. The full Sumsub API reference documents the individual check toggles, and the regulator guidance from the UK Gambling Commission increasingly expects exactly this posture.
Frequently Asked Questions
Is the Sumsub KYC API free to test?
Yes. The sandbox environment is free and uses sbx:-prefixed App Tokens, so you can build and test the entire flow – applicants, access tokens, WebSDK, and webhooks – at no cost. Production verifications are billed per check, quoted to operators directly rather than published as a fixed public rate.
What is the difference between the WebSDK callback and the webhook?
The WebSDK’s onMessage completion event runs in the user’s browser and is a user-experience signal only – it can be spoofed and must never authorise money movement. The server-to-server webhook, verified with the x-payload-digest signature, is the only trustworthy source of truth for whether a user passed.
Which header verifies a Sumsub webhook?
Sumsub sends the signature in the x-payload-digest header and names the algorithm in x-payload-digest-alg (for example HMAC_SHA256_HEX). Compute the HMAC over the raw request body using your webhook secret and compare with a constant-time function.
What do GREEN and RED review answers mean?
GREEN means the applicant is approved; RED means rejected. A RED answer includes a reviewRejectType of RETRY (the user can resubmit, usually a quality issue) or FINAL (permanent rejection). Always branch on the reject type.
Do I have to store the player’s documents myself?
No, and you generally should not. Sumsub captures and stores the documents and biometric data inside its own environment; your platform stores only your externalUserId, the Sumsub applicantId, and the verification status. That data-minimisation keeps your GDPR exposure low.
Can a player reuse a verification across my brands?
Yes. Sumsub’s Reusable KYC lets a previously verified user confirm with a tap instead of re-uploading documents, which lifts conversion and lowers cost. It is enabled at the workflow level in the dashboard rather than in your code.
How do I satisfy the UKGC “verify before deposit” rule?
Gate your deposit and bet endpoints on a server-side kyc_verified flag that only turns true after a signed GREEN applicantReviewed webhook. Combine identity verification with age confirmation and, for US markets, geolocation, so no money moves until all required checks pass.
How long does a verification take?
Fully automated document-and-liveness checks typically resolve in seconds to a minute, and Non-Doc Verification is faster still because it skips the upload. Cases routed to manual review – usually AML hits surfaced as applicantOnHold – take longer and depend on your review team’s staffing.







