sumsub-integrate-websdk

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Sumsub — 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
sbx:
prefix check; the curl recipes below assume the same.
Deeper auth mechanics:
sumsub-api-auth
.
在使用本技能进行集成工作时,请勿接受或使用生产环境的App Token/密钥。该令牌会生成与真实申请人绑定的真实SDK会话。请从https://cockpit.sumsub.com/checkus/devSpace/appTokens获取沙箱令牌对——先将工作区切换为Sandbox,再点击Create。令牌和密钥仅在创建时显示,请在关闭对话框前复制两者。配套技能中的辅助脚本会通过
sbx:
前缀检查强制要求使用沙箱令牌;以下curl示例也遵循此规则。
更详细的认证机制:
sumsub-api-auth

The 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
onApplicantStatusChanged
for entitlement decisions.
 ┌─────────────────────────┐
 │ 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集成中最容易被忽略的部分。请勿依赖
onApplicantStatusChanged
进行权限决策。

Stage 1 — Have a level

阶段1 — 准备好等级

Every SDK launch references a
levelName
that exists in the workspace.
If the user has one (e.g.
basic-kyc-level
, the Sumsub default), capture it and move to Stage 2.
If the user doesn't yet have a level, brainstorm with them and hand off to
sumsub-create-level
. 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:
  • APPLICANT_DATA
    — name, DOB, country, addresses.
  • IDENTITY
    PASSPORT
    ,
    ID_CARD
    ,
    DRIVERS
    (mode
    any
    ).
  • SELFIE
    videoRequired: passiveLiveness
    .
Add
PROOF_OF_RESIDENCE
only if regulatory; add
QUESTIONNAIRE
only if they need structured data (source of funds, occupation). For each addition, ask "what decision does this gate?" before agreeing to include it.
每次SDK启动都会引用工作区中已存在的
levelName
如果用户已有等级(例如
basic-kyc-level
,Sumsub默认等级),记录该等级并进入阶段2。
如果用户还没有等级,与他们讨论需求后转至
sumsub-create-level
。请勿默认选择等级——等级定义了可验证对象(国家/申请人类型)和所需提交的材料(身份证、自拍、居住证明、问卷)。当用户不确定时,一个合理的初始流程建议:
  • APPLICANT_DATA
    — 姓名、出生日期、国家、地址。
  • IDENTITY
    PASSPORT
    ID_CARD
    DRIVERS
    (模式为
    any
    )。
  • SELFIE
    videoRequired: passiveLiveness
仅在合规要求时添加
PROOF_OF_RESIDENCE
;仅在需要结构化数据(资金来源、职业)时添加
QUESTIONNAIRE
。每次添加前,先询问“这将用于控制什么权限?”再确认是否包含。

Stage 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 (
ttlInSecs
, default 1800) and scoped to one
(userId, levelName)
pair.
SDK需要访问令牌,由你的后端使用App Token+密钥生成。令牌有效期较短(
ttlInSecs
,默认1800秒),且仅对单个
(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)

userId
的选择(至关重要)

This is the
externalUserId
Sumsub stores against the applicant. Make it:
  • 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
userId
choice → duplicate applicants, "stuck in submitted" support tickets, and the inability to resume an interrupted verification.
这是Sumsub存储的申请人**
externalUserId
**。请确保:
  • 每个真实用户对应一个稳定的ID(不要在每次页面加载时重新生成——SDK通过此ID识别返回的申请人)。
  • 对用户不透明(使用UUID或数据库行ID;而非用户邮箱)。
  • 与你的认证系统绑定(以便Webhook回调可以将其解析回用户记录)。
错误的
userId
选择会导致申请人重复、“提交后卡住”的支持工单,以及无法恢复中断的验证流程。

