关于 sumsub-integrate-websdk
用于将 Sumsub WebSDK 集成到 Web 应用的技能,涵盖令牌签发、浏览器端配置和验证回调。它强调使用沙盒凭据,并以服务端验证作为 KYC 结果的权威依据。
主要功能
- 01服务端访问令牌接口指南
- 02原生 JavaScript 与 React 启动方式
- 03客户端生命周期事件处理
- 04Webhook 签名验证
- 05优先使用沙盒的集成指南
适用场景
- 01在网站注册流程中加入 Sumsub KYC
- 02展示验证进度和错误信息
- 03通过 Webhook 确认 KYC 结果
- 04在本地开发中测试 Webhook 投递
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ sumsub/ agent-skills --skill sumsub-integrate-websdk安装后在 Claude Code 中输入/sumsub-integrate-websdk确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
examples/react-component.tsxvanilla.htmlwebhook-verify.jsreferences/lifecycle.md详细介绍
Sumsub — WebSDK 集成
将 Sumsub KYC 端到端嵌入 Web 项目,从创建等级到触发控制用户访问的 "applicantReviewed" webhook。
⚠️ 仅限沙盒令牌
在使用此技能进行集成时,请勿接受或使用生产 App Token / secret。
令牌会生成与真实申请人关联的真实 SDK 会话。请坚持使用来自
https://cockpit.sumsub.com/checkus/home?sbx=true 的一组沙盒凭据——Connect Sumsub to
your AI agent -> Build & configure -> Generate token。创建时令牌和 secret 只会显示一次;关闭对话框前请复制两者。同级技能中的辅助脚本会通过检查 sbx: 前缀来强制执行此要求;下方的 curl 示例也基于同样的前提。
更深入的身份验证机制: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.
└─────────────────────────┘
浏览器事件(UX)与 webhook + 服务端读取(权威事实)之间的区分,是 WebSDK 集成中最容易被忽略的部分。不要依赖 onApplicantStatusChanged 来决定权限。
阶段 1 — 准备一个等级
每次启动 SDK 都会引用工作区中已存在的 levelName。
如果用户已有等级(例如 Sumsub 默认的 basic-kyc-level),请记录该等级并进入阶段 2。
如果用户还没有等级,请与他们讨论需求,然后交由
sumsub-create-level 处理。不要默默选择默认值——等级决定哪些人可以验证(国家 / 申请人类型),以及他们必须提供哪些材料(身份证件、人脸自拍、居住证明、问卷)。如果用户确实不确定,以下是一个合理的入门流程:
APPLICANT_DATA— 姓名、出生日期、国家、地址。IDENTITY—PASSPORT、ID_CARD、DRIVERS(模式any)。SELFIE—videoRequired: passiveLiveness。
仅在法规要求时添加 PROOF_OF_RESIDENCE;仅在需要结构化数据(资金来源、职业)时添加 QUESTIONNAIRE。每次添加前,都要先问:“这项要求用于做出什么决定?”
阶段 2 — 服务端访问令牌端点
SDK 需要一个访问令牌,由你的后端使用 App
Token + secret 生成。令牌有效期很短(ttlInSecs,默认 1800),且仅限于一个 (userId, levelName) 对。
端点形式
POST https://api.sumsub.com/resources/accessTokens
?userId=<your-stable-user-id>
&levelName=<level-from-stage-1>
&ttlInSecs=600
- 请求体:空。
- 身份验证:App Token + HMAC 签名(参见
sumsub-api-auth)。 - 响应:
{ "token": "_act-sbx-<...>", "userId": "..." }。
userId 的选择(至关重要)
这是 Sumsub 存储在申请人记录中的 externalUserId。请确保它:
- 对每个真实用户保持稳定(不要在每次页面加载时重新生成——SDK 会通过此 ID 查找返回的申请人)。
- 对用户不可识别(使用 UUID 或数据库行 ID,而不是邮箱)。
- 与你的身份验证系统关联(这样 webhook 回调就能将其解析为对应的用户记录)。
错误选择 userId 会导致申请人重复、出现“卡在已提交状态”的支持工单,以及无法恢复中断的验证。
Curl 示例
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}"
如果 userId 可能包含 /、? 或 &,请对其进行 URL 编码。签名字符串必须与线上请求 URI 完全一致——请对编码后的形式签名。
接入用户的后端
将端点设计为:
- 路径:任意(例如
POST /api/sumsub/access-token)。 - 输入:已通过身份验证的用户 ID、
levelName(通常按页面硬编码)。 - 身份验证:用户必须登录你的应用——任何访问此路由的人都可以为该
userId创建验证会话。 - 输出:原样转发 Sumsub 的响应正文,或仅返回
token字段。不要在服务端缓存令牌;浏览器每次启动时都会请求令牌。
根据用户实际使用的技术栈展示代码片段(Express、FastAPI、Go 等),但契约都相同:签名、调用 Sumsub、返回令牌。
阶段 3 — 前端 SDK 初始化
加载构建器
<script src="https://static.sumsub.com/idensic/static/sns-websdk-builder.js"></script>
这会暴露全局变量 snsWebSdk。基于打包工具的项目可以使用 npm 包,但 Sumsub 官方文档记录的是 CDN 脚本,所有框架封装最终也都会调用它。
容器
<div class="kyc-stage" style="position: relative; min-height: 600px;">
<div id="sumsub-websdk-container"></div>
<div id="kyc-loader" style="position: absolute; inset: 0; display: grid; place-items: center;">
Loading verification…
</div>
</div>
为舞台设置明确的 min-height(例如 600px),以免 iframe 在 SDK 调整其高度之前折叠。
不要跳过覆盖层加载器。 从 .launch() 返回到 SDK iframe 加载 api.sumsub.com/websdk/websdk.html 中的内容之间,有 1–3 秒的间隔,此时容器中只有一个空 iframe,看起来像是出了问题——尤其是在用户刚打开的模态框中。挂载一个覆盖容器的加载器,然后在 idCheck.onReady 处理程序中将其隐藏(阶段 4)。把 onReady 当作提示信息处理并留空,是最常见的“组件显示空白”问题。
规范的原生 JavaScript 启动方式
可运行的文件见 examples/vanilla.html。
最简结构:
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');
React 方案
examples/react-component.tsx — 在带清理逻辑的 useEffect 中封装同一个构建器。它处理了两个容易踩坑的地方:
- 读取
snsWebSdk之前,必须先加载 CDN 脚本。可以在文档 head 中注入一次,也可以动态加载并await<script>的load事件。 - 在开发环境的 React 18 严格模式下,组件会挂载两次——清理函数必须移除 iframe/清空容器,否则会出现两个堆叠的组件。
其他框架
构建器 API 与框架无关。对于 Vue/Svelte/Angular,可参照 React 方案:挂载时执行生命周期钩子 → 获取令牌 → 构建 → 启动到引用的元素中;卸载时 → 清空容器。
阶段 4 — 客户端生命周期事件
在 SDK 实例上注册这些处理程序。将它们视为用户体验信号,而不是权威状态。
| 事件 | 触发时机 | 用途 |
|---|---|---|
idCheck.onReady | SDK iframe 内容已加载 | 隐藏自定义加载器。 必须处理——否则启动后 1–3 秒内模态框看起来是空的。 |
idCheck.onInitialized | 首个屏幕已渲染 | 分析:“用户看到了 KYC 步骤” |
idCheck.onStepInitiated | 文档类型屏幕已显示 | 按文档类型收集遥测数据 |
idCheck.onStepCompleted | 某个步骤已完成 | 进度条 |
idCheck.onApplicantSubmitted | 文档已提交,服务端正在处理 | 将用户切换到“等待”视图 |
idCheck.onApplicantStatusChanged | 状态已变更 | 实时进度提示(仍不可信) |
idCheck.onApplicantResubmitted | 被拒后重新上传 | 重新启用等待视图 |
idCheck.onApplicantReviewed(或 2.0 中的 onApplicantVerificationCompleted) | 客户端已得出最终判定 | 显示初步结果,然后在服务端验证 |
idCheck.onError | SDK 出错 | 显示友好的重试 CTA,并记录 code + reason |
idCheck.onUploadError / onUploadWarning | 上传时文档被拒 | 提供行内指导(例如“照片模糊”) |
idCheck.onLivenessCompleted(仅限 2.0) | 活体检测尝试已结束 | 根据 answer 分支处理重试体验 |
idCheck.onResize | 框架大小已调整 | 调整周围布局 |
各事件的完整负载字段见:references/lifecycle.md。
基础集成必须注册的处理程序
如果不注册其他处理程序,至少要注册以下三个。漏掉任何一个都会导致已知的不良用户体验:
idCheck.onReady→ 隐藏阶段 3 中的覆盖层加载器。不处理时,.launch()后 1–3 秒内模态框看起来是空的。idCheck.onApplicantSubmitted→ 将用户切换到“我们正在审核”状态。不处理时,用户会重新上传或联系支持人员。idCheck.onError→ 在 UI 中显示可重试的错误。不处理时,失败只会记录到console.error中,而用户会看到一直卡住的加载器。
为什么不能只相信 onApplicantReviewed
浏览器事件在 iframe 内触发。恶意行为者可以轻易伪造它。唯一权威的信号来自服务端:Webhook 投递(阶段 5),或通过身份验证的 GET 请求 /resources/applicants/{userId}/one。
阶段 5 — 服务端真实数据源
Webhook 接收器
Sumsub 会在每次发生事件时向你的 URL POST JSON。注册 Webhook 有两种途径:
- 沙盒(开发此集成期间): 使用
sumsub-manage-webhooks技能。它会根据精简规格构建clientWebhooks负载,通过 App Token 身份验证向/resources/api/agent/clientWebhooks发送 POST 请求,拒绝非sbx:令牌,提前拒绝目标地址为localhost/127.0.0.1的情况,并指导用户通过ngrok http <port>暴露本地接收器,让 Sumsub 能够实际访问。将用户需要的target、types[]和signatureAlgorithm交给该技能,由它发送 POST 请求。 - 生产环境: 不要通过任何技能(包括本技能)创建生产环境 Webhook。生产环境 Webhook 必须由人工直接在 Sumsub 控制面板中设置(Integrations → Webhooks,工作区切换为 Production)。签名密钥用于验证真实的个人身份信息(PII)数据传送;审计记录应将设置操作归属到具体人员。先在此处对照沙盒验证规格,然后将最终设置(target、事件列表、签名算法、自定义标头)交给拥有生产环境访问权限的人员,由其手动重新创建。
需要关注的标头:
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,因为重新序列化会改变空格和键顺序:
// 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.js。
使用 ngrok 进行本地测试
Sumsub 需要可从公网访问的 URL,你笔记本电脑上的 localhost 不行。本地开发最快的端到端流程如下:
# 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.site;但它无法回送 200,因此不能验证传送流程。
KYC 流程中重要的 Webhook 事件
type | 含义 | 操作 |
|---|---|---|
applicantCreated | 首次为此 externalUserId 创建令牌 | 记录;无需操作 |
applicantPending | 用户已完成上传;Sumsub 正在审核 | 显示“审核中” |
applicantPrechecked | 主要数据处理已完成,等待人工/AML 审核 | 仍显示“审核中” |
applicantOnHold | 已暂停(通常是 AML 命中,需要分析师处理) | 通知运营团队;告知用户“需要额外检查” |
applicantReviewed | 最终结论——reviewResult.reviewAnswer 为 GREEN 或 RED | 在此处控制访问权限。 将用户标记为已验证或已拒绝。 |
applicantPersonalInfoChanged | 用户在提交后修改了信息 | 授予访问权限前重新审核 |
applicantWorkflowCompleted | 整个工作流(多级)已完成 | 对于单级流程,与 applicantReviewed 相同 |
applicantActionPending / Reviewed | 单次操作(与级别流程分开) | 按操作分别处理 |
reviewResult.reviewAnswer:
GREEN— 已通过。RED— 已拒绝。rejectLabels会说明原因;reviewRejectType为FINAL(无法重试)或RETRY(用户可以重新提交)。
完整列表:Sumsub Webhook 文档。
服务端状态检查(备用 / 按需)
对于需要同步检查状态的页面(例如,用户在 Webhook 到达而数据库尚未更新期间重新登录):
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}"
读取 reviewStatus 和 reviewResult.reviewAnswer。开销较低;将其用作备用检查,不要循环轮询——Webhook 才是主要信号。
恢复 / 回访用户
SDK 会通过 externalUserId 查找申请人。如果用户开始验证,中途放弃,并在 3 天后返回:
- 你的端点会为相同的
userId+ 相同的levelName生成一个新的访问令牌。 - SDK 会从用户离开的步骤继续(只重新上传缺失的步骤)。
- 不会创建重复的申请人。
不要为回访用户生成新的 userId——这是导致“用户卡住,客服看到两个申请人”的头号原因。
令牌刷新
.init(token, refreshCallback) 的第一个参数用于处理会话中途令牌过期的情况:
- 令牌即将过期时,SDK 会调用你的
refreshCallback。 - 它必须返回一个解析为新鲜令牌的
Promise<string>(再次调用你的端点)。 - 如果你返回了过期令牌或
userId不匹配的令牌,SDK 就会卡住。
对于大多数流程,600 秒的 TTL 已经足够。不要提前获取并缓存——按需生成。
沙盒测试
在沙盒模式下:
- 使用 Sumsub 文档中的测试文档(“Test documents”页面),无需上传真实 PII 即可触发
GREEN和RED结果。 - AML 命中为模拟结果——
Greenacre之类的名字会通过;Aikman和类似名字则预先录入为匹配项。 - Webhook 投递方式相同。对于本地接收端,可通过
ngrok http <port>(或 Cloudflare Tunnel / Tailscale Funnel)将其公开,并通过sumsub-manage-webhooks技能注册公开 URL——该技能提供完整操作指南,并会在请求到达 Sumsub 前拒绝原始的localhost目标。若要检查负载结构而不使用真实接收端,可以使用webhook.site。 - 沙盒中的自拍不会进行真实的生物识别——任何人脸都可以。
沙盒令牌(sbx:)只会对沙盒工作区生效。生产令牌(prd:)只会对生产环境生效。二者不会互通。
上线检查清单
当用户说“我们准备切换到生产环境”时:
- Webhook 接收端会针对原始字节验证签名(针对真实投递重放 Stage 5 测试)。
-
externalUserId是稳定的用户 ID,而不是电子邮件 / 显示名称。 - 验证状态以服务器为准。可以完全忽略浏览器事件。
- 令牌端点设有身份验证门槛(只有已登录用户才能为自己生成令牌)。
-
applicantReviewed会在数据库中触发面向用户的状态变更,并实现幂等性(同一事件可能到达两次)。 - 按用户处理重试:
reviewResult.reviewRejectType === 'RETRY'⇒ 允许用户重新启动 SDK;'FINAL'⇒ 阻止。 - 生产 App Token + 密钥 + 独立的 Webhook 密钥均存储在生产环境的密钥存储中。沙盒值只保留在开发环境。
另请参阅
references/lifecycle.md— 完整事件目录,包含负载字段和 Webhook 事件参考。examples/vanilla.html— 可运行的单文件集成示例。examples/react-component.tsx— React 钩子 + 清理模式。examples/webhook-verify.js— 使用原始请求体处理方式验证签名。sumsub-api-auth— API 身份验证签名参考,与所有其他 Sumsub 技能共用。sumsub-create-level— 用于 Stage-1 交接。sumsub-manage-webhooks— 用于 Stage-5 交接:通过公开 API 创建 / 列出 / 更新 / 禁用沙盒 Webhook,内置 localhost 拒绝机制和 ngrok 操作指南。 生产环境的 Webhook 设置只能在控制面板中进行,且必须由人工完成。- Sumsub 文档索引 — 如果本技能中的内容有偏差,请以此权威来源为准。