Onfido Identity Verification SDK: 14-Step Casino KYC 2026

Most Onfido tutorials stop at “mount the SDK, read onComplete, ship it.” That is fine for a hobby project and dangerous for a regulated casino. When a UK Gambling Commission auditor asks why a player who failed identity verification still managed to deposit, “the webhook didn’t arrive” is not an answer that keeps your licence. This guide takes a different stance: we treat the Onfido identity verification SDK as one dependency inside a hardened verification microservice – idempotent, fail-closed, fully reconciled, and built to survive Onfido outages, duplicate webhooks, and multi-jurisdiction rule sprawl.

Everything here is pinned to the stack shipping in June 2026: the Onfido Smart Capture Web SDK (onfido-sdk-ui) at v14.77.0, the Onfido REST API at v3.6, Onfido Studio workflow orchestration, and the Atlas AI decisioning engine – all now delivered under Entrust, which acquired Onfido in 2024 and folded the docs into documentation.identity.entrust.com while keeping the onfido brand, packages, and endpoints intact. If you only need the bare 12-step walkthrough, we already published that as a separate piece; this one is the production-grade build.

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 a Dedicated Onfido Verification Microservice in 2026

A casino onboarding flow is not a contact form. The identity check sits on the critical path between “user wants to deposit” and “regulator says you may let them.” That single position creates three properties most SDK tutorials ignore: it must be auditable (every decision reproducible years later), available (an outage cannot quietly let unverified players through), and idempotent (network retries cannot create duplicate applicants, duplicate checks, or duplicate billing). A thin frontend integration satisfies none of these. A microservice with its own datastore, event ledger, and reconciliation loop satisfies all three.

The compliance position, not the UI, defines the design

Under the UK Gambling Commission’s LCCP, licensed remote operators must verify a customer’s name, address and date of birth before they deposit or gamble – not after. The Malta Gaming Authority and Ontario’s AGCO impose parallel obligations. The technical implication is blunt: your service must default to “deny access” and only open the gate on a positive, cryptographically verified result. Architecturally, that is a fail-closed state machine, not an event listener.

The verified June 2026 stack

Before designing anything, pin the components you are actually building against. These are the live, non-archived packages and versions as of this writing – confirmed against npm, the public GitHub repositories, and the Entrust documentation portal.

Component Package / source Version Released Notes
Smart Capture Web SDK onfido-sdk-ui (npm) 14.77.0 Jun 24, 2026 Primary SDK for this build; 194★ repo
iOS SDK onfido-ios-sdk 33.1.1 May 28, 2026 Native capture, NFC chip reading
Android SDK onfido-android-sdk 23.9.0 Jun 24, 2026 Dynamic feature module friendly
React Native @onfido/react-native-sdk 16.2.0 Jun 8, 2026 MIT licensed wrapper
REST API api.eu / api.us / api.ca .onfido.com v3.6 Current Region-pinned base URLs

Notice the cadence: the Web SDK shipped a release the same week this guide was written, and the Android SDK the same day. Onfido moves fast, which is exactly why version pinning (covered in Step 4) is not optional. The rest of this guide assumes Node.js 22 LTS, TypeScript 5.6, PostgreSQL 16, and a Redis 7 instance for the circuit-breaker and idempotency layers.

Prerequisites: Versions, Accounts, and Regulatory Footing

Get these in place before writing a line of code. Skipping the regulatory step is the most common reason a technically-perfect integration fails its first audit.

Accounts, regions, and tokens

  • An Onfido/Entrust dashboard account with both a sandbox and a live API token. Tokens are region-bound – a token issued for the EU region will not authenticate against api.us.onfido.com.
  • At least one Onfido Studio workflow published, with its workflow_id. Studio is the no-code orchestration layer where you define the welcome → document → face → watchlist → decision graph.
  • A webhook token (distinct from the API token) used to verify inbound webhook signatures.
  • Your data-residency decision: EU operators typically pin to api.eu.onfido.com; Ontario operators often require Canadian residency via api.ca.onfido.com.

Runtime and dependency matrix

Dependency Minimum version Why this floor
Node.js 22.x LTS Native fetch, stable crypto.timingSafeEqual
TypeScript 5.6 Matches onfido-sdk-ui bundled types
onfido-sdk-ui 14.77.0 Workflow-run init signature, cross-device beta flags
PostgreSQL 16 INSERT ... ON CONFLICT for idempotent ledgers
Redis 7.2 Circuit-breaker state, distributed locks
express / fastify 4.x / 4.x Raw-body access for signature verification

Environment variables