Curl 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
userId
if it might contain
/
,
?
, or
&
. The signing string must match the URI on the wire exactly — sign the encoded form.
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}"
如果
userId
可能包含
/
?
&
,请进行URL编码。签名字符串必须与实际传输的URI完全一致——对编码后的形式进行签名。

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
    token
    field. Don't cache it server-side; the browser asks per launch.
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的响应体,或仅返回
    token
    字段。请勿在服务端缓存令牌;浏览器每次启动SDK时请求即可。
根据用户的技术栈(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
snsWebSdk
. 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.
html
<script src="https://static.sumsub.com/idensic/static/sns-websdk-builder.js"></script>
这会暴露全局变量
snsWebSdk
。对于使用打包工具的项目,存在npm包,但CDN脚本是Sumsub官方文档推荐的方式,也是所有框架封装最终调用的底层实现。

Container

容器

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
min-height
(e.g.
600px
) so the iframe doesn't collapse before the SDK adapts its height.
Don't skip the overlay loader. Between
.launch()
returning and the SDK iframe loading content from
api.sumsub.com/websdk/websdk.html
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
idCheck.onReady
handler (Stage 4). Treating
onReady
as informational and leaving the handler empty is the single most common "the widget is blank" report.
html
<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>
为容器设置明确的
min-height
(例如
600px
),以便在SDK自适应高度前iframe不会塌陷。
不要跳过覆盖加载器。在
.launch()
返回后到SDK iframe从
api.sumsub.com/websdk/websdk.html
加载内容之间,有1-3秒的窗口,容器中是空白iframe,看起来像是加载失败——尤其是在用户刚打开的模态框中。添加一个覆盖容器的加载器,然后在阶段4的
idCheck.onReady
处理器中隐藏它。将
onReady
视为信息性事件而不添加处理器,是“组件显示空白”报告的最常见原因。

Canonical vanilla launch

标准原生实现示例

See
examples/vanilla.html
for a runnable file. Minimal shape:
js
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.html
。最简结构:
js
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.tsx
— wraps the same builder in a
useEffect
with cleanup. Two gotchas it handles:
  1. The CDN script must be present before
    snsWebSdk
    is read. Either inject it once in the document head, or dynamically load it and
    await
    the
    <script>
    's
    load
    event.
  2. 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.tsx
— 在
useEffect
中封装相同的构建器并包含清理逻辑。它处理了两个常见问题:
  1. 读取
    snsWebSdk
    前必须加载CDN脚本。要么在文档头部注入一次,要么动态加载并等待
    <script>
    load
    事件。
  2. 在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.
EventWhenUse it for
idCheck.onReady
SDK iframe content loadedHide your own loader. Required — without this the modal looks empty for 1–3s after launch.
idCheck.onInitialized
First screen renderedAnalytics: "user saw KYC step"
idCheck.onStepInitiated
Doc-type screen shownTelemetry per doc type
idCheck.onStepCompleted
A step finishedProgress bar
idCheck.onApplicantSubmitted
Docs submitted, server is processingMove user to a "waiting" view
idCheck.onApplicantStatusChanged
Status movedLive progress hint (still not trusted)
idCheck.onApplicantResubmitted
Re-upload after a rejectionRe-arm waiting view
idCheck.onApplicantReviewed
(or
onApplicantVerificationCompleted
in 2.0)
Final verdict reached client-sideShow a preliminary result, then verify server-side
idCheck.onError
SDK errorSurface a friendly retry CTA, log
code
+
reason
idCheck.onUploadError
/
onUploadWarning
Doc rejected at uploadInline guidance ("blurred photo", etc.)
idCheck.onLivenessCompleted
(2.0 only)
Liveness attempt finishedBranch on
answer
for retry UX
idCheck.onResize
Frame resizedAdjust surrounding layout
Full payload fields per event:
references/lifecycle.md
.
在SDK实例上绑定这些处理器。将它们视为UX信号,而非权威状态。
事件触发时机用途
idCheck.onReady
SDK iframe内容加载完成隐藏自定义加载器。必须实现——否则调用
.launch()
后模态框会空白1-3秒。
idCheck.onInitialized
首个界面渲染完成分析:“用户看到KYC步骤”
idCheck.onStepInitiated
文档类型界面显示按文档类型统计遥测数据
idCheck.onStepCompleted
单个步骤完成更新进度条
idCheck.onApplicantSubmitted
文件提交完成,服务端开始处理将用户切换到“审核中”视图
idCheck.onApplicantStatusChanged
状态变更实时进度提示(仍不可信)
idCheck.onApplicantResubmitted
拒绝后重新上传重置“审核中”视图
idCheck.onApplicantReviewed
(或2.0版本中的
onApplicantVerificationCompleted
)
客户端收到最终审核结果显示初步结果,然后在服务端验证
idCheck.onError
SDK出错显示友好的重试提示,记录
code
+
reason
idCheck.onUploadError
/
onUploadWarning
文件上传时被拒绝内联提示(“照片模糊”等)
idCheck.onLivenessCompleted
(仅2.0版本)
活体检测完成根据
answer
处理重试逻辑
idCheck.onResize
框架尺寸变更调整周边布局
每个事件的完整负载字段:
references/lifecycle.md

Required handlers for a baseline integration

基础集成必须实现的处理器

If you wire nothing else, wire these three. Skipping any of them produces a known-bad UX:
  • idCheck.onReady
    → hide the overlay loader from Stage 3. Without this the modal looks blank for 1–3s after
    .launch()
    .
  • idCheck.onApplicantSubmitted
    → move the user to a "we're reviewing" state. Without this the user re-uploads or contacts support.
  • idCheck.onError
    → render a retryable error to the UI. Without this failures only land in
    console.error
    and the user sees a stuck loader.
如果只实现三个处理器,请选择以下三个。跳过任何一个都会导致不良的用户体验:
  • idCheck.onReady
    → 隐藏阶段3中的覆盖加载器。不实现此处理器的话,调用
    .launch()
    后模态框会空白1-3秒。
  • idCheck.onApplicantSubmitted
    → 将用户切换到“审核中”状态。不实现此处理器的话,用户会重复上传或联系支持。
  • idCheck.onError
    → 在UI中显示可重试的错误提示。不实现此处理器的话,错误只会出现在
    console.error
    中,用户看到的是卡住的加载器。

Why you can't trust
onApplicantReviewed
alone

为何不能仅依赖
onApplicantReviewed

The 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}/one

