Loading...
Loading...
End-to-end recipe for adding Sumsub KYC to a website or web app via the Sumsub WebSDK. TRIGGER when the user asks to "integrate / embed / add Sumsub", "show the KYC widget", "add WebSDK", "verify users with Sumsub on the frontend", supplies an existing levelName they want to plug into a page, or asks how to wire up access tokens / lifecycle events / webhooks for Sumsub verification in an arbitrary project. Covers the whole loop — level setup, server-side access-token signing, snsWebSdk init (vanilla canonical, React recipe), client lifecycle events, token refresh, source-of-truth via webhooks + applicant GET, sandbox testing, go-live checklist. SKIP for building the level/questionnaire/POA-preset payload itself (use the sibling skills), or for backend-only API calls with no frontend (use `sumsub-api-generic`).
npx skill4agent add sumsub/agent-skills sumsub-integrate-websdksbx:sumsub-api-auth ┌─────────────────────────┐
│ 1. Level exists in the │ ← one-time, done in dashboard or
│ workspace │ via sumsub-create-level
└─────────────┬───────────┘
│ levelName
┌─────────────▼───────────┐ ┌──────────────────────────────────┐
│ 2. Server-side token │◀─┤ Browser calls /api/sumsub/token │
│ endpoint (HMAC-signed │ └──────────────────────────────────┘
│ POST /resources/ │
│ accessTokens) │
└─────────────┬───────────┘
│ {token, userId}
┌─────────────▼───────────┐
│ 3. Browser: snsWebSdk │ ← user fills doc capture / selfie / form
│ init → build → launch │ events fire: onApplicantSubmitted, etc.
└─────────────┬───────────┘
│ documents submitted
┌─────────────▼───────────┐ ┌──────────────────────────────────┐
│ 4. Sumsub runs checks │─▶│ Webhook POST → your server │
│ (async, ~seconds–min) │ │ (applicantReviewed = the truth) │
└─────────────┬───────────┘ └──────────────────────────────────┘
│ verdict
┌─────────────▼───────────┐
│ 5. Your app gates access │ ← server checks reviewAnswer, not
│ by reading applicant │ the browser. Browser events are
│ via GET /applicants… │ UX only.
└─────────────────────────┘onApplicantStatusChangedlevelNamebasic-kyc-levelsumsub-create-levelAPPLICANT_DATAIDENTITYPASSPORTID_CARDDRIVERSanySELFIEvideoRequired: passiveLivenessPROOF_OF_RESIDENCEQUESTIONNAIREttlInSecs(userId, levelName)POST https://api.sumsub.com/resources/accessTokens
?userId=<your-stable-user-id>
&levelName=<level-from-stage-1>
&ttlInSecs=600sumsub-api-auth{ "token": "_act-sbx-<...>", "userId": "..." }userIdexternalUserIduserIdSUMSUB_APP_TOKEN='sbx:...'
SUMSUB_SECRET_KEY='...'
USER_ID='u-12345'
LEVEL='basic-kyc-level'
PATH_Q="/resources/accessTokens?userId=${USER_ID}&levelName=${LEVEL}&ttlInSecs=600"
TS=$(date -u +%s)
SIG=$(printf '%s%s%s' "$TS" "POST" "$PATH_Q" \
| openssl dgst -sha256 -hmac "$SUMSUB_SECRET_KEY" -hex \
| awk '{print $NF}')
curl -sS -X POST \
-H "X-App-Token: $SUMSUB_APP_TOKEN" \
-H "X-App-Access-Ts: $TS" \
-H "X-App-Access-Sig: $SIG" \
"https://api.sumsub.com${PATH_Q}"userId/?&POST /api/sumsub/access-tokenuserIdtoken<script src="https://static.sumsub.com/idensic/static/sns-websdk-builder.js"></script>snsWebSdk<div class="kyc-stage" style="position: relative; min-height: 600px;">
<div id="sumsub-websdk-container"></div>
<!-- Overlay loader covers the empty-iframe window. Hide on idCheck.onReady. -->
<div id="kyc-loader" style="position: absolute; inset: 0; display: grid; place-items: center;">
Loading verification…
</div>
</div>min-height600px.launch()api.sumsub.com/websdk/websdk.htmlidCheck.onReadyonReadyexamples/vanilla.htmlasync function getAccessToken() {
const r = await fetch('/api/sumsub/access-token', { method: 'POST' });
if (!r.ok) throw new Error('failed to mint access token');
return (await r.json()).token;
}
const initialToken = await getAccessToken();
const sdk = snsWebSdk
.init(initialToken, () => getAccessToken()) // refresh callback, returns Promise<string>
.withConf({
lang: 'en',
email: currentUser.email, // optional, prefills
phone: currentUser.phone, // optional, prefills
theme: 'light', // 'light' | 'dark'
})
.withOptions({
addViewportTag: false, // host page already sets it
adaptIframeHeight: true,
})
.on('idCheck.onReady', () => {
// SDK iframe content loaded — hide the overlay loader from the container snippet.
document.getElementById('kyc-loader')?.style.setProperty('display', 'none');
})
.on('idCheck.onApplicantSubmitted', () => {
// user just finished uploading; show "we're reviewing"
})
.on('idCheck.onApplicantStatusChanged', (payload) => {
// status moved; payload.reviewStatus = 'pending' | 'queued' | 'completed' | ...
})
.on('idCheck.onError', (err) => {
console.error('sumsub error', err);
})
.onMessage((type, payload) => {
// catch-all firehose — useful for analytics or debugging
})
.build();
sdk.launch('#sumsub-websdk-container');examples/react-component.tsxuseEffectsnsWebSdkawait<script>load| Event | When | Use it for |
|---|---|---|
| SDK iframe content loaded | Hide your own loader. Required — without this the modal looks empty for 1–3s after launch. |
| First screen rendered | Analytics: "user saw KYC step" |
| Doc-type screen shown | Telemetry per doc type |
| A step finished | Progress bar |
| Docs submitted, server is processing | Move user to a "waiting" view |
| Status moved | Live progress hint (still not trusted) |
| Re-upload after a rejection | Re-arm waiting view |
| Final verdict reached client-side | Show a preliminary result, then verify server-side |
| SDK error | Surface a friendly retry CTA, log |
| Doc rejected at upload | Inline guidance ("blurred photo", etc.) |
| Liveness attempt finished | Branch on |
| Frame resized | Adjust surrounding layout |
references/lifecycle.mdidCheck.onReady.launch()idCheck.onApplicantSubmittedidCheck.onErrorconsole.erroronApplicantReviewed/resources/applicants/{userId}/onesumsub-manage-webhooksclientWebhooks/resources/api/agent/clientWebhookssbx:localhost127.0.0.1ngrok http <port>targettypes[]signatureAlgorithmx-payload-digestx-payload-digest-algHMAC_SHA256_HEXHMAC_SHA512_HEXHMAC_SHA1_HEXSUMSUB_WEBHOOK_SECRET// Node/Express — bodyParser.raw() so req.body is a Buffer
import crypto from 'node:crypto';
const ALG = { HMAC_SHA1_HEX: 'sha1', HMAC_SHA256_HEX: 'sha256', HMAC_SHA512_HEX: 'sha512' };
function verifySumsubWebhook(req, secret) {
const alg = ALG[req.header('x-payload-digest-alg') || 'HMAC_SHA256_HEX'];
const expected = req.header('x-payload-digest');
const actual = crypto.createHmac(alg, secret).update(req.body).digest('hex');
return Buffer.from(actual, 'hex').length === Buffer.from(expected, 'hex').length
&& crypto.timingSafeEqual(Buffer.from(actual, 'hex'), Buffer.from(expected, 'hex'));
}examples/webhook-verify.jslocalhost# 1. In one shell, start your local receiver (Node, Python, whatever).
node server.js # listens on http://localhost:3000
# 2. In another shell, tunnel that port.
ngrok http 3000 # prints https://<random>.ngrok-free.app -> http://localhost:3000
# 3. Register the webhook against the ngrok URL.
# Either via the dashboard (Integrations → Webhooks) OR via
# sumsub-manage-webhooks `create` with target = the ngrok https URL.
# 4. Trigger an event by running a sandbox WebSDK verification end-to-end.
# Watch the request arrive in your local server logs.
# 5. When you're happy, PATCH the webhook to point at your real server
# hostname (sumsub-manage-webhooks `update` command).webhook.site200 | What it means | Action |
|---|---|---|
| First time you minted a token for this | Log; nothing required |
| User finished uploading; Sumsub is checking | Show "in review" |
| Primary data processing done, queued for human/AML | Still "in review" |
| Paused (often AML hit needing analyst) | Surface to ops; tell user "extra checks" |
| Final verdict — | Gate access here. Mark user verified or rejected. |
| User edited info after submission | Re-check before granting access |
| Whole workflow (multi-level) done | Same as |
| One-off action (separate from the level flow) | Per-action handling |
reviewResult.reviewAnswerGREENREDrejectLabelsreviewRejectTypeFINALRETRYPATH_Q="/resources/applicants/${USER_ID}/one"
TS=$(date -u +%s)
SIG=$(printf '%s%s%s' "$TS" "GET" "$PATH_Q" \
| openssl dgst -sha256 -hmac "$SUMSUB_SECRET_KEY" -hex \
| awk '{print $NF}')
curl -sS \
-H "X-App-Token: $SUMSUB_APP_TOKEN" \
-H "X-App-Access-Ts: $TS" \
-H "X-App-Access-Sig: $SIG" \
"https://api.sumsub.com${PATH_Q}"reviewStatusreviewResult.reviewAnswerexternalUserIduserIdlevelNameuserId.init(token, refreshCallback)refreshCallbackPromise<string>userIdGREENREDGreenacreAikmanngrok http <port>sumsub-manage-webhookslocalhostwebhook.sitesbx:prd:externalUserIdapplicantReviewedreviewResult.reviewRejectType === 'RETRY''FINAL'references/lifecycle.mdexamples/vanilla.htmlexamples/react-component.tsxexamples/webhook-verify.jssumsub-api-authsumsub-create-levelsumsub-manage-webhooks