Centralise secrets; never embed an API token in frontend code (the SDK uses short-lived SDK tokens, not the API token – more in Step 3).

ONFIDO_API_TOKEN=live_xxx          # server-only, never shipped to the browser
ONFIDO_REGION=eu                   # eu | us | ca
ONFIDO_WORKFLOW_ID_UK=2b8f...      # Studio workflow per jurisdiction
ONFIDO_WORKFLOW_ID_MGA=9c1a...
ONFIDO_WEBHOOK_TOKEN=whk_xxx       # used for X-SHA2-Signature HMAC
DATABASE_URL=postgres://...
REDIS_URL=redis://...

Architecture: The KYC Microservice Topology

The service – call it kyc-svc – owns every interaction with Onfido. The casino monolith never calls Onfido directly; it calls kyc-svc, which gives you a single choke point for rate-limiting, retries, audit logging, and cost control. This is the structural difference between a production build and a tutorial: the boundary, not the SDK call.

Components and trust boundaries

Five logical components live inside the service:

  1. Onboarding API – creates applicants and workflow runs, mints SDK tokens.
  2. Webhook ingress – verifies signatures, writes to an idempotent event ledger.
  3. Reconciliation worker – polls Onfido for any workflow run that never fired a webhook.
  4. Decision engine – maps Atlas AI workflow output to an internal verdict, fail-closed.
  5. Evidence warehouse – immutable store of every decision and its inputs for audit.

Data flow at a glance

The happy path is: player requests verification → kyc-svc creates applicant + workflow run (idempotently) → returns SDK token + workflowRunId to the browser → Smart Capture Web SDK captures documents and a selfie → Onfido processes via Atlas AI → workflow_run.completed webhook arrives → signature verified → decision engine writes verdict → casino account flagged verified. The unhappy paths – webhook lost, Onfido down, duplicate submit, declined result – are where most of this guide lives.

API surface you will actually call

Method Endpoint (v3.6) Purpose
POST /applicants Create the person record
POST /workflow_runs Start a Studio workflow for an applicant
POST /sdk_token Mint a short-lived browser token
GET /workflow_runs/{id} Reconcile / read final status + output
POST /webhooks Register the webhook endpoint (one-time)

Steps 1–2: Idempotent Applicant and Workflow-Run Creation

The first failure mode in every real integration is the double-submit: a player taps “verify” twice, or your own retry logic fires after a slow response, and you create two applicants and two billable workflow runs for one person. The fix is an idempotency key derived from your internal user ID, stored before you call Onfido.

Step 1 – Create the applicant once

// src/onfido/client.ts
const BASE = `https://api.${process.env.ONFIDO_REGION}.onfido.com/v3.6`;
const HEADERS = {
  'Authorization': `Token token=${process.env.ONFIDO_API_TOKEN}`,
  'Content-Type': 'application/json',
};

export async function createApplicant(user) {
  const res = await fetch(`${BASE}/applicants`, {
    method: 'POST',
    headers: HEADERS,
    body: JSON.stringify({
      first_name: user.firstName,
      last_name: user.lastName,
      // location drives which document rules Atlas AI applies
      location: { country_of_residence: user.countryIso3 },
    }),
  });
  if (!res.ok) throw new OnfidoError(res.status, await res.text());
  return res.json(); // { id: 'applicant-uuid', ... }
}

Step 2 – Bind an idempotent workflow run

Wrap creation in a Postgres upsert so a retry returns the existing run instead of minting a new one. The unique key is your user ID plus the verification reason (onboarding, withdrawal, periodic), because a player legitimately re-verifies many times over an account’s life.

// idempotent: one workflow run per (userId, reason) until it terminates
export async function startVerification(db, user, reason) {
  const applicant = await getOrCreateApplicant(db, user);     // cached by userId
  const existing = await db.query(
    `SELECT workflow_run_id FROM kyc_runs
     WHERE user_id=$1 AND reason=$2 AND status IN ('processing','awaiting_input')`,
    [user.id, reason]);
  if (existing.rows[0]) return existing.rows[0].workflow_run_id;

  const run = await fetch(`${BASE}/workflow_runs`, {
    method: 'POST', headers: HEADERS,
    body: JSON.stringify({
      workflow_id: workflowFor(user.jurisdiction),   // see Step 12
      applicant_id: applicant.id,
    }),
  }).then(r => r.json());

  await db.query(
    `INSERT INTO kyc_runs(user_id, reason, applicant_id, workflow_run_id, status)
     VALUES($1,$2,$3,$4,'processing')
     ON CONFLICT (user_id, reason) DO NOTHING`,
    [user.id, reason, applicant.id, run.id]);
  return run.id;
}