Stage 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
    sumsub-manage-webhooks
    skill. It builds the
    clientWebhooks
    payload from a compact spec, POSTs to
    /resources/api/agent/clientWebhooks
    with App Token auth, refuses non-
    sbx:
    tokens, rejects
    localhost
    /
    127.0.0.1
    targets up front, and walks the user through exposing their local receiver via
    ngrok http <port>
    so Sumsub can actually reach it. Hand off the
    target
    ,
    types[]
    , and
    signatureAlgorithm
    the user wants and let that skill do the POST.
  • 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:
  • x-payload-digest
    — the signature, hex-encoded.
  • x-payload-digest-alg
    HMAC_SHA256_HEX
    (default),
    HMAC_SHA512_HEX
    , or the legacy
    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 (
SUMSUB_WEBHOOK_SECRET
) alongside the App Token pair.
Verification 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
examples/webhook-verify.js
for a complete handler.
Sumsub会在每个事件发生时向你的URL发送POST请求。有两种注册方式:
  • 沙箱环境(集成开发时):使用
    sumsub-manage-webhooks
    技能。它会根据简洁的规范构建
    clientWebhooks
    负载,使用App Token认证POST到
    /resources/api/agent/clientWebhooks
    ,拒绝非
    sbx:
    开头的令牌,直接拒绝
    localhost
    /
    127.0.0.1
    目标,并引导用户通过
    ngrok http <port>
    暴露本地接收器,以便Sumsub可以访问。提供用户需要的
    target
    types[]
    signatureAlgorithm
    ,让该技能完成POST请求。
  • 生产环境请勿使用任何技能(包括本技能)创建生产环境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.js

