mcpField
S
sumsub-integrate-websdk

sumsub-integrate-websdk

github.com/sumsub开发71.2k 次安装4 周前更新
代码仓库

关于 sumsub-integrate-websdk

用于将 Sumsub WebSDK 集成到 Web 应用的技能,涵盖令牌签发、浏览器端配置和验证回调。它强调使用沙盒凭据,并以服务端验证作为 KYC 结果的权威依据。

主要功能

  1. 01服务端访问令牌接口指南
  2. 02原生 JavaScript 与 React 启动方式
  3. 03客户端生命周期事件处理
  4. 04Webhook 签名验证
  5. 05优先使用沙盒的集成指南

适用场景

  1. 01在网站注册流程中加入 Sumsub KYC
  2. 02展示验证进度和错误信息
  3. 03通过 Webhook 确认 KYC 结果
  4. 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.tsx
vanilla.html
webhook-verify.js
references/
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 中封装同一个构建器。它处理了两个容易踩坑的地方:

  1. 读取 snsWebSdk 之前,必须先加载 CDN 脚本。可以在文档 head 中注入一次,也可以动态加载并 await <script> 的 load 事件。
  2. 在开发环境的 React 18 严格模式下,组件会挂载两次——清理函数必须移除 iframe/清空容器,否则会出现两个堆叠的组件。
其他框架

构建器 API 与框架无关。对于 Vue/Svelte/Angular,可参照 React 方案:挂载时执行生命周期钩子 → 获取令牌 → 构建 → 启动到引用的元素中;卸载时 → 清空容器。

阶段 4 — 客户端生命周期事件

在 SDK 实例上注册这些处理程序。将它们视为用户体验信号,而不是权威状态。

事件触发时机用途
idCheck.onReadySDK iframe 内容已加载隐藏自定义加载器。 必须处理——否则启动后 1–3 秒内模态框看起来是空的。
idCheck.onInitialized首个屏幕已渲染分析:“用户看到了 KYC 步骤”
idCheck.onStepInitiated文档类型屏幕已显示按文档类型收集遥测数据
idCheck.onStepCompleted某个步骤已完成进度条
idCheck.onApplicantSubmitted文档已提交,服务端正在处理将用户切换到“等待”视图
idCheck.onApplicantStatusChanged状态已变更实时进度提示(仍不可信)
idCheck.onApplicantResubmitted被拒后重新上传重新启用等待视图
idCheck.onApplicantReviewed(或 2.0 中的 onApplicantVerificationCompleted)客户端已得出最终判定显示初步结果,然后在服务端验证
idCheck.onErrorSDK 出错显示友好的重试 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 天后返回:

  1. 你的端点会为相同的 userId + 相同的 levelName 生成一个新的访问令牌。
  2. SDK 会从用户离开的步骤继续(只重新上传缺失的步骤)。
  3. 不会创建重复的申请人。

不要为回访用户生成新的 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 密钥均存储在生产环境的密钥存储中。沙盒值只保留在开发环境。

另请参阅

评论

支持 Markdown