Expected output: a single workflow_run_id (a UUID), and exactly one row in kyc_runs no matter how many times the endpoint is hammered. If you watch the Onfido dashboard, you should see one workflow run per player per reason – not a pile of duplicates. This is also the cleanest lever for cost control, which matters because Onfido pricing is quote-based per check/report, not a public per-unit number.

Steps 3–4: SDK Tokens and Mounting Smart Capture Web SDK v14.77

The browser never sees your API token. Instead, the service mints a short-lived SDK token scoped to one applicant and one referrer origin. The Smart Capture Web SDK exchanges that token for permission to upload media directly to Onfido, so document images never transit your servers – a meaningful data-minimisation win for GDPR and PCI scope.

Step 3 – Mint the SDK token server-side

// POST /verify/start  -> returns { sdkToken, workflowRunId }
app.post('/verify/start', requireAuth, async (req, res) => {
  const workflowRunId = await startVerification(db, req.user, 'onboarding');
  const tokenRes = await fetch(`${BASE}/sdk_token`, {
    method: 'POST', headers: HEADERS,
    body: JSON.stringify({
      applicant_id: req.user.applicantId,
      // lock the token to your origin to stop token theft
      referrer: 'https://casino.example.com/*',
    }),
  }).then(r => r.json());
  res.json({ sdkToken: tokenRes.token, workflowRunId });
});

Step 4 – Initialise the Web SDK against a workflow run

With Onfido Studio, the frontend init is minimal: hand the SDK the token and the workflowRunId and it renders the entire welcome → document → poa → face → complete flow defined in Studio. The capture steps and their order are controlled server-side in the workflow, not hard-coded in the browser – change the flow without shipping frontend code.

import { Onfido } from 'onfido-sdk-ui';     // pin to 14.77.0

const { sdkToken, workflowRunId } =
  await fetch('/verify/start', { method: 'POST' }).then(r => r.json());

const instance = Onfido.init({
  token: sdkToken,
  workflowRunId,
  containerId: 'onfido-mount',
  onComplete: () => {
    // DO NOT trust this for the verdict. It only means capture finished.
    // The real decision arrives via webhook. Here, just show "we're reviewing".
    window.location.assign('/verify/pending');
  },
  onError: (err) => reportClientError(err),
  onUserExit: (reason) => analytics.track('kyc_abandon', { reason }),
});

Critical correctness point: onComplete fires when the user finishes capturing, not when Onfido finishes deciding. Treating onComplete as “verified” is the single most common security bug in casino KYC integrations. The verdict is authoritative only after the signed webhook (Steps 6–7). Pin the dependency to avoid silent breaking changes:

// package.json – exact pin, no caret
"dependencies": { "onfido-sdk-ui": "14.77.0" }

Step 5: Cross-Device Handoff Orchestration

Desktop players have terrible cameras and worse patience. The Smart Capture Web SDK’s cross-device navigation lets a player start on desktop, hand off the camera-dependent document and face steps to their phone via QR code or SMS link, then resume on desktop – all inside the same workflow run. Most basic tutorials skip this entirely; for casinos it routinely lifts completion rates because document capture quality determines whether Atlas AI can auto-approve.

Why cross-device is a conversion lever

A blurry desktop-webcam passport photo forces a manual review or a re-capture, both of which bleed conversions. Pushing capture to a phone camera raises first-pass document quality, which raises straight-through approval, which lowers your manual-review headcount. It is the rare feature that improves UX and unit economics simultaneously.

Enforcing and customising the handoff

The v14.77 SDK exposes cross-device controls (some still flagged beta) to enforce the phone hop for capture steps and to brand the hand-off screen. You can require cross-device for the document and face steps while keeping welcome and complete on desktop:

Onfido.init({
  token: sdkToken,
  workflowRunId,
  containerId: 'onfido-mount',
  crossDeviceClientIntroProductName: 'Example Casino verification',
  steps: [
    'welcome',
    { type: 'document', options: { forceCrossDevice: true } },
    'face',
    'complete',
  ],
  onComplete: () => window.location.assign('/verify/pending'),
});

Because the workflow run is the unit of state, the handoff survives a desktop refresh: re-mounting the SDK with the same workflowRunId and a fresh SDK token resumes exactly where the player left off. Persist workflowRunId in your session so a dropped connection is recoverable rather than a restart.

Steps 6–7: Signed Webhooks and an Idempotent Event Ledger