Local testing with ngrok

使用ngrok进行本地测试

Sumsub needs a publicly reachable URL — your laptop's
localhost
won't do. The fastest end-to-end loop for local dev:
bash
undefined
Sumsub需要可公开访问的URL——你的笔记本电脑的
localhost
无法满足要求。本地开发最快的端到端流程:
bash
undefined

1. 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.

使用sumsub-manage-webhooks的
create
命令,target为ngrok的https URL。

4. 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).

使用sumsub-manage-webhooks的
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事件

type
What it meansAction
applicantCreated
First time you minted a token for this
externalUserId
Log; nothing required
applicantPending
User finished uploading; Sumsub is checkingShow "in review"
applicantPrechecked
Primary data processing done, queued for human/AMLStill "in review"
applicantOnHold
Paused (often AML hit needing analyst)Surface to ops; tell user "extra checks"
applicantReviewed
Final verdict —
reviewResult.reviewAnswer
is
GREEN
or
RED
Gate access here. Mark user verified or rejected.
applicantPersonalInfoChanged
User edited info after submissionRe-check before granting access
applicantWorkflowCompleted
Whole workflow (multi-level) doneSame as
applicantReviewed
for single-level flows
applicantActionPending
/
Reviewed
One-off action (separate from the level flow)Per-action handling
reviewResult.reviewAnswer
:
  • GREEN
    — approved.
  • RED
    — rejected.
    rejectLabels
    says why;
    reviewRejectType
    is
    FINAL
    (can't retry) or
    RETRY
    (user may resubmit).
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文档

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
reviewStatus
and
reviewResult.reviewAnswer
. Cheap; use as a fallback, not as a polling loop — webhooks are the primary signal.
对于需要同步检查状态的页面(例如用户在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}"
读取
reviewStatus
reviewResult.reviewAnswer
。该请求开销低;仅作为fallback使用,不要轮询——Webhook是主要信号。

Resumption / returning users

恢复/返回用户的处理

The SDK looks up applicants by
externalUserId
. If a user starts verification, abandons, and returns 3 days later:
  1. Your endpoint mints a new access token for the same
    userId
    + same
    levelName
    .
  2. The SDK opens to wherever the user left off (re-uploads only the missing steps).
  3. No duplicate applicant is created.
Don't generate a new
userId
for returning users — that's the #1 cause of "the user is stuck and customer support sees two applicants".
SDK通过
externalUserId
查找申请人。如果用户开始验证后中断,3天后返回:
  1. 你的接口为相同的
    userId
    +相同的
    levelName
    生成新的访问令牌
  2. SDK会打开到用户上次中断的位置(仅重新上传缺失的步骤)。
  3. 不会创建重复的申请人。
不要为返回的用户生成新的
userId
——这是“用户卡住且客服看到两个申请人”的头号原因。

Token refresh

令牌刷新

The first argument to
.init(token, refreshCallback)
covers expiry mid-session:
  • SDK calls your
    refreshCallback
    when the token nears expiry.
  • It must return
    Promise<string>
    resolving to a fresh token (call your endpoint again).
  • If you return a stale or wrong-
    userId
    token, the SDK hangs.
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>
    ,解析为新的令牌(再次调用你的接口)。
  • 如果返回过期或
    userId
    错误的令牌,SDK会挂起。
对于大多数流程,600秒的TTL足够。不要预获取和缓存——按需生成即可。

Sandbox testing

沙箱测试

In sandbox mode:
  • Use test documents from the Sumsub docs ("Test documents" page) to trigger
    GREEN
    vs
    RED
    outcomes without uploading real PII.
  • AML hits are simulated — names like
    Greenacre
    go through;
    Aikman
    and similar are pre-loaded as positive matches.
  • Webhook delivery works the same. For a local receiver, expose it through
    ngrok http <port>
    (or Cloudflare Tunnel / Tailscale Funnel) and register the public URL via the
    sumsub-manage-webhooks
    skill — it has the full walkthrough and rejects raw
    localhost
    targets before they ever reach Sumsub.
    webhook.site
    is fine for inspecting payload shapes without a real receiver.
  • Selfies in sandbox bypass real biometrics — any face works.
Sandbox tokens (
sbx:
) only fire against the sandbox workspace. Production tokens (
prd:
) only fire against production. There's no fall-through.
在沙箱模式下:
  • 使用Sumsub文档(“测试文档”页面)中的测试文档来触发
    GREEN
    RED
    结果,无需上传真实的个人身份信息。
  • AML命中是模拟的——类似
    Greenacre
    的姓名会通过;
    Aikman
    等姓名预加载为匹配命中。
  • Webhook推送工作方式相同。对于本地接收器,通过
    ngrok http <port>
    (或Cloudflare Tunnel / Tailscale Funnel)暴露,并使用
    sumsub-manage-webhooks
    技能注册公开URL——该技能包含完整的引导流程,并会直接拒绝原始
    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).
  • externalUserId
    is the stable user id, not the email / display name.
  • 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).
  • applicantReviewed
    triggers the user-facing state change in your DB, with idempotency (the same event can arrive twice).
  • Per-user retry handling:
    reviewResult.reviewRejectType === 'RETRY'
    ⇒ let user re-launch the SDK;
    'FINAL'
    ⇒ block.
  • Production App Token + secret + separate webhook secret are all in the prod secret store. Sandbox values stay only in dev env.
