KYC Provider Integration (Sumsub, Onfido, Jumio)
Choosing a KYC provider affects conversion rate, verification cost, and regional coverage. Integration takes 1-3 weeks depending on provider and requirements — the main challenge is not API technical details, but correct edge case handling and webhook logic.
Provider Comparison
| Parameter | Sumsub | Onfido | Jumio |
|---|---|---|---|
| Document coverage | 220+ countries | 195+ countries | 200+ countries |
| Crypto compliance | Native support | Limited | Limited |
| Cost | Average | Above average | Above average |
| Best for | Crypto/fintech WW | EU market | Enterprise KYB |
| SDK quality | Excellent | Good | Good |
For crypto projects: Sumsub is the standard choice due to native crypto compliance logic.
Sumsub Integration
Backend Token Generation
import crypto from "crypto";
import axios from "axios";
const SUMSUB_APP_TOKEN = process.env.SUMSUB_APP_TOKEN!;
const SUMSUB_SECRET_KEY = process.env.SUMSUB_SECRET_KEY!;
function createSignature(timestamp: number, method: string, url: string, body?: string): string {
const data = timestamp + method + url + (body || "");
return crypto.createHmac("sha256", SUMSUB_SECRET_KEY).update(data).digest("hex");
}
async function createAccessToken(userId: string, levelName: string): Promise<string> {
const timestamp = Math.floor(Date.now() / 1000);
const url = `/resources/accessTokens?userId=${userId}&levelName=${levelName}&ttlInSecs=1800`;
const response = await axios.post(`https://api.sumsub.com${url}`, {}, {
headers: {
"X-App-Token": SUMSUB_APP_TOKEN,
"X-App-Access-Sig": createSignature(timestamp, "POST", url),
"X-App-Access-Ts": timestamp,
},
});
return response.data.token;
}
Webhook Processing
app.post("/webhooks/sumsub", express.raw({ type: "application/json" }), async (req, res) => {
const signature = req.headers["x-payload-digest"] as string;
const secret = process.env.SUMSUB_WEBHOOK_SECRET!;
const expected = crypto.createHmac("sha256", secret).update(req.body).digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).send("Invalid signature");
}
const payload = JSON.parse(req.body.toString());
switch (payload.type) {
case "applicantReviewed":
await handleApplicantReviewed(payload);
break;
case "applicantPending":
await handleApplicantPending(payload.applicantId);
break;
case "applicantPersonalInfoChanged":
await handlePersonalInfoChanged(payload.applicantId);
break;
}
res.status(200).send("OK");
});
async function handleApplicantReviewed(payload: any) {
const { applicantId, reviewResult } = payload;
const userId = await getUserByApplicantId(applicantId);
if (reviewResult.reviewAnswer === "GREEN") {
await approveUser(userId, applicantId);
} else if (reviewResult.reviewAnswer === "RED") {
const reasons = reviewResult.reviewRejectType; // array of reasons
await rejectUser(userId, reasons);
} else if (reviewResult.reviewAnswer === "YELLOW") {
// Requires manual review by compliance officer
await flagForManualReview(userId, applicantId);
}
}
Frontend SDK (React)
import SumsubWebSdk from "@sumsub/websdk";
import { useEffect, useRef } from "react";
interface KYCWidgetProps {
userId: string;
levelName: string;
onApproved: () => void;
onRejected: (reason: string) => void;
}
export function KYCWidget({ userId, levelName, onApproved, onRejected }: KYCWidgetProps) {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
let sdk: any;
async function initSDK() {
const { accessToken } = await fetch("/api/kyc/token", {
method: "POST",
body: JSON.stringify({ userId, levelName }),
headers: { "Content-Type": "application/json" },
}).then(r => r.json());
sdk = SumsubWebSdk.init(accessToken, () => refreshKYCToken(userId), {
lang: "en",
onMessage: (type: string, payload: any) => {
if (type === "idCheck.onApplicantStatusChanged") {
if (payload.reviewResult?.reviewAnswer === "GREEN") onApproved();
if (payload.reviewResult?.reviewAnswer === "RED") {
onRejected(payload.reviewResult.reviewRejectType?.[0] || "unknown");
}
}
},
});
sdk.launch(containerRef.current);
}
initSDK();
return () => sdk?.destroy();
}, [userId]);
return <div ref={containerRef} style={{ minHeight: "600px" }} />;
}
Onfido Integration (for EU Market)
import { DefaultApi, Configuration } from "@onfido/api";
const onfido = new DefaultApi(new Configuration({ apiToken: ONFIDO_API_TOKEN }));
// Create applicant
const applicant = await onfido.createApplicant({
firstName: "Ivan",
lastName: "Petrov",
email: "[email protected]",
});
// SDK token for frontend
const sdkToken = await onfido.generateSdkToken({
applicantId: applicant.id,
referrer: "https://yoursite.com/*",
});
// Start check after document upload
const check = await onfido.createCheck({
applicantId: applicant.id,
reportNames: ["document", "facial_similarity_photo", "watchlist_enhanced"],
});
Onfido uses watchlist_enhanced for PEP/sanctions screening in the same request — convenient for EU compliance.
Complete KYC provider integration (backend API + webhook + frontend SDK + admin UI) — 2-3 weeks.