This is where the microservice earns its keep. Onfido reports the real outcome by calling your registered webhook with a workflow_run.completed event. Two hard requirements: verify the signature (anyone can POST JSON to a public URL) and process each event exactly once (Onfido retries, so the same event can arrive several times).

Step 6 – Verify the X-SHA2-Signature

Onfido signs the raw request body with HMAC-SHA256 using your webhook token and sends the hex digest in the X-SHA2-Signature header. You must compute over the raw bytes, before any JSON parsing, and compare in constant time.

import crypto from 'node:crypto';

// mount with express.raw so req.body is a Buffer, not parsed JSON
app.post('/webhooks/onfido',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const sig = req.get('X-SHA2-Signature') || '';
    const expected = crypto
      .createHmac('sha256', process.env.ONFIDO_WEBHOOK_TOKEN)
      .update(req.body)              // raw Buffer
      .digest('hex');

    const ok = sig.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
    if (!ok) return res.sendStatus(401);

    const event = JSON.parse(req.body.toString('utf8'));
    enqueueEvent(event);             // ack fast, process async
    res.sendStatus(200);
  });

Step 7 – Write to an idempotent event ledger

Acknowledge within milliseconds, then process from a durable ledger keyed by Onfido’s event ID. The ON CONFLICT DO NOTHING makes a redelivered event a no-op.

async function enqueueEvent(event) {
  const id = event.payload?.object?.id;
  const inserted = await db.query(
    `INSERT INTO kyc_events(event_id, resource_type, action, payload, status)
     VALUES($1,$2,$3,$4,'pending')
     ON CONFLICT (event_id) DO NOTHING RETURNING event_id`,
    [event.payload.object.id, event.payload.resource_type,
     event.payload.action, event]);
  if (inserted.rows.length) worker.notify();   // only the first delivery works
}

Expected output: a 200 returned in well under 100 ms, one ledger row per real event, and zero double-processing even when Onfido redelivers. Note the ack-then-process split – never run your decision logic inside the HTTP handler, or a slow database write turns into webhook timeouts and a retry storm. For teams layering transaction surveillance on top of KYC, the same idempotent-ledger pattern underpins real-time AML transaction monitoring, so it is worth standardising once and reusing. For broader market context, see our coverage of best sweepstakes casino sites.

Step 8: The Reconciliation Sweeper for Missed Webhooks

Webhooks are best-effort, not guaranteed. A deploy that bounces your pod mid-delivery, a five-minute ingress outage, a mis-scoped firewall rule – any of these silently drops a workflow_run.completed. If you trust webhooks alone, those players sit in “pending” forever, open support tickets, and churn. The fix is a reconciliation sweeper that closes the loop by polling.

Why webhooks get lost

Onfido retries failed deliveries on a schedule, but retries also expire. Any run that has been processing in your database longer than its expected SLA is a candidate for reconciliation regardless of whether a webhook ever showed up. The sweeper is your safety net and, conveniently, your monitoring signal – a rising reconciliation rate means your webhook ingress is degrading.

The sweeper loop

// runs every 60s; polls Onfido for stuck runs and backfills the verdict
async function reconcile(db) {
  const stuck = await db.query(
    `SELECT workflow_run_id FROM kyc_runs
     WHERE status='processing' AND created_at < now() - interval '10 minutes'
     LIMIT 200`);

  for (const { workflow_run_id } of stuck.rows) {
    const run = await fetch(`${BASE}/workflow_runs/${workflow_run_id}`,
      { headers: HEADERS }).then(r => r.json());
    if (['approved','declined','review','error','abandoned'].includes(run.status)) {
      await applyDecision(db, run);          // same path the webhook uses
    }
  }
}
setInterval(() => reconcile(db).catch(log.error), 60_000);

Crucially, both the webhook path and the sweeper call the same applyDecision function, and that function is itself idempotent (keyed on workflow_run_id). Whether the verdict arrives by push or by poll, the player ends in the same state exactly once. Add jitter and a LIMIT so the sweeper never stampedes the Onfido API during a backlog.

Step 9: Atlas AI Decisioning with Fail-Closed Thresholds

Atlas AI is Onfido’s machine-learning engine for document authentication and biometric (face-to-document) matching. In a Studio workflow it produces a terminal status on the workflow run plus structured output. Your job is to map that to an internal verdict – and to choose a safe default for everything that is not an explicit pass.

Reading the workflow output

Workflow runs terminate in one of a handful of statuses. Map them deliberately rather than treating “not declined” as “approved”:

Workflow run status Meaning Internal verdict Account access
approved Atlas AI cleared all checks VERIFIED Granted
review Needs human adjudication PENDING_REVIEW Held
declined Failed document or biometric REJECTED Denied
abandoned Player never finished INCOMPLETE Denied
error Processing failure ERROR Denied (retry)

Fail-closed by default

function verdictFor(run) {
  switch (run.status) {
    case 'approved': return { verdict: 'VERIFIED', grant: true };
    case 'review':   return { verdict: 'PENDING_REVIEW', grant: false };
    case 'declined': return { verdict: 'REJECTED', grant: false };
    // anything we don't explicitly recognise => deny, never grant
    default:         return { verdict: 'BLOCKED', grant: false };
  }
}

The default branch is the whole point: a new status string, a malformed payload, or an unexpected enum value results in denied access, not accidental approval. In a regulated environment, a false “denied” costs you a support ticket; a false “approved” costs you your licence. If you are still choosing a provider, our breakdown of Sumsub vs Veriff vs Onfido vs Jumio for iGaming KYC compares exactly these decisioning models head to head.

Step 10: Circuit Breaker and Degraded Mode

Onfido will, occasionally, be slow or unreachable. The wrong reaction is to let onboarding hang or – far worse – to fail open and admit unverified players. A circuit breaker around the Onfido client gives you a controlled, fail-closed degradation instead of a cascade.

Breaker states

Track consecutive failures in Redis. After a threshold, open the breaker: stop calling Onfido, immediately return a “verification temporarily unavailable” response, and queue the intent for later. After a cooldown, half-open and probe with a single request before fully closing again.

const breaker = {
  async call(fn) {
    const state = await redis.get('onfido:breaker');
    if (state === 'open') throw new ServiceUnavailable('kyc_degraded');
    try {
      const out = await fn();
      await redis.del('onfido:failcount');
      return out;
    } catch (e) {
      const n = await redis.incr('onfido:failcount');
      if (n >= 5) await redis.set('onfido:breaker', 'open', 'EX', 30);
      throw e;
    }
  }
};

What “degraded” must never do

Degraded mode can show a friendly retry screen, hold the player at a soft wall, and page your on-call. It must not grant deposit access on the assumption that verification “probably would have passed.” For casinos this is non-negotiable: the breaker fails closed. The health of this breaker – open/closed transitions per hour – belongs on the same dashboard as your geolocation checks, since the two together form the gate. (If you run US markets, our comparison of GeoComply vs Xpoint vs LocationSmart geolocation providers covers the second half of that gate.)

Steps 11–12: Step-Up Re-KYC Gates and Multi-Jurisdiction Routing

Onboarding verification is the floor, not the ceiling. Regulators expect risk-based re-verification: a player who deposits modestly for months and then tries to withdraw a five-figure win triggers obligations that the original onboarding check never covered. And the rules differ by jurisdiction, which means one workflow does not fit all.

Step 11 – Step-up triggers

Model re-KYC as gates that start fresh workflow runs with a different reason (so the idempotency from Step 2 keeps them distinct from onboarding). Common casino triggers:

Trigger Workflow reason Added reports Gate behaviour
Cumulative deposits over threshold edd_deposit Proof of address, watchlist Soft block on next deposit
Withdrawal request withdrawal Re-auth selfie, doc re-check Hard block until cleared
Large/anomalous win sof Source-of-funds evidence Hard block, manual review
Periodic (e.g. 12 months) periodic Document expiry re-check Soft block, grace window

The withdrawal gate is the one auditors scrutinise hardest, and it is a hard block by design – you verify the human before money leaves. Because KYC status gates the cashier, teams often co-locate this logic with their payment routing; if that is you, our look at Stripe vs Worldpay vs Nuvei for licensed gambling card processing shows how the verified flag flows downstream into settlement. For broader market context, see our coverage of leading online casino sites.

Step 12 – Jurisdiction routing table

Select the Studio workflow (and therefore the report stack and data region) from a config table keyed by jurisdiction. New market? Add a row, not a deploy.

const ROUTING = {
  GB: { workflow: process.env.ONFIDO_WORKFLOW_ID_UK,  region: 'eu', residency: 'eu' },
  MT: { workflow: process.env.ONFIDO_WORKFLOW_ID_MGA, region: 'eu', residency: 'eu' },
  ON: { workflow: process.env.ONFIDO_WORKFLOW_ID_AGCO, region: 'ca', residency: 'ca' },
};
function workflowFor(jurisdiction) {
  const r = ROUTING[jurisdiction];
  if (!r) throw new Error(`No KYC workflow for ${jurisdiction}`); // fail closed
  return r.workflow;
}

Watchlist, PEP, and Ongoing AML Screening

Identity verification proves a player is who they claim. Anti-money-laundering screening proves they are someone you are allowed to do business with. The two are distinct obligations, and casinos must satisfy both – the FATF recommendations that underpin UK, Maltese and Ontario AML regimes require sanctions, politically-exposed-person (PEP), and adverse-media screening at onboarding and on an ongoing basis. Onfido delivers this through Watchlist reports that you attach as tasks inside the same Studio workflow, so screening runs in the same run that captures the document.

Adding watchlist tasks to the workflow

Rather than a separate integration, AML screening is a report type in the workflow output. A Watchlist (AML) report returns matches against global sanctions and PEP lists plus adverse media; a monitored variant re-screens the applicant continuously and fires a fresh webhook when a new hit appears. Read the result alongside the identity verdict and route any match to manual review rather than auto-approving:

function amlGate(run) {
  const watchlist = run.output?.watchlist;          // from the Studio task
  if (!watchlist) return { clear: false, reason: 'aml_missing' };  // fail closed
  const hits = watchlist.matches ?? [];
  if (hits.length === 0) return { clear: true };
  // any sanctions/PEP/adverse-media hit => hold for compliance, never auto-pass
  return { clear: false, reason: 'aml_review', hits };
}

Ongoing monitoring and escalation

Onboarding screening is a snapshot; a player clean today can appear on a sanctions list next month. Enrol verified players in monitored watchlist screening so that a new match arrives as a workflow_run.completed (or watchlist-specific) webhook, lands in the same idempotent ledger from Step 7, and triggers an account freeze pending review. Pair this with the transaction-side surveillance described in our AML monitoring guide so that a behavioural red flag and a watchlist hit converge on one case queue rather than two disconnected alerts. The compliance team needs one screen, not a scavenger hunt.

Steps 13–14: Evidence Warehouse, SLOs, and Observability

Two years from now an auditor will ask you to reproduce a specific decision: which document, which Atlas AI result, which workflow version, which thresholds. If that data lives only in Onfido’s dashboard, you are at the mercy of retention windows and export tooling. Step 13 builds your own immutable evidence store; Step 14 instruments the pipeline so you find problems before regulators do.

Step 13 – Immutable evidence warehouse

On every terminal decision, write an append-only record: the workflow run ID, applicant ID, final status, the raw output, the workflow version, and a timestamp. Store it where it cannot be silently edited – a write-once bucket or an append-only table with row-level immutability.

async function applyDecision(db, run) {
  const { verdict, grant } = verdictFor(run);
  await db.query(`BEGIN`);
  await db.query(
    `INSERT INTO kyc_evidence(workflow_run_id, applicant_id, status,
        verdict, raw_output, decided_at)
     VALUES($1,$2,$3,$4,$5, now())
     ON CONFLICT (workflow_run_id) DO NOTHING`,    // idempotent
    [run.id, run.applicant_id, run.status, verdict, run]);
  await db.query(
    `UPDATE kyc_runs SET status=$2 WHERE workflow_run_id=$1`,
    [run.id, run.status]);
  await db.query(`COMMIT`);
  if (grant) await casino.markVerified(run.applicant_id);
}

Step 14 – SLOs and metrics that matter

Metric What it tells you Example SLO target
Capture-to-decision latency (p95) End-to-end verification speed < 90 s
Straight-through approval rate Share auto-approved by Atlas AI Track trend, alert on drop
Reconciliation rate Webhook delivery health < 1% of runs
Breaker open minutes / day Onfido availability impact Page on any open
Abandonment by step Where players drop off Investigate spikes

Export these as Prometheus counters and histograms. The two that predict incidents are reconciliation rate (rising = webhook ingress failing) and breaker open minutes (rising = Onfido or your network degrading). Alert on both before they become a pile of stuck players.

Testing: Sandbox Documents, k6 Load, and Canary Rollout

You cannot test a casino KYC flow against real passports. Onfido’s sandbox accepts deterministic test documents that force specific outcomes, letting you exercise every branch – approve, decline, review – without a single real identity.

Sandbox outcome forcing

Point the service at api.eu.onfido.com with your sandbox token and use Onfido’s documented test documents to deterministically trigger approved, declined, and review. Wire one end-to-end test per workflow-run status from the Step 9 table so the fail-closed default is itself covered by a test that feeds an unknown status string.

k6 load test for the webhook ingress

The webhook endpoint is your highest-throughput surface during a marketing spike. Prove it acks fast under load and that idempotency holds when the same event is replayed concurrently.

