sumsub-integrate-websdk
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSumsub — WebSDK integration
Sumsub — WebSDK 集成
Embed Sumsub KYC into a web project end-to-end, from level creation to the
"applicantReviewed" webhook that gates user access.
将Sumsub KYC端到端嵌入Web项目,从等级创建到用于控制用户访问权限的"applicantReviewed" Webhook。
⚠️ Sandbox tokens only
⚠️ 仅使用沙箱令牌
Do not accept or use a production App Token / secret during integration
work with this skill. The token generates real SDK sessions tied to real
applicants. Insist on a sandbox pair from
https://cockpit.sumsub.com/checkus/devSpace/appTokens — toggle the workspace
to Sandbox first, then Create. Token + secret are revealed once at
creation; copy both before closing the dialog. Helper scripts in sibling
skills enforce this with an prefix check; the curl recipes below
assume the same.
sbx:Deeper auth mechanics: .
sumsub-api-auth在使用本技能进行集成工作时,请勿接受或使用生产环境的App Token/密钥。该令牌会生成与真实申请人绑定的真实SDK会话。请从https://cockpit.sumsub.com/checkus/devSpace/appTokens获取沙箱令牌对——先将工作区切换为Sandbox,再点击Create。令牌和密钥仅在创建时显示,请在关闭对话框前复制两者。配套技能中的辅助脚本会通过前缀检查强制要求使用沙箱令牌;以下curl示例也遵循此规则。
sbx:更详细的认证机制:。
sumsub-api-authThe lifecycle in one picture
完整流程示意图
┌─────────────────────────┐
│ 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.
└─────────────────────────┘The split between browser events (UX) and webhooks + server reads
(authoritative truth) is the most-missed part of a WebSDK integration. Don't
trust for entitlement decisions.
onApplicantStatusChanged ┌─────────────────────────┐
│ 1. 工作区中已存在等级 │ ← 一次性操作,在控制台完成或
│ │ 通过sumsub-create-level实现
└─────────────┬───────────┘
│ levelName
┌─────────────▼───────────┐ ┌──────────────────────────────────┐
│ 2. 服务端令牌接口 │◀─┤ 浏览器调用 /api/sumsub/token │
│ (HMAC签名的POST请求 │ └──────────────────────────────────┘
│ /resources/accessTokens)
└─────────────┬───────────┘
│ {token, userId}
┌─────────────▼───────────┐
│ 3. 浏览器端:snsWebSdk │ ← 用户上传文件/自拍/填写表单
│ 初始化→构建→启动 │ 触发事件:onApplicantSubmitted等
└─────────────┬───────────┘
│ 文件已提交
┌─────────────▼───────────┐ ┌──────────────────────────────────┐
│ 4. Sumsub执行检查 │─▶│ Webhook POST请求发送至你的服务端 │
│ (异步操作,耗时几秒到几分钟) │ │ (applicantReviewed为权威状态信号) │
└─────────────┬───────────┘ └──────────────────────────────────┘
│ 审核结果
┌─────────────▼───────────┐
│ 5. 你的应用控制用户访问 │ ← 服务端检查reviewAnswer,而非
│ 通过GET /applicants…读取申请人信息 │ 浏览器事件。浏览器事件仅用于UX展示。
└─────────────────────────┘浏览器事件(UX展示)与Webhook+服务端读取(权威状态)的分离是WebSDK集成中最容易被忽略的部分。请勿依赖进行权限决策。
onApplicantStatusChangedStage 1 — Have a level
阶段1 — 准备好等级
Every SDK launch references a that exists in the workspace.
levelNameIf the user has one (e.g. , the Sumsub default), capture it
and move to Stage 2.
basic-kyc-levelIf the user doesn't yet have a level, brainstorm with them and hand off to
. Don't silently pick
defaults — the level encodes who can verify (country / applicant type) and
what they must provide (ID, selfie, PoA, questionnaire). A reasonable
starter flow when the user is genuinely unsure:
sumsub-create-level- — name, DOB, country, addresses.
APPLICANT_DATA - —
IDENTITY,PASSPORT,ID_CARD(modeDRIVERS).any - —
SELFIE.videoRequired: passiveLiveness
Add only if regulatory; add only if
they need structured data (source of funds, occupation). For each addition,
ask "what decision does this gate?" before agreeing to include it.
PROOF_OF_RESIDENCEQUESTIONNAIRE每次SDK启动都会引用工作区中已存在的。
levelName如果用户已有等级(例如,Sumsub默认等级),记录该等级并进入阶段2。
basic-kyc-level如果用户还没有等级,与他们讨论需求后转至。请勿默认选择等级——等级定义了可验证对象(国家/申请人类型)和所需提交的材料(身份证、自拍、居住证明、问卷)。当用户不确定时,一个合理的初始流程建议:
sumsub-create-level- — 姓名、出生日期、国家、地址。
APPLICANT_DATA - —
IDENTITY、PASSPORT、ID_CARD(模式为DRIVERS)。any - —
SELFIE。videoRequired: passiveLiveness
仅在合规要求时添加;仅在需要结构化数据(资金来源、职业)时添加。每次添加前,先询问“这将用于控制什么权限?”再确认是否包含。
PROOF_OF_RESIDENCEQUESTIONNAIREStage 2 — Server-side access-token endpoint
阶段2 — 服务端访问令牌接口
The SDK needs an access token, generated by your backend with the App
Token + secret. The token is short-lived (, default 1800) and scoped to one pair.
ttlInSecs(userId, levelName)SDK需要访问令牌,由你的后端使用App Token+密钥生成。令牌有效期较短(,默认1800秒),且仅对单个对生效。
ttlInSecs(userId, levelName)Endpoint shape
接口格式
POST https://api.sumsub.com/resources/accessTokens
?userId=<your-stable-user-id>
&levelName=<level-from-stage-1>
&ttlInSecs=600- Body: empty.
- Auth: App Token + HMAC signature (see ).
sumsub-api-auth - Response: .
{ "token": "_act-sbx-<...>", "userId": "..." }
POST https://api.sumsub.com/resources/accessTokens
?userId=<你的稳定用户ID>
&levelName=<阶段1的等级名称>
&ttlInSecs=600- 请求体:空。
- 认证:App Token + HMAC签名(参见)。
sumsub-api-auth - 响应:。
{ "token": "_act-sbx-<...>", "userId": "..." }
userId
choice (load-bearing)
userIduserId
的选择(至关重要)
userIdThis is the Sumsub stores against the applicant. Make it:
externalUserId- Stable per real user (don't regenerate on each page load — the SDK looks up returning applicants by this id).
- Opaque to the user (a UUID or DB row id; not their email).
- Tied to your auth system (so a webhook callback can resolve it back to a user record).
Wrong choice → duplicate applicants, "stuck in submitted" support
tickets, and the inability to resume an interrupted verification.
userId这是Sumsub存储的申请人****。请确保:
externalUserId- 每个真实用户对应一个稳定的ID(不要在每次页面加载时重新生成——SDK通过此ID识别返回的申请人)。
- 对用户不透明(使用UUID或数据库行ID;而非用户邮箱)。
- 与你的认证系统绑定(以便Webhook回调可以将其解析回用户记录)。
错误的选择会导致申请人重复、“提交后卡住”的支持工单,以及无法恢复中断的验证流程。
userIdCurl recipe
Curl示例
bash
SUMSUB_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}"URL-encode if it might contain , , or . The signing string
must match the URI on the wire exactly — sign the encoded form.
userId/?&bash
SUMSUB_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}"如果可能包含、或,请进行URL编码。签名字符串必须与实际传输的URI完全一致——对编码后的形式进行签名。
userId/?&Wiring it into the user's backend
集成到用户的后端
Frame the endpoint as:
- Path: any (e.g. ).
POST /api/sumsub/access-token - Inputs: the authenticated user's id, the levelName (often hardcoded per page).
- Auth: user must be logged in to your app — anyone hitting this route
can spin up a verification session for that .
userId - Output: forward Sumsub's response body verbatim, or just the field. Don't cache it server-side; the browser asks per launch.
token
Show the snippet for the user's actual stack (Express, FastAPI, Go, etc.)
but the contract is the same in all of them: sign, call Sumsub, return token.
将该接口定义为:
- 路径:任意(例如)。
POST /api/sumsub/access-token - 输入:已认证用户的ID、levelName(通常在页面中硬编码)。
- 认证:用户必须已登录你的应用——任何访问此路由的人都可以为该创建验证会话。
userId - 输出:直接返回Sumsub的响应体,或仅返回字段。请勿在服务端缓存令牌;浏览器每次启动SDK时请求即可。
token
根据用户的技术栈(Express、FastAPI、Go等)展示代码片段,但所有栈的契约都是相同的:签名、调用Sumsub、返回令牌。
Stage 3 — Frontend SDK init
阶段3 — 前端SDK初始化
Load the builder
加载构建器
html
<script src="https://static.sumsub.com/idensic/static/sns-websdk-builder.js"></script>This exposes the global . For bundler-based projects, an npm
package exists but the CDN script is what Sumsub officially documents and
what every framework wrapper ends up calling.
snsWebSdkhtml
<script src="https://static.sumsub.com/idensic/static/sns-websdk-builder.js"></script>这会暴露全局变量。对于使用打包工具的项目,存在npm包,但CDN脚本是Sumsub官方文档推荐的方式,也是所有框架封装最终调用的底层实现。
snsWebSdkContainer
容器
html
<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>Give the stage a defined (e.g. ) so the iframe doesn't
collapse before the SDK adapts its height.
min-height600pxDon't skip the overlay loader. Between returning and the SDK
iframe loading content from there is a
1–3s window where the container holds an empty iframe and looks broken —
especially inside a modal that the user just opened. Mount a loader that
covers the container, then hide it in the handler (Stage 4).
Treating as informational and leaving the handler empty is the
single most common "the widget is blank" report.
.launch()api.sumsub.com/websdk/websdk.htmlidCheck.onReadyonReadyhtml
<div class="kyc-stage" style="position: relative; min-height: 600px;">
<div id="sumsub-websdk-container"></div>
<!-- 覆盖加载器用于填充空iframe窗口。在idCheck.onReady事件触发时隐藏。 -->
<div id="kyc-loader" style="position: absolute; inset: 0; display: grid; place-items: center;">
加载验证组件…
</div>
</div>为容器设置明确的(例如),以便在SDK自适应高度前iframe不会塌陷。
min-height600px不要跳过覆盖加载器。在返回后到SDK iframe从加载内容之间,有1-3秒的窗口,容器中是空白iframe,看起来像是加载失败——尤其是在用户刚打开的模态框中。添加一个覆盖容器的加载器,然后在阶段4的处理器中隐藏它。将视为信息性事件而不添加处理器,是“组件显示空白”报告的最常见原因。
.launch()api.sumsub.com/websdk/websdk.htmlidCheck.onReadyonReadyCanonical vanilla launch
标准原生实现示例
See for a runnable file.
Minimal shape:
examples/vanilla.htmljs
async 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/vanilla.htmljs
async function getAccessToken() {
const r = await fetch('/api/sumsub/access-token', { method: 'POST' });
if (!r.ok) throw new Error('获取访问令牌失败');
return (await r.json()).token;
}
const initialToken = await getAccessToken();
const sdk = snsWebSdk
.init(initialToken, () => getAccessToken()) // 刷新回调,返回Promise<string>
.withConf({
lang: 'en',
email: currentUser.email, // 可选,预填充
phone: currentUser.phone, // 可选,预填充
theme: 'light', // 'light' | 'dark'
})
.withOptions({
addViewportTag: false, // 宿主页面已设置viewport标签
adaptIframeHeight: true,
})
.on('idCheck.onReady', () => {
// SDK iframe内容已加载——隐藏容器代码中的覆盖加载器
document.getElementById('kyc-loader')?.style.setProperty('display', 'none');
})
.on('idCheck.onApplicantSubmitted', () => {
// 用户刚完成上传;显示“审核中”提示
})
.on('idCheck.onApplicantStatusChanged', (payload) => {
// 状态变更;payload.reviewStatus = 'pending' | 'queued' | 'completed' | ...
})
.on('idCheck.onError', (err) => {
console.error('sumsub错误', err);
})
.onMessage((type, payload) => {
// 通用事件监听——用于分析或调试
})
.build();
sdk.launch('#sumsub-websdk-container');React recipe
React方案
examples/react-component.tsxuseEffect- The CDN script must be present before is read. Either inject it once in the document head, or dynamically load it and
snsWebSdktheawait's<script>event.load - On React 18 strict mode in dev, components mount twice — the cleanup function must remove the iframe / clear the container, otherwise you get two stacked widgets.
examples/react-component.tsxuseEffect- 读取前必须加载CDN脚本。要么在文档头部注入一次,要么动态加载并等待
snsWebSdk的<script>事件。load - 在React 18开发严格模式下,组件会挂载两次——清理函数必须移除iframe/清空容器,否则会出现两个堆叠的组件。
Other frameworks
其他框架
The builder API is framework-agnostic. For Vue/Svelte/Angular, mirror the
React pattern: lifecycle hook on mount → fetch token → build → launch into a
ref'd element; on unmount → empty the container.
构建器API与框架无关。对于Vue/Svelte/Angular,可参考React模式:挂载时触发生命周期钩子→获取令牌→构建→启动到引用元素;卸载时→清空容器。
Stage 4 — Client lifecycle events
阶段4 — 客户端生命周期事件
Wire these handlers on the SDK instance. Treat them as UX signals, not
authoritative state.
| 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 |
Full payload fields per event: .
references/lifecycle.md在SDK实例上绑定这些处理器。将它们视为UX信号,而非权威状态。
| 事件 | 触发时机 | 用途 |
|---|---|---|
| SDK iframe内容加载完成 | 隐藏自定义加载器。必须实现——否则调用 |
| 首个界面渲染完成 | 分析:“用户看到KYC步骤” |
| 文档类型界面显示 | 按文档类型统计遥测数据 |
| 单个步骤完成 | 更新进度条 |
| 文件提交完成,服务端开始处理 | 将用户切换到“审核中”视图 |
| 状态变更 | 实时进度提示(仍不可信) |
| 拒绝后重新上传 | 重置“审核中”视图 |
| 客户端收到最终审核结果 | 显示初步结果,然后在服务端验证 |
| SDK出错 | 显示友好的重试提示,记录 |
| 文件上传时被拒绝 | 内联提示(“照片模糊”等) |
| 活体检测完成 | 根据 |
| 框架尺寸变更 | 调整周边布局 |
每个事件的完整负载字段:。
references/lifecycle.mdRequired handlers for a baseline integration
基础集成必须实现的处理器
If you wire nothing else, wire these three. Skipping any of them produces a
known-bad UX:
- → hide the overlay loader from Stage 3. Without this the modal looks blank for 1–3s after
idCheck.onReady..launch() - → move the user to a "we're reviewing" state. Without this the user re-uploads or contacts support.
idCheck.onApplicantSubmitted - → render a retryable error to the UI. Without this failures only land in
idCheck.onErrorand the user sees a stuck loader.console.error
如果只实现三个处理器,请选择以下三个。跳过任何一个都会导致不良的用户体验:
- → 隐藏阶段3中的覆盖加载器。不实现此处理器的话,调用
idCheck.onReady后模态框会空白1-3秒。.launch() - → 将用户切换到“审核中”状态。不实现此处理器的话,用户会重复上传或联系支持。
idCheck.onApplicantSubmitted - → 在UI中显示可重试的错误提示。不实现此处理器的话,错误只会出现在
idCheck.onError中,用户看到的是卡住的加载器。console.error
Why you can't trust onApplicantReviewed
alone
onApplicantReviewed为何不能仅依赖onApplicantReviewed
onApplicantReviewedThe browser event fires from inside the iframe. A bad actor can spoof it
trivially. The only authoritative signal is server-side: either a
webhook delivery (Stage 5) or an authenticated GET against
.
/resources/applicants/{userId}/one浏览器事件在iframe内部触发,恶意攻击者可以轻易伪造它。唯一的权威信号是服务端:要么是Webhook推送(阶段5),要么是通过认证的GET请求访问。
/resources/applicants/{userId}/oneStage 5 — Server-side source of truth
阶段5 — 服务端权威状态获取
Webhook receiver
Webhook接收器
Sumsub POSTs JSON to your URL on every event. Two paths for registering it:
- Sandbox (while building this integration): use the
skill. It builds the
sumsub-manage-webhookspayload from a compact spec, POSTs toclientWebhookswith App Token auth, refuses non-/resources/api/agent/clientWebhookstokens, rejectssbx:/localhosttargets up front, and walks the user through exposing their local receiver via127.0.0.1so Sumsub can actually reach it. Hand off thengrok http <port>,target, andtypes[]the user wants and let that skill do the POST.signatureAlgorithm - Production: do not create the prod webhook from any skill, including this one. Production webhook setup must be done by a human directly in the Sumsub dashboard (Integrations → Webhooks, workspace toggle on Production). The signing secret authenticates real PII deliveries; the audit trail should attribute setup to a person. Prototype the spec against sandbox here, then hand the final settings (target, event list, signature algorithm, custom headers) to whoever has prod access to recreate manually.
Headers you care about:
- — the signature, hex-encoded.
x-payload-digest - —
x-payload-digest-alg(default),HMAC_SHA256_HEX, or the legacyHMAC_SHA512_HEX.HMAC_SHA1_HEX
The signing secret is not your App Token secret. It's a separate
webhook secret generated (or supplied) at webhook-creation time in the
dashboard. Store it in env () alongside the App Token
pair.
SUMSUB_WEBHOOK_SECRETVerification recipe — note the raw bytes requirement; do NOT JSON-parse
before computing the digest, because re-serialising changes whitespace and
key order:
js
// 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'));
}See for a
complete handler.
examples/webhook-verify.jsSumsub会在每个事件发生时向你的URL发送POST请求。有两种注册方式:
- 沙箱环境(集成开发时):使用技能。它会根据简洁的规范构建
sumsub-manage-webhooks负载,使用App Token认证POST到clientWebhooks,拒绝非/resources/api/agent/clientWebhooks开头的令牌,直接拒绝sbx:/localhost目标,并引导用户通过127.0.0.1暴露本地接收器,以便Sumsub可以访问。提供用户需要的ngrok http <port>、target和types[],让该技能完成POST请求。signatureAlgorithm - 生产环境:请勿使用任何技能(包括本技能)创建生产环境Webhook。生产环境Webhook必须由人工直接在Sumsub控制台中设置(集成→Webhooks,将工作区切换为Production)。签名密钥用于认证真实的PII推送;审计记录应将设置归因于具体人员。在此处针对沙箱环境原型化规范,然后将最终设置(目标、事件列表、签名算法、自定义头部)交给拥有生产环境权限的人员手动创建。
需要关注的头部:
- — 签名,十六进制编码。
x-payload-digest - —
x-payload-digest-alg(默认)、HMAC_SHA256_HEX或旧版HMAC_SHA512_HEX。HMAC_SHA1_HEX
签名密钥不是你的App Token密钥。它是在控制台创建Webhook时生成(或提供)的独立Webhook密钥。将其与App Token对一起存储在环境变量中()。
SUMSUB_WEBHOOK_SECRET验证示例——注意原始字节的要求;计算摘要前不要进行JSON解析,因为重新序列化会改变空格和键的顺序:
js
// Node/Express — 使用bodyParser.raw()确保req.body是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.jsLocal testing with ngrok
使用ngrok进行本地测试
Sumsub needs a publicly reachable URL — your laptop's won't do.
The fastest end-to-end loop for local dev:
localhostbash
undefinedSumsub需要可公开访问的URL——你的笔记本电脑的无法满足要求。本地开发最快的端到端流程:
localhostbash
undefined1. In one shell, start your local receiver (Node, Python, whatever).
1. 在一个终端中启动本地接收器(Node、Python等)。
node server.js # listens on http://localhost:3000
node server.js # 监听http://localhost:3000
2. In another shell, tunnel that port.
2. 在另一个终端中建立隧道。
ngrok http 3000 # prints https://<random>.ngrok-free.app -> http://localhost:3000
ngrok http 3000 # 输出https://<随机字符串>.ngrok-free.app -> http://localhost:3000
3. Register the webhook against the ngrok URL.
3. 针对ngrok URL注册Webhook。
Either via the dashboard (Integrations → Webhooks) OR via
可以通过控制台(集成→Webhooks)或
sumsub-manage-webhooks create
with target = the ngrok https URL.
create使用sumsub-manage-webhooks的create
命令,target为ngrok的https URL。
create4. Trigger an event by running a sandbox WebSDK verification end-to-end.
4. 完成一次沙箱WebSDK验证流程来触发事件。
Watch the request arrive in your local server logs.
在本地服务器日志中查看请求是否到达。
5. When you're happy, PATCH the webhook to point at your real server
5. 测试通过后,更新Webhook指向真实服务器域名
hostname (sumsub-manage-webhooks update
command).
update使用sumsub-manage-webhooks的update
命令。
update
`webhook.site` works too if you just want to see the raw payload without
running a local receiver — but it can't echo back a `200` to verify the
delivery flow.
如果你只想查看原始负载而不想运行本地接收器,`webhook.site`也可以使用——但它无法返回`200`状态码来验证推送流程。Webhook events that matter for a KYC flow
KYC流程中重要的Webhook事件
| 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.reviewAnswer- — approved.
GREEN - — rejected.
REDsays why;rejectLabelsisreviewRejectType(can't retry) orFINAL(user may resubmit).RETRY
Full list: Sumsub webhook docs.
| 含义 | 操作 |
|---|---|---|
| 首次为此 | 记录日志;无需其他操作 |
| 用户完成上传;Sumsub开始检查 | 显示“审核中” |
| 主要数据处理完成,等待人工/AML审核 | 仍显示“审核中” |
| 暂停审核(通常是AML命中需要分析师处理) | 通知运营人员;告知用户“正在进行额外检查” |
| 最终审核结果—— | 在此处控制访问权限。标记用户为已验证或拒绝。 |
| 用户提交后编辑了信息 | 授予访问权限前重新检查 |
| 整个工作流(多等级)完成 | 单等级流程中与 |
| 单次操作(与等级流程分离) | 根据操作类型处理 |
reviewResult.reviewAnswer- — 通过审核。
GREEN - — 审核拒绝。
RED说明原因;rejectLabels为reviewRejectType(不可重试)或FINAL(用户可重新提交)。RETRY
完整列表:Sumsub Webhook文档。
Server-side status check (fallback / on-demand)
服务端状态检查( fallback / 按需查询)
For pages that need to check status synchronously (e.g. user logs back in
between webhook arriving and your DB updating):
bash
PATH_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}"Reads and . Cheap; use as a
fallback, not as a polling loop — webhooks are the primary signal.
reviewStatusreviewResult.reviewAnswer对于需要同步检查状态的页面(例如用户在Webhook到达与数据库更新之间重新登录):
bash
PATH_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}"读取和。该请求开销低;仅作为fallback使用,不要轮询——Webhook是主要信号。
reviewStatusreviewResult.reviewAnswerResumption / returning users
恢复/返回用户的处理
The SDK looks up applicants by . If a user starts
verification, abandons, and returns 3 days later:
externalUserId- Your endpoint mints a new access token for the same + same
userId.levelName - The SDK opens to wherever the user left off (re-uploads only the missing steps).
- No duplicate applicant is created.
Don't generate a new for returning users — that's the #1 cause of
"the user is stuck and customer support sees two applicants".
userIdSDK通过查找申请人。如果用户开始验证后中断,3天后返回:
externalUserId- 你的接口为相同的+相同的
userId生成新的访问令牌。levelName - SDK会打开到用户上次中断的位置(仅重新上传缺失的步骤)。
- 不会创建重复的申请人。
不要为返回的用户生成新的——这是“用户卡住且客服看到两个申请人”的头号原因。
userIdToken refresh
令牌刷新
The first argument to covers expiry mid-session:
.init(token, refreshCallback)- SDK calls your when the token nears expiry.
refreshCallback - It must return resolving to a fresh token (call your endpoint again).
Promise<string> - If you return a stale or wrong-token, the SDK hangs.
userId
For most flows a 600-second TTL is plenty. Don't pre-fetch and cache —
mint on demand.
.init(token, refreshCallback)- 当令牌即将过期时,SDK会调用你的。
refreshCallback - 该回调必须返回,解析为新的令牌(再次调用你的接口)。
Promise<string> - 如果返回过期或错误的令牌,SDK会挂起。
userId
对于大多数流程,600秒的TTL足够。不要预获取和缓存——按需生成即可。
Sandbox testing
沙箱测试
In sandbox mode:
- Use test documents from the Sumsub docs ("Test documents" page) to
trigger vs
GREENoutcomes without uploading real PII.RED - AML hits are simulated — names like go through;
Greenacreand similar are pre-loaded as positive matches.Aikman - Webhook delivery works the same. For a local receiver, expose it through
(or Cloudflare Tunnel / Tailscale Funnel) and register the public URL via the
ngrok http <port>skill — it has the full walkthrough and rejects rawsumsub-manage-webhookstargets before they ever reach Sumsub.localhostis fine for inspecting payload shapes without a real receiver.webhook.site - Selfies in sandbox bypass real biometrics — any face works.
Sandbox tokens () only fire against the sandbox workspace. Production
tokens () only fire against production. There's no fall-through.
sbx:prd:在沙箱模式下:
- 使用Sumsub文档(“测试文档”页面)中的测试文档来触发或
GREEN结果,无需上传真实的个人身份信息。RED - AML命中是模拟的——类似的姓名会通过;
Greenacre等姓名预加载为匹配命中。Aikman - Webhook推送工作方式相同。对于本地接收器,通过(或Cloudflare Tunnel / Tailscale Funnel)暴露,并使用
ngrok http <port>技能注册公开URL——该技能包含完整的引导流程,并会直接拒绝原始sumsub-manage-webhooks目标。如果不需要真实接收器,localhost可用于查看负载格式。webhook.site - 沙箱中的自拍会绕过真实生物识别——任何人脸都可通过。
沙箱令牌()仅对沙箱工作区生效。生产令牌()仅对生产环境生效。两者不会交叉生效。
sbx:prd:Going live checklist
上线检查清单
When the user says "we're ready to switch to prod":
- Webhook receiver verifies the signature on raw bytes (replay Stage 5 test against a real delivery).
- is the stable user id, not the email / display name.
externalUserId - Server is the source of truth for verification state. Browser events may be ignored entirely.
- Token endpoint is auth-gated (only logged-in users can mint a token for themselves).
- triggers the user-facing state change in your DB, with idempotency (the same event can arrive twice).
applicantReviewed - Per-user retry handling: ⇒ let user re-launch the SDK;
reviewResult.reviewRejectType === 'RETRY'⇒ block.'FINAL' - Production App Token + secret + separate webhook secret are all in the prod secret store. Sandbox values stay only in dev env.
当用户表示“准备切换到生产环境”时:
- Webhook接收器对原始字节验证签名(针对真实推送重新测试阶段5)。
- 是稳定的用户ID,而非邮箱/显示名称。
externalUserId - 服务端是验证状态的权威来源。可完全忽略浏览器事件。
- 令牌接口已做权限控制(仅已登录用户可为自己生成令牌)。
- 触发你的数据库中用户状态的变更,并实现幂等性(同一事件可能推送两次)。
applicantReviewed - 按用户处理重试:⇒ 允许用户重新启动SDK;
reviewResult.reviewRejectType === 'RETRY'⇒ 阻止用户。'FINAL' - 生产环境的App Token+密钥+独立的Webhook密钥都已存储在生产环境密钥管理系统中。沙箱值仅保留在开发环境。
See also
相关链接
- — full event catalog with payload fields, plus webhook event reference.
references/lifecycle.md - — runnable single-file integration.
examples/vanilla.html - — React hook + cleanup pattern.
examples/react-component.tsx - — signature verification with raw-body handling.
examples/webhook-verify.js - — the auth signing reference, shared with every other Sumsub skill.
sumsub-api-auth - — for the Stage-1 hand-off.
sumsub-create-level - — for the Stage-5 hand-off: create / list / update / disable sandbox webhooks via the public API, with the localhost-rejection and ngrok walkthrough built in. Production webhook setup is dashboard-only and must be done by a human.
sumsub-manage-webhooks - Sumsub docs index — authoritative source if anything in this skill drifts.
- — 完整事件目录及负载字段,以及Webhook事件参考。
references/lifecycle.md - — 可运行的单文件集成示例。
examples/vanilla.html - — React钩子+清理模式示例。
examples/react-component.tsx - — 原始体处理的签名验证示例。
examples/webhook-verify.js - — 认证签名参考,所有Sumsub技能共享。
sumsub-api-auth - — 阶段1的转至技能。
sumsub-create-level - — 阶段5的转至技能:通过公开API创建/列出/更新/禁用沙箱Webhook,内置localhost拒绝和ngrok引导流程。生产环境Webhook必须由人工在控制台设置。
sumsub-manage-webhooks - Sumsub文档索引 — 如果本技能内容有偏差,以此为权威来源。