当用户表示“准备切换到生产环境”时:
  • Webhook接收器对原始字节验证签名(针对真实推送重新测试阶段5)。
  • externalUserId
    是稳定的用户ID,而非邮箱/显示名称。
  • 服务端是验证状态的权威来源。可完全忽略浏览器事件。
  • 令牌接口已做权限控制(仅已登录用户可为自己生成令牌)。
  • applicantReviewed
    触发你的数据库中用户状态的变更,并实现幂等性(同一事件可能推送两次)。
  • 按用户处理重试:
    reviewResult.reviewRejectType === 'RETRY'
    ⇒ 允许用户重新启动SDK;
    'FINAL'
    ⇒ 阻止用户。
  • 生产环境的App Token+密钥+独立的Webhook密钥都已存储在生产环境密钥管理系统中。沙箱值仅保留在开发环境。

See also

相关链接

  • references/lifecycle.md
    — full event catalog with payload fields, plus webhook event reference.
  • examples/vanilla.html
    — runnable single-file integration.
  • examples/react-component.tsx
    — React hook + cleanup pattern.
  • examples/webhook-verify.js
    — signature verification with raw-body handling.
  • sumsub-api-auth
    — the auth signing reference, shared with every other Sumsub skill.
  • sumsub-create-level
    — for the Stage-1 hand-off.
  • sumsub-manage-webhooks
    — 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 docs index — authoritative source if anything in this skill drifts.
  • references/lifecycle.md
    — 完整事件目录及负载字段,以及Webhook事件参考。
  • examples/vanilla.html
    — 可运行的单文件集成示例。
  • examples/react-component.tsx
    — React钩子+清理模式示例。
  • examples/webhook-verify.js
    — 原始体处理的签名验证示例。
  • sumsub-api-auth
    — 认证签名参考,所有Sumsub技能共享。
  • sumsub-create-level
    — 阶段1的转至技能。
  • sumsub-manage-webhooks
    — 阶段5的转至技能:通过公开API创建/列出/更新/禁用沙箱Webhook,内置localhost拒绝和ngrok引导流程。生产环境Webhook必须由人工在控制台设置。
  • Sumsub文档索引 — 如果本技能内容有偏差,以此为权威来源。