import http from 'k6/http';
import { check } from 'k6';

export const options = { vus: 200, duration: '2m' };
const body = open('./fixtures/workflow_completed.json');
const sig = __ENV.PRECOMPUTED_SIG;   // HMAC of the fixture body

export default function () {
  const res = http.post('https://kyc.example.com/webhooks/onfido', body, {
    headers: { 'Content-Type': 'application/json', 'X-SHA2-Signature': sig },
  });
  check(res, {
    'acked 200': (r) => r.status === 200,
    'fast': (r) => r.timings.duration < 100,
  });
}

Canary the decision engine

Never flip a new workflow or decisioning change to 100% of traffic. Route 5% of new verifications to the new Studio workflow, watch straight-through approval and decline rates against the control for 24 hours, then ramp. A workflow change that quietly tanks approval rates is a revenue incident; a canary catches it at 5% instead of 100%.

Five Critical Pitfalls (and How to Avoid Them)

Each of these has put a real operator into a remediation project. They are ordered by how often they cause an audit finding.

  1. Trusting onComplete as the verdict. It means “capture finished,” nothing more. Grant access only on the signed webhook (or reconciled poll). This is the number-one casino KYC bug.
  2. Parsing the webhook body before verifying the signature. If you let a body parser touch the bytes first, your HMAC is computed over re-serialised JSON and will never match. Always sign over the raw buffer.
  3. No reconciliation. Webhook-only designs strand players whenever a delivery is dropped. The sweeper in Step 8 is not optional for production.
  4. Failing open under load or outage. A timeout that defaults to “let them in” is a licence-ending bug. Default every unknown state to denied.
  5. Floating SDK versions. A ^14.0.0 range silently pulled 14.77.0 mid-sprint can change capture behaviour. Pin exact, upgrade deliberately, test in sandbox first.

Eight-Item Troubleshooting Guide

  • 401 on every API call. Wrong region or wrong token format. The header is Authorization: Token token=YOUR_TOKEN – the literal word token= is required, and the token must match the region in the base URL.
  • Webhook returns 401 from your own handler. You are almost certainly hashing parsed JSON. Switch the route to express.raw and HMAC the buffer.
  • SDK shows “token expired.” SDK tokens are short-lived. Mint a fresh one on page load; do not cache them client-side across sessions.
  • Cross-device QR loops back to start. The phone is hitting a different workflowRunId. Persist and reuse the same run ID; do not call /verify/start again on the mobile leg.
  • Duplicate billable runs. Your idempotency key is missing or too broad. Key on (userId, reason) and enforce it with a Postgres unique constraint, not application logic alone.
  • Players stuck in “pending” forever. Webhook ingress is down and you have no sweeper. Deploy Step 8 and alert on reconciliation rate.
  • Workflow run status is error. Usually a document Atlas AI could not read. Surface a clear re-capture prompt; force cross-device for the document step to raise image quality.
  • Signature matches in test, fails in prod. A proxy or WAF is mutating the body (gzip, charset rewrite). Verify before any middleware can touch bytes, and exclude the webhook path from body-altering filters.

Beyond the Standard Coverage: What Competitor Guides Miss

We pulled the top-ranking Onfido SDK tutorials – Onfido/Entrust’s own SDK reference, the npm and GitHub package pages, and third-party walkthroughs such as Alpaca’s brokerage-app integration. They are competent at one thing: getting the SDK to render. The Alpaca guide, for example, is a clean five-step Android flow (add dependency, configure token, instantiate client, start flow, handle callbacks). What none of them cover is everything that makes the integration survivable in production.

The gaps, concretely

  • No webhook reliability story. Reference docs describe the webhook payload but not idempotent ledgers, signature-before-parse ordering, or reconciliation for dropped deliveries.
  • No failure semantics. Not one of the top guides specifies fail-closed behaviour, circuit breaking, or degraded mode – the exact properties a regulator cares about.
  • No re-KYC lifecycle. They treat verification as a one-shot onboarding event, ignoring withdrawal gates, source-of-funds escalation, and periodic re-checks.
  • No multi-jurisdiction routing. A brokerage tutorial has no reason to model UKGC vs MGA vs AGCO workflow selection; a casino must.
  • No auditability. None build an evidence warehouse or define SLOs, leaving you unable to reproduce a decision under audit.

Why the gap exists

It exists because the official material is reference documentation and the third-party guides target fintech onboarding, not real-money gambling. The SDK call is 10% of the work; the other 90% – idempotency, reconciliation, fail-closed decisioning, jurisdiction routing, and evidence retention – is what this guide front-loads. That is the difference between a demo that renders and a service that keeps your licence.

Complete Project Example

A minimal but production-shaped layout that ties every step together:

kyc-svc/
├─ src/
│  ├─ onfido/
│  │  ├─ client.ts          # createApplicant, sdkToken, workflowRun (Steps 1-4)
│  │  └─ breaker.ts         # circuit breaker (Step 10)
│  ├─ api/
│  │  ├─ start.ts           # POST /verify/start (Step 3)
│  │  └─ webhook.ts         # POST /webhooks/onfido, HMAC verify (Steps 6-7)
│  ├─ workers/
│  │  ├─ reconcile.ts       # missed-webhook sweeper (Step 8)
│  │  └─ decide.ts          # applyDecision + evidence (Steps 9, 13)
│  ├─ routing.ts            # jurisdiction table (Step 12)
│  ├─ stepup.ts             # re-KYC gates (Step 11)
│  └─ metrics.ts            # Prometheus SLOs (Step 14)
├─ migrations/
│  ├─ 001_kyc_runs.sql      # (user_id, reason) UNIQUE
│  ├─ 002_kyc_events.sql    # event_id UNIQUE  (idempotent ledger)
│  └─ 003_kyc_evidence.sql  # workflow_run_id UNIQUE, append-only
├─ test/
│  └─ load/webhook.k6.js    # k6 ingress test
└─ package.json             # onfido-sdk-ui pinned 14.77.0

The three unique constraints – (user_id, reason), event_id, and workflow_run_id – are what make the whole system idempotent at the database layer rather than hoping application code never races. Wire the frontend (Steps 4–5) into your existing player-onboarding page, deploy kyc-svc behind your API gateway, register the webhook once, and you have a verification pipeline that satisfies UKGC and MGA expectations rather than merely rendering a camera.

Frequently Asked Questions

Is Onfido still a product after the Entrust acquisition?

Yes. Entrust acquired Onfido in 2024 and integrated it into its identity portfolio, but the Onfido Smart Capture SDKs, Onfido Studio, Atlas AI, and the v3.6 API all remain live under the onfido brand. Documentation now lives at documentation.identity.entrust.com, and packages like onfido-sdk-ui (14.77.0 as of June 2026) continue shipping frequent releases.

What is the difference between the SDK token and the API token?

The API token is a long-lived server secret that authenticates REST calls and must never reach the browser. The SDK token is short-lived, scoped to one applicant and origin, and is the only credential the Smart Capture Web SDK ever holds. Mint a fresh SDK token per session server-side.

Why can’t I trust the SDK’s onComplete callback as the verdict?

onComplete only signals that the user finished capturing documents and a selfie. Atlas AI decisioning happens afterward, asynchronously. The authoritative result arrives via the signed workflow_run.completed webhook (or a reconciled poll). Granting account access on onComplete is the most common security flaw in casino KYC builds.

How does Onfido sign its webhooks?

Onfido computes an HMAC-SHA256 over the raw request body using your webhook token and sends the hex digest in the X-SHA2-Signature header. Verify it over the unparsed buffer with a constant-time comparison before processing. Compute over re-serialised JSON and it will never match.

Do I need Onfido Studio, or can I call checks directly?

You can create checks and reports directly via the API, but Studio’s workflow orchestration is what makes multi-jurisdiction routing, conditional steps, and watchlist screening manageable without redeploying. For a regulated casino, Studio workflows selected from a routing table (Step 12) are the maintainable path.

What does Onfido cost for an iGaming operator?

Onfido does not publish public per-check pricing; it quotes custom contracts based on volume and the mix of reports (document, facial similarity, watchlist, proof of address). Model total cost as price-per-report multiplied by your verification and re-KYC volume, and use the idempotency controls in Steps 2 and 11 to avoid paying for duplicate runs.

How do I handle a player who must re-verify before withdrawing?

Start a new workflow run with a distinct reason such as withdrawal, which keeps it separate from the onboarding run under the Step 2 idempotency key. Treat the withdrawal gate as a hard block: the funds do not move until the new run reaches approved. This is the gate auditors examine most closely.

Which SDK should I use – Web, iOS, Android, or React Native?

Use the Smart Capture Web SDK (onfido-sdk-ui 14.77.0) for browser and webview flows; it supports cross-device handoff so desktop users can capture on their phones. Use the native iOS (33.1.1) or Android (23.9.0) SDKs when you need NFC chip reading or the tightest in-app experience, and the React Native wrapper (16.2.0) for cross-platform apps. All speak to the same v3.6 API and Studio workflows.

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