sumsub-integrate-id-connect
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSumsub — ID Connect integration
Sumsub — ID Connect 集成
Embed the "Verify with Sumsub ID" OIDC flow end-to-end — preconditions →
preflight → button → backend exchange → consent → applicant in your workspace.
端到端嵌入「通过Sumsub ID验证」OIDC流程——前置条件 →
预检 → 按钮 → 后端交换 → 同意 → 申请人信息落地到你的工作区。
⚠️ Sandbox tokens only
⚠️ 仅使用沙箱令牌
Do not use a production App Token / secret while wiring this up. 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. The preflight enforces this
with an prefix check.
sbx:Deeper auth mechanics: .
sumsub-api-auth在配置过程中请勿使用生产环境的App Token/密钥。请从https://cockpit.sumsub.com/checkus/devSpace/appTokens获取沙箱令牌对——先将工作区切换为Sandbox,再点击Create。令牌和密钥仅在创建时显示一次;请在关闭对话框前复制两者。预检流程会通过前缀检查强制验证这一点。
sbx:更深入的认证机制:。
sumsub-api-auth⛔ Order of operations — do NOT scaffold an app first
⛔ 操作顺序——请勿先搭建应用
This is the single most important rule of this skill. Write no frontend or
backend code, create no demo project, scaffold nothing until BOTH gates below
are green. The client-side Sumsub setup (Stage 0a) is manual and cannot be done
by you — jumping to code before it exists produces an app that can't
authenticate, can't redirect, and can't reuse. Always follow this sequence:
- Confirm Stage 0a preconditions with the user — one by one. Do not assume any item is done. For each, ask the user whether it exists; if it doesn't, stop and hand them the exact manual step and where to do it (the support email template, the Dev Space → OIDC Settings registration, the App Token minting page, the level creation hand-off). These are Sumsub-dashboard / support actions only — there is no public API for them, so you cannot do them for the user. Wait until every item is confirmed before moving on.
- Run Stage 0b preflight and require exit 0. Only after the preconditions
are confirmed, run . Resolve every FAIL (loop back to Stage 0a as needed). Do not proceed on any FAIL.
scripts/preflight.sh - Only now build. With both gates green, proceed to Stage 1 and scaffold the frontend/backend.
If the user says "just build the demo" before the gates are green, briefly
explain why it won't work yet and walk them through the missing precondition
first. The code is the last step, not the first.
这是本方案最重要的规则。在以下两个检查项全部通过前,请勿编写任何前端或后端代码、创建演示项目、搭建任何框架。客户端侧的Sumsub设置(Stage 0a)为手动操作,无法自动完成——跳过该步骤直接编写代码会导致应用无法认证、无法重定向、无法复用信息。请严格遵循以下顺序:
- 逐一与用户确认Stage 0a的前置条件。请勿假设任何项已完成。对于每一项,询问用户是否已完成;若未完成,停止操作并告知用户具体的手动步骤及操作位置(支持邮件模板、Dev Space → OIDC Settings注册页面、App Token生成页面、验证等级创建流程)。这些操作仅能通过Sumsub控制台/支持团队完成——无公开API可用,因此你无法代用户完成。等待所有项确认完成后再进行下一步。
- 运行Stage 0b预检流程并要求返回值为0。仅在前置条件全部确认后,运行。解决所有FAIL项(必要时回到Stage 0a)。若存在任何FAIL项,请勿继续。
scripts/preflight.sh - 仅在此时开始构建。在两个检查项全部通过后,再进入Stage 1并搭建前端/后端。
如果用户在检查项通过前要求「直接构建演示项目」,请简要解释为何无法正常工作,并先引导他们完成缺失的前置条件。代码编写是最后一步,而非第一步。
What ID Connect is (and isn't)
ID Connect是什么(以及不是什么)
Sumsub ID Connect lets a business request specific verified identity claims —
age, name, nationality, email, country of residence, humanness — from a
Sumsub ID account belonging to the end-user. Sumsub hosts the entire
user-facing flow: sign-in, account creation (if needed), email OTP, any
missing verification steps, and a per-recipient consent screen. Your app
receives the result as a signed OIDC token plus, optionally, an applicant
materialised in your workspace through Reusable KYC.
This is not the WebSDK. The WebSDK runs a full KYC capture inside your
page; ID Connect delegates the whole UX to Sumsub.
Sumsub ID Connect允许企业向终端用户的Sumsub ID账户请求特定的已验证身份信息——年龄、姓名、国籍、邮箱、居住国、真人验证状态。Sumsub托管整个面向用户的流程:登录、账户创建(如需)、邮箱OTP、任何缺失的验证步骤,以及针对接收方的同意界面。你的应用将收到签名后的OIDC令牌,还可选择通过可复用KYC将申请人信息落地到你的工作区。
这不是WebSDK。WebSDK在你的页面内运行完整的KYC采集流程;而ID Connect将整个用户体验委托给Sumsub。
The lifecycle in one picture
生命周期概览
┌──────────────────────────────┐
│ Stage 0a Preconditions check │ manual setup on Sumsub side
│ Stage 0b scripts/preflight.sh │ automated validation
└──────────────┬───────────────┘
│ clientId, levelName, allowlist
┌──────────────▼───────────────┐
│ Stage 1: Frontend "Verify…" │ authorize redirect with PKCE
│ button │
└──────────────┬───────────────┘
│ onSuccess({ code, state })
┌──────────────▼───────────────┐
│ Stage 2: code → access_token │ POST id.sumsub.com/api/snsId/oauth/token
│ (client_secret_basic) │
└──────────────┬───────────────┘
│ access_token
┌──────────────▼───────────────┐
│ Stage 3a: → share token │ POST /resources/accessTokens/sumsubIdShareToken
└──────────────┬───────────────┘
│ sharingAllowed?
┌───────┴────────┐
│ false │
▼ ▼ true
┌──────────────────┐ ┌──────────────────────────────┐
│ Stage 3b: │ │ Stage 4: reuse/preview + │
│ shareLink │ │ reuse/commit │
│ redirect + retry │ │ → applicant in your workspace │
└────────┬─────────┘ └──────────────────────────────┘
│
└──────► back to Stage 3a (retry — now sharingAllowed: true) ┌──────────────────────────────┐
│ Stage 0a Preconditions check │ Sumsub侧的手动设置
│ Stage 0b scripts/preflight.sh │ 自动化验证
└──────────────┬───────────────┘
│ clientId, levelName, allowlist
┌──────────────▼───────────────┐
│ Stage 1: Frontend "Verify…" │ 带PKCE的授权重定向
│ button │
└──────────────┬───────────────┘
│ onSuccess({ code, state })
┌──────────────▼───────────────┐
│ Stage 2: code → access_token │ POST id.sumsub.com/api/snsId/oauth/token
│ (client_secret_basic) │
└──────────────┬───────────────┘
│ access_token
┌──────────────▼───────────────┐
│ Stage 3a: → share token │ POST /resources/accessTokens/sumsubIdShareToken
└──────────────┬───────────────┘
│ sharingAllowed?
┌───────┴────────┐
│ false │
▼ ▼ true
┌──────────────────┐ ┌──────────────────────────────┐
│ Stage 3b: │ │ Stage 4: reuse/preview + │
│ shareLink │ │ reuse/commit │
│ redirect + retry │ │ → 申请人信息落地到你的工作区 │
└────────┬─────────┘ └──────────────────────────────┘
│
└──────► 返回Stage 3a(重试——此时sharingAllowed: true)Endpoints
端点
| METHOD | URL | Auth | Stage |
|---|---|---|---|
| (browser redirect) | | none | 1 |
| GET | | none | 0b |
| GET | | none | (id_token validation) |
| POST | | | 2 |
| POST | | App Token + HMAC | 3a |
| POST | | App Token + HMAC + Bearer | 3b |
| GET | | App Token + HMAC | 4 |
| POST | | App Token + HMAC | 4 |
The "Enable Sumsub ID Connect" toggle, OIDC client registration
(client_secret + redirect_uri allowlist), and sharing-partner registration
are Sumsub dashboard UI only — no public-API equivalent.
| METHOD | URL | Auth | Stage |
|---|---|---|---|
| (浏览器重定向) | | none | 1 |
| GET | | none | 0b |
| GET | | none | (id_token验证) |
| POST | | | 2 |
| POST | | App Token + HMAC | 3a |
| POST | | App Token + HMAC + Bearer | 3b |
| GET | | App Token + HMAC | 4 |
| POST | | App Token + HMAC | 4 |
「启用Sumsub ID Connect」开关、OIDC客户端注册(client_secret + redirect_uri白名单)、共享合作伙伴注册均仅能通过Sumsub控制台UI操作——无公开API等效操作。
Stage 0a — Preconditions checklist
Stage 0a — 前置条件清单
Before any code or preflight, the following must already exist on the Sumsub
side. Each item maps to a check below or to a runtime failure if missed.
-
Sumsub support has enabled ID Connect for your workspace. This is not a self-serve toggle — until support flips it, no OIDC client can be created and the preflight will report. Email your CSM or
connect_token: ID Connect not enabled for this workspacefirst and wait for confirmation. Template:support@sumsub.com``` Subject: Enable Sumsub ID Connect for clientId <YOUR_CLIENT_ID> (sandbox) Hi Sumsub team, Please enable Sumsub ID Connect for our workspace. Environment: sandbox clientId: <YOUR_CLIENT_ID> (Dashboard top-left) Intended scopes: openid, share, name (adjust as needed) Intended recipients: self (or list partner clientIds) Use case: <one-line description of where the button will live> Once enabled we'll register the OIDC client + redirect URIs in Dev Space → OIDC Settings. Thanks ``` -
App Token + secret (sandbox). Minted at https://cockpit.sumsub.com/checkus/devSpace/appTokens with workspace toggled to Sandbox. Both values shown once at creation. → exported as/
SUMSUB_APP_TOKEN.SUMSUB_SECRET_KEY -
OIDC client registered at Dashboard → Dev Space → OIDC Settings. Capture all three: -(public — goes in the browser, used as
client_idfor the share token in the common self-recipient case) -forClientId(shown ONCE at creation; if you lose it you must regenerate, which invalidates existing integrations). Required for the Stage 2 code→token exchange — keep it server-side only. -client_secretallowlist populated for BOTH callbacks (Sumsub matches byte-for-byte — trailing slash, casing, port, query all matter): - your authorize callback (e.g.redirect_uri) - your shareLink callback (e.g.https://yourapp.com/auth/callback) Sumsub redirects the browser back to these URLs and requires public HTTPS — a barehttps://yourapp.com/share-completewon't work for local testing. To develop on your machine, expose your local server with a public HTTPS tunnel (ngrok) and allowlist that origin — see Local development with ngrok below.http://localhost:3000 -
At least one verification level exists in this workspace. Capture. If none, hand off to
levelNamefirst — Stage 4 can't materialise an applicant without one.sumsub-create-level -
At least one Sumsub ID account with verified data for end-to-end testing. An email-only account (no completed KYC) makes Stage 4 fail with errorCode— see the error table at the end of Stage 4.
10521 reusable-kyc-inactive-sumsub-id-account
在编写任何代码或执行预检前,Sumsub侧必须已完成以下操作。每一项对应下方的检查项,若遗漏会导致运行时失败。
-
Sumsub支持团队已为你的工作区启用ID Connect。这不是自助开关——在支持团队启用前,无法创建OIDC客户端,预检流程会报告。请先联系你的客户成功经理或发送邮件至
connect_token: ID Connect not enabled for this workspace,等待确认。邮件模板:support@sumsub.com``` Subject: Enable Sumsub ID Connect for clientId <YOUR_CLIENT_ID> (sandbox) Hi Sumsub team, Please enable Sumsub ID Connect for our workspace. Environment: sandbox clientId: <YOUR_CLIENT_ID> (控制台左上角) Intended scopes: openid, share, name (按需调整) Intended recipients: self (或列出合作伙伴clientIds) Use case: <按钮部署场景的一行描述> Once enabled we'll register the OIDC client + redirect URIs in Dev Space → OIDC Settings. Thanks ``` -
App Token + 密钥(沙箱环境)。在https://cockpit.sumsub.com/checkus/devSpace/appTokens生成,需先将工作区切换为Sandbox。两者仅在创建时显示一次。→ 导出为/
SUMSUB_APP_TOKEN。SUMSUB_SECRET_KEY -
已注册OIDC客户端,路径为控制台 → Dev Space → OIDC Settings。记录以下三项: -(公开——可在浏览器中使用,在常见的自接收方场景中用作共享令牌的
client_id) -forClientId(仅在创建时显示一次;若丢失必须重新生成,这会使现有集成失效)。Stage 2的码转令牌交换需要此字段——请仅在服务器侧保存。 -client_secret白名单,需填入两个回调地址(Sumsub会进行字节级匹配——末尾斜杠、大小写、端口、查询参数均会影响匹配): - 你的授权回调地址(例如redirect_uri) - 你的shareLink回调地址(例如https://yourapp.com/auth/callback) Sumsub会将浏览器重定向回这些URL,且要求地址为公开HTTPS——本地测试使用的https://yourapp.com/share-complete无法正常工作。如需在本地开发,请使用公开HTTPS隧道(如ngrok)暴露本地服务器,并将该域名加入白名单——详见下方的使用ngrok进行本地开发。http://localhost:3000 -
工作区中至少存在一个验证等级。记录。若不存在,请先使用
levelName创建——Stage 4无法在无验证等级的情况下落地申请人信息。sumsub-create-level -
至少存在一个带有已验证数据的Sumsub ID账户,用于端到端测试。仅含邮箱的账户(未完成KYC)会导致Stage 4失败,错误码为——详见Stage 4末尾的错误表。
10521 reusable-kyc-inactive-sumsub-id-account
Stage 0b — Preflight
Stage 0b — 预检流程
scripts/preflight.shbash
SUMSUB_APP_TOKEN=sbx:... SUMSUB_SECRET_KEY=... \
bash scripts/preflight.shChecks (each independent — one failing doesn't abort the others):
| Check | What it proves |
|---|---|
| App-Token HMAC signing is accepted and ID Connect is enabled for the workspace — probes the endpoint with a known-bad code and expects |
| |
| At least one verification level exists in this workspace |
Exit code: on all-PASS / PASS+WARN, on any FAIL. Resolve all FAILs
before Stage 1.
010scripts/preflight.shbash
SUMSUB_APP_TOKEN=sbx:... SUMSUB_SECRET_KEY=... \
bash scripts/preflight.sh检查项(彼此独立——一项失败不会终止其他检查):
| 检查项 | 验证内容 |
|---|---|
| App-Token的HMAC签名被接受,且工作区已启用ID Connect——向端点发送已知无效的代码,预期返回 |
| |
| 工作区中至少存在一个验证等级 |
退出码:所有项通过/通过+警告时返回,存在任何失败项时返回。在进入Stage 1前解决所有失败项。
010Local development with ngrok
使用ngrok进行本地开发
ID Connect can't be tested against . Sumsub redirects the
browser back to your after the authorize flow (Stage 1) and
after the shareLink consent flow (Stage 3b), and it requires the URL to be
public HTTPS and present in the OIDC client's allowlist
byte-for-byte. A bare URL is neither public nor HTTPS, so the
redirect fails. For local development, put a public HTTPS tunnel in front of
your local server with ngrok and allowlist that origin.
http://localhostredirect_uriredirect_urilocalhost-
Start your backend locally (the example listens on):
PORT=3000bashPORT=3000 node examples/express-callback.js -
Open a tunnel to that port. Prefer a reserved (static) domain so the URL survives restarts — a free random ngrok URL changes on every launch and forces you to re-edit the allowlist each time:bash
ngrok http 3000 --domain=your-reserved-name.ngrok-free.app # or, ephemeral URL (changes each restart): ngrok http 3000ngrok prints a forwarding URL, e.g..https://your-reserved-name.ngrok-free.app -
Allowlist BOTH callbacks at the ngrok origin in Dashboard → Dev Space → OIDC Settings (sandbox workspace) — byte-for-byte, including trailing slash:
https://your-reserved-name.ngrok-free.app/ https://your-reserved-name.ngrok-free.app/share-complete -
Point the app at the public URL, not localhost. The browser'sand the server-side shareLink
redirect_urimust derive from the same public origin, so setredirectUriand restart:PUBLIC_BASE_URLbashPUBLIC_BASE_URL=https://your-reserved-name.ngrok-free.app \ PORT=3000 node examples/express-callback.jsThen open the ngrok URL (not) in the browser so thelocalhost:3000it sends matches the allowlist.redirect_uri
⚠️ Re-register on every URL change. If you use an ephemeral ngrok URL, the allowlist entry (and) must be updated each time ngrok restarts. A reserved domain avoids this churn.PUBLIC_BASE_URLngrok's free interstitial ("You are about to visit…") only affects API/XHR calls, not top-level browser navigation, so it doesn't block the OIDC redirects. A reserved domain or paid plan removes it entirely.
ID Connect无法针对进行测试。Sumsub在授权流程(Stage 1)和shareLink同意流程(Stage 3b)后会将浏览器重定向回你的,且要求URL为公开HTTPS并存在于OIDC客户端的白名单中(字节级匹配)。纯地址既非公开地址也非HTTPS,因此重定向会失败。如需本地开发,请使用ngrok在本地服务器前搭建公开HTTPS隧道,并将该域名加入白名单。
http://localhostredirect_uriredirect_urilocalhost-
在本地启动后端服务(示例监听):
PORT=3000bashPORT=3000 node examples/express-callback.js -
为该端口开启隧道。建议使用保留(静态)域名,以便URL在重启后保持不变——免费的随机ngrok URL每次启动都会变化,会迫使你每次都重新编辑白名单:bash
ngrok http 3000 --domain=your-reserved-name.ngrok-free.app # 或使用临时URL(每次重启变化):ngrok http 3000ngrok会打印转发URL,例如。https://your-reserved-name.ngrok-free.app -
在控制台 → Dev Space → OIDC Settings(沙箱工作区)中将两个回调地址加入白名单——字节级匹配,包括末尾斜杠:
https://your-reserved-name.ngrok-free.app/ https://your-reserved-name.ngrok-free.app/share-complete -
将应用指向公开URL,而非localhost。浏览器的和服务器侧的shareLink
redirect_uri必须来自同一个公开域名,因此请设置redirectUri并重启服务:PUBLIC_BASE_URLbashPUBLIC_BASE_URL=https://your-reserved-name.ngrok-free.app \ PORT=3000 node examples/express-callback.js然后在浏览器中打开ngrok URL(而非),确保发送的localhost:3000与白名单匹配。redirect_uri
⚠️ 每次URL变化都需重新注册。若使用临时ngrok URL,每次ngrok重启后都必须更新白名单条目(以及)。使用保留域名可避免此类重复操作。PUBLIC_BASE_URLngrok的免费 interstitial页面("You are about to visit…")仅影响API/XHR调用,不影响顶级浏览器导航,因此不会阻止OIDC重定向。使用保留域名或付费计划可完全移除该页面。
Stage 1 — Frontend: the button
Stage 1 — 前端:按钮实现
⛔ Gate. Do not start this stage until Stage 0a preconditions are all confirmed with the user AND Stage 0b preflight exits 0. If you haven't done both, go back — see "Order of operations" above. This is the first stage where you write code; everything before it is setup you must verify first.
⛔ 检查项。在与用户确认Stage 0a的前置条件全部完成且Stage 0b预检流程返回0前,请勿开始本阶段。若未完成这两项,请返回——详见上方的「操作顺序」。这是第一个需要编写代码的阶段;此前的所有步骤均为需先验证的设置项。
Architecture — what's yours vs Sumsub's
架构——你的部分 vs Sumsub的部分
Sumsub provides: the OIDC service (), the HMAC API
(), and the optional JS library that
mounts the button.
id.sumsub.comapi.sumsub.com@sumsub/id-connectYou write both halves of the integration:
- Frontend (this stage) — page with the button. Generates PKCE, kicks
off the OIDC redirect to , receives
id.sumsub.comin the callback, POSTs?code=to your own backend.{ code, codeVerifier } - Backend (Stages 2-4) — HTTP endpoint that your frontend POSTs to.
Signs Sumsub API calls with your App Token + secret (which must NEVER
reach the browser), orchestrates the chain , and returns the materialised applicant.
code → access_token → share token → reuse
The two halves are coupled by one route name —
in the examples; rename to fit your routing. Reference pair (matched):
(frontend) +
(backend).
The backend is a thin orchestrator (~180 lines of Node/Express); port it to
your stack of choice — the wire format is identical regardless of language.
POST /api/sumsub/id-connect/exchangeexamples/oidc-button.htmlexamples/express-callback.js⚠️is not a verification signal. It only confirms the user finished the OIDC consent step and Sumsub issued an authorization code. The actual verification verdict comes from Stage 4 —onSuccessin theapplicant.review.reviewStatusresponse (immediate, if your recipient level runs no additional checks) or the/reusewebhook (authoritative, for any level that runs post-reuse checks). Never grant access or unlock features based onapplicantReviewedfiring.onSuccess
Three variants — use Method 1 () by default; the others
exist for design-system or no-bundler scenarios.
createButtonSumsub提供:OIDC服务()、HMAC API(),以及可选的 JS库用于挂载按钮。
id.sumsub.comapi.sumsub.com@sumsub/id-connect你需要编写集成的两端:
- 前端(本阶段)——包含按钮的页面。生成PKCE,触发向的OIDC重定向,在回调中接收
id.sumsub.com,将?code=POST到你自己的后端。{ code, codeVerifier } - 后端(Stages 2-4)——前端POST请求的HTTP端点。使用你的App Token + 密钥对Sumsub API调用进行签名(绝不能让该密钥到达浏览器),协调的流程,并返回落地后的申请人信息。
code → access_token → 共享令牌 → 复用
两端通过一个路由名称关联——示例中为;可根据你的路由规则重命名。配套参考示例:(前端) + (后端)。后端是一个轻量的协调器(约180行Node/Express代码);可将其移植到你使用的技术栈——无论使用何种语言,通信格式均相同。
POST /api/sumsub/id-connect/exchangeexamples/oidc-button.htmlexamples/express-callback.js⚠️并非验证信号。它仅确认用户完成了OIDC同意步骤,且Sumsub已颁发授权码。实际的验证结果来自Stage 4——onSuccess响应中的/reuse(若你的接收方等级无需额外检查则会立即返回)或applicant.review.reviewStatuswebhook(权威结果,适用于任何复用后需检查的等级)。切勿基于applicantReviewed触发就授予权限或解锁功能。onSuccess
三种实现方式——默认使用方法1();其他方式适用于设计系统限制或无打包工具的场景。
createButtonMethod 1 — pre-built button via @sumsub/id-connect
(recommended)
@sumsub/id-connect方法1 — 通过@sumsub/id-connect
使用预构建按钮(推荐)
@sumsub/id-connectjs
import { createButton } from '@sumsub/id-connect';
// PKCE helpers (S256 — full versions in examples/oidc-button.html).
const b64url = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const sha256 = (s) => crypto.subtle.digest('SHA-256', new TextEncoder().encode(s));
// Generate the PKCE pair yourself, store the verifier under YOUR key — Stage 2
// on the backend needs that verifier to exchange the code for an access token.
const verifier = b64url(crypto.getRandomValues(new Uint8Array(32)));
const challenge = b64url(await sha256(verifier));
sessionStorage.setItem('myapp.pkce.verifier', verifier);
createButton({
clientId: '<your-client-id>',
permissions: ['openid', 'share', 'name'],
container: document.getElementById('button-container'),
codeChallenge: challenge, // library uses YOUR challenge — skips its own PKCE gen
loginHint: 'user@example.com', // optional, prefills the email field
onSuccess: async ({ code, state }) => {
const v = sessionStorage.getItem('myapp.pkce.verifier');
await fetch('/api/sumsub/id-connect/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code, codeVerifier: v, redirectUri: location.href }),
});
},
onError: (err) => console.error('sumsub-id-connect error', err),
});Why generate PKCE yourself: the Stage 2 token exchange needs the .
If you let the library generate it, you depend on the library's internal
key (undocumented per minor version) to read it back in
. Passing your own keeps the verifier under a key
you own. Same pattern works for Methods 2 and 3.
verifiersessionStorageonSuccesscodeChallengeRunnable example: .
examples/oidc-button.htmljs
import { createButton } from '@sumsub/id-connect';
// PKCE工具函数(S256——完整版本见examples/oidc-button.html)。
const b64url = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const sha256 = (s) => crypto.subtle.digest('SHA-256', new TextEncoder().encode(s));
// 自行生成PKCE对,将verifier存储在你自己的键下——后端的Stage 2需要该verifier来交换授权码获取access token。
const verifier = b64url(crypto.getRandomValues(new Uint8Array(32)));
const challenge = b64url(await sha256(verifier));
sessionStorage.setItem('myapp.pkce.verifier', verifier);
createButton({
clientId: '<your-client-id>',
permissions: ['openid', 'share', 'name'],
container: document.getElementById('button-container'),
codeChallenge: challenge, // 库使用你提供的challenge——跳过自身的PKCE生成
loginHint: 'user@example.com', // 可选,预填充邮箱字段
onSuccess: async ({ code, state }) => {
const v = sessionStorage.getItem('myapp.pkce.verifier');
await fetch('/api/sumsub/id-connect/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code, codeVerifier: v, redirectUri: location.href }),
});
},
onError: (err) => console.error('sumsub-id-connect error', err),
});自行生成PKCE的原因:Stage 2的令牌交换需要。若让库自行生成,你需依赖库内部的键(每个小版本可能未公开)才能在中读取它。传入你自己的可让verifier存储在你自己控制的键下。该模式同样适用于方法2和方法3。
verifiersessionStorageonSuccesscodeChallenge可运行示例:。
examples/oidc-button.htmlMethod 2 — your trigger + library modal
方法2 — 自定义触发元素 + 库弹窗
Same library, you own the trigger element; library opens the flow in an
overlay via . Use when your design system has its own
buttons.
openModal({...})Runnable example: .
examples/oidc-modal.html使用同一个库,但你自行控制触发元素;库通过在浮层中打开流程。适用于你的设计系统有自定义按钮的场景。
openModal({...})可运行示例:。
examples/oidc-modal.htmlMethod 3 — raw OIDC redirect (no library)
方法3 — 原生OIDC重定向(无库依赖)
Direct navigation to the OIDC authorize endpoint, no Sumsub script on the
page. Reach for this when CSP forbids third-party scripts, you're in an
SSR-only environment, or you maintain a generic OIDC client across multiple
providers.
GET https://id.sumsub.com/api/snsId/oauth/authorize
?client_id=<your-client-id>
&response_type=code
&redirect_uri=<allowlisted URI>
&scope=openid+share+name
&state=<random CSRF nonce>
&code_challenge=<base64url(SHA256(code_verifier))>
&code_challenge_method=S256
&login_hint=<optional email>You generate the (43–128 chars, URL-safe), derive
, store the verifier
(), and forward it to your backend after the callback.
Sumsub only accepts ( is rejected) and
the must match the allowlist byte-for-byte.
code_verifiercode_challenge = base64url(SHA256(verifier))sessionStoragecode_challenge_method=S256plainredirect_uriRunnable example: .
examples/oidc-redirect.html直接导航到OIDC授权端点,页面中无需引入Sumsub脚本。适用于CSP禁止第三方脚本、处于纯SSR环境,或你维护了跨多个提供商的通用OIDC客户端的场景。
GET https://id.sumsub.com/api/snsId/oauth/authorize
?client_id=<your-client-id>
&response_type=code
&redirect_uri=<白名单中的URI>
&scope=openid+share+name
&state=<随机CSRF随机数>
&code_challenge=<base64url(SHA256(code_verifier))>
&code_challenge_method=S256
&login_hint=<可选邮箱>你需要生成(43–128字符,URL安全),推导,存储verifier(),并在回调后将其转发到后端。Sumsub仅接受(会被拒绝),且必须与白名单字节级匹配。
code_verifiercode_challenge = base64url(SHA256(verifier))sessionStoragecode_challenge_method=S256plainredirect_uri可运行示例:。
examples/oidc-redirect.htmlChoosing scopes
选择scopes
Default . Add , , or
(refresh token) only when downstream logic actually reads
them — extra scopes trigger extra checks in the user-facing flow. Full table:
.
['openid', 'share', 'name']emailprofileoffline_accessreferences/scopes-and-claims.md默认值为。仅当下游逻辑实际需要时,才添加、或(刷新令牌)——额外的scopes会在用户流程中触发额外的检查。完整列表:。
['openid', 'share', 'name']emailprofileoffline_accessreferences/scopes-and-claims.mdStage 2 — Backend: code → access_token
Stage 2 — 后端:code → access_token
Exchange the authorization code on the OIDC token endpoint. The code came from
the browser authorize flow, so its counterpart is the standard OIDC token
endpoint — authenticated with the OIDC (),
not HMAC.
client_secretclient_secret_basicPOST https://id.sumsub.com/api/snsId/oauth/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(<client_id>:<client_secret>)
grant_type=authorization_code
&code=<the code your frontend got>
&redirect_uri=<exact-match URI from Stage 1 — byte-for-byte>
&code_verifier=<PKCE verifier matched to the code_challenge from Stage 1>Response (24-hour ):
access_tokenjson
{
"access_token": "snd-id-con-a-...",
"refresh_token": "snd-id-con-r-...", // only with `offline_access`
"token_type": "Bearer",
"expires_in": 86400,
"id_token": "eyJhbGc..." // only with `openid` — RS256-signed
}Refresh later via ; you cannot request more
scopes on refresh than were granted at consent time.
grant_type: "refresh_token"Thestays server-side — never in the browser bundle. The exchange isclient_secret/client_secret_basic; do not HMAC-sign it. (The App-Token + HMAC auth is still used in Stage 3a/4 for the share token and reuse calls.)client_secret_post
在OIDC令牌端点交换授权码。该代码来自浏览器的授权流程,因此对应的是标准OIDC令牌端点——使用OIDC 进行认证(),而非HMAC。
client_secretclient_secret_basicPOST https://id.sumsub.com/api/snsId/oauth/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(<client_id>:<client_secret>)
grant_type=authorization_code
&code=<前端获取的code>
&redirect_uri=<与Stage 1完全匹配的URI——字节级匹配>
&code_verifier=<与Stage 1的code_challenge匹配的PKCE verifier>响应(有效期24小时的):
access_tokenjson
{
"access_token": "snd-id-con-a-...",
"refresh_token": "snd-id-con-r-...", // 仅当使用`offline_access`时返回
"token_type": "Bearer",
"expires_in": 86400,
"id_token": "eyJhbGc..." // 仅当使用`openid`时返回——RS256签名
}后续可通过刷新令牌;刷新时无法请求比授权时更多的scopes。
grant_type: "refresh_token"需保留在服务器侧——绝不能出现在浏览器包中。交换使用client_secret/client_secret_basic;请勿对其进行HMAC签名。(App-Token + HMAC认证仍用于Stage 3a/4的共享令牌和复用调用。)client_secret_post
Gotchas
常见问题
- 401 has three common causes: PKCE mismatch (verifier doesn't hash to the challenge sent at authorize), code already used (single-use; refreshing the callback page double-fires), or code expired (short TTL — exchange immediately).
Invalid code - must match Stage 1 byte-for-byte — trailing slash, casing, port and query all count, or the exchange fails.
redirect_uri - validation — use a mature library (
id_token,jose,pyjwt) against thejjwtfromjwks_uri. Never trust unverified JWT contents..well-known/openid-configuration
- 401有三个常见原因:PKCE不匹配(verifier哈希后与授权时发送的challenge不一致)、代码已被使用(单次有效;刷新回调页面会导致重复触发)、代码过期(TTL较短——需立即交换)。
Invalid code - 必须与Stage 1字节级匹配——末尾斜杠、大小写、端口和查询参数均会影响匹配,否则交换会失败。
redirect_uri - 验证——使用成熟的库(
id_token、jose、pyjwt)结合jjwt中的.well-known/openid-configuration进行验证。切勿信任未验证的JWT内容。jwks_uri
Stage 3 — Backend: access_token → share token + consent
Stage 3 — 后端:access_token → 共享令牌 + 同意流程
Mint a share token scoped to a recipient (). Always implement
the consent fallback (Stage 3b) — is returned
whenever the user hasn't yet authorised sharing with this specific
. In the common self-recipient case both consents are usually
granted in one click on the Stage 1 consent screen, but production code that
only handles breaks the first time a user re-enters
the flow or a recipient configuration differs.
forClientIdsharingAllowed: falseforClientIdsharingAllowed: true生成针对接收方()的共享令牌。务必实现同意回退流程(Stage 3b)——当用户尚未授权与该特定共享时,会返回。在常见的自接收方场景中,通常会在Stage 1的同意界面中一键授予两个权限,但仅处理的生产代码会在用户重新进入流程或接收方配置不同时失效。
forClientIdforClientIdsharingAllowed: falsesharingAllowed: trueStage 3a — mint share token
Stage 3a — 生成共享令牌
POST https://api.sumsub.com/resources/accessTokens/sumsubIdShareToken
{
"sumsubIdConnectToken": "<access_token from Stage 2>",
"forClientId": "<recipient clientId>",
"ttlInSecs": 1800
}forClientId- Self-recipient (common case): your own workspace , visible in the Dashboard top-left.
clientId - Third-party recipient: the partner's (they must be pre-registered as a partner in the Dashboard).
clientId
Response:
json
{
"token": "_act-snsId-...",
"forClientId": "<echoed>",
"sharingAllowed": true // or false → continue to Stage 3b
}POST https://api.sumsub.com/resources/accessTokens/sumsubIdShareToken
{
"sumsubIdConnectToken": "<Stage 2获取的access_token>",
"forClientId": "<接收方clientId>",
"ttlInSecs": 1800
}forClientId- 自接收方(常见场景):你自己工作区的,可在控制台左上角查看。
clientId - 第三方接收方:合作伙伴的(他们必须已在控制台中注册为合作伙伴)。
clientId
响应:
json
{
"token": "_act-snsId-...",
"forClientId": "<回显值>",
"sharingAllowed": true // 或false → 进入Stage 3b
}Stage 3b — partner-consent shareLink
Stage 3b — 合作伙伴同意shareLink
If , mint a consent link and redirect the user:
sharingAllowed: falsePOST https://api.sumsub.com/resources/snsId/oauth/shareLink
Authorization: Bearer <access_token from Stage 2>
X-App-Token + X-App-Access-Ts + X-App-Access-Sig
{
"redirectUri": "<your shareLink callback URL — must be in the allowlist>",
"forClientId": "<same recipient clientId>",
"displayMode": "page",
"state": "<csrf-or-correlation-id>"
}Response: — expires in 5
minutes.
{ "link": "https://id.sumsub.com/share/<id>" }Persist the server-side keyed by (in-memory Map is OK
for demos; use Redis / encrypted cookie / session table for prod). Redirect
the user to the . After consent, Sumsub redirects back to
as — a signed RS256 JWT whose payload carries
(alongside and ).
access_tokenstatelinkredirectUri?token=<JWT>statesharingAllowed: trueforClientId⚠️is inside the JWT payload, not a top-levelstatequery param. Decode the?state=JWT to readtokenback (verify its signature against thestatefirst), then look up the savedjwks_uriby thataccess_token. Re-call Stage 3a — the share token now returnsstate. Decoded payload shape:sharingAllowed: true.{ iss, sub, aud, iat, exp, sharingAllowed, state, forClientId }
Full reference implementation:
(in-memory
Map keyed by , handler).
examples/express-callback.jspendingSharestate/share-complete若,生成同意链接并重定向用户:
sharingAllowed: falsePOST https://api.sumsub.com/resources/snsId/oauth/shareLink
Authorization: Bearer <Stage 2获取的access_token>
X-App-Token + X-App-Access-Ts + X-App-Access-Sig
{
"redirectUri": "<你的shareLink回调URL——必须在白名单中>",
"forClientId": "<相同的接收方clientId>",
"displayMode": "page",
"state": "<csrf或关联ID>"
}响应:——有效期5分钟。
{ "link": "https://id.sumsub.com/share/<id>" }将在服务器侧以为键存储(演示环境可使用内存Map;生产环境请使用Redis/加密Cookie/会话表)。将用户重定向到该。用户同意后,Sumsub会将用户重定向回,格式为——这是一个签名后的RS256 JWT,其payload中包含(同时包含和)。
access_tokenstatelinkredirectUri?token=<JWT>statesharingAllowed: trueforClientId⚠️在JWT payload中,而非顶级state查询参数。解码?state=JWT以读取token(先验证其签名与state匹配),然后通过该jwks_uri查找保存的state。重新调用Stage 3a——此时共享令牌会返回access_token。解码后的payload格式:sharingAllowed: true。{ iss, sub, aud, iat, exp, sharingAllowed, state, forClientId }
完整参考实现:(内存 Map以为键,处理器)。
examples/express-callback.jspendingSharestate/share-completeStage 4 — Materialise the applicant
Stage 4 — 落地申请人信息
With a usable share token, create the applicant in your workspace. Always
preview first.
在获得可用的共享令牌后,在你的工作区中创建申请人信息。务必先预览。
Preview (no side effects)
预览(无副作用)
GET https://api.sumsub.com/resources/api/reusableIdentity/reuse/preview
?shareToken=<token>
&levelName=<your-level>
&userId=<externalUserId>Returns the same shape that would create — use
it to surface compatibility errors before committing.
ApplicantPublicDto/reuseGET https://api.sumsub.com/resources/api/reusableIdentity/reuse/preview
?shareToken=<token>
&levelName=<你的验证等级>
&userId=<externalUserId>返回与创建的相同的格式——可用于在提交前排查兼容性错误。
/reuseApplicantPublicDtoCommit (creates the applicant)
提交(创建申请人)
POST https://api.sumsub.com/resources/api/reusableIdentity/reuse
?shareToken=<token>
&levelName=<your-level>
&userId=<externalUserId>userIdexternalUserIdPOST https://api.sumsub.com/resources/api/reusableIdentity/reuse
?shareToken=<token>
&levelName=<你的验证等级>
&userId=<externalUserId>userIdexternalUserIdReuse error codes
复用错误码
All responses include (integer) and (kebab-case)
suitable for code branching. is generic token validity — no ,
just .
400errorCodeerrorName401errorCodedescription| HTTP / errorCode | name | Cause | Action |
|---|---|---|---|
| 401 / — | | Share token expired (TTL exceeded), already consumed, or malformed | Re-mint via Stage 3a — call |
400 / | | | Add the recipient as a partner in the Dashboard (Sumsub UI only — no public API) |
400 / | | The partner ID extracted from the share token is invalid | Re-check |
400 / | | Share token is malformed or expired | Re-mint via Stage 3a |
400 / | | Share token is not suitable for reuse at the given | Re-mint share token with matching scope, retry against the same level |
400 / | | Reusable KYC is disabled for this workspace | Contact Sumsub support to enable Reusable KYC for your workspace |
400 / | | Generic fallback — donor doesn't meet reuse eligibility and no more specific reason matched | Inspect the donor in the Dashboard; pick a different donor or fall back to the WebSDK |
400 / | | Donor applicant is not in an approved state (pending / rejected / on-hold) | Wait for donor's KYC to be approved; use a donor with approved status |
400 / | | Donor applicant is inactive (deactivated / blocked) | Use a different donor — reactivation is Sumsub-side |
400 / | | Donor's source moderation type is not suitable for reuse | Donor needs standard KYC moderation; specialised flows aren't reusable |
400 / | | Required selfie or identity document is missing on donor | Pick a level without that doc-set, or have donor re-verify |
400 / | | Recipient level requires liveness; donor's selfie wasn't captured with liveness | Use a level without |
400 / | | Required document types don't overlap between donor and recipient levels | Pick a recipient level whose required docs are a subset of donor's |
400 / | | Recipient level requires document types the donor doesn't carry | Use a less strict level, or fall back to the WebSDK for full capture |
400 / | | Donor's Proof of Identity is outdated and not valid for reuse | Donor must re-submit fresh POI, or relax recipient level's POI freshness window |
400 / | | Donor's Proof of Address is outdated and not valid for reuse | Donor must re-submit fresh POA, or relax recipient level's POA freshness window |
400 / | | Donor doesn't meet the age criteria of the recipient level | No recovery — different user required |
400 / | | Capture settings (file upload vs live camera, etc.) mismatch between donor and recipient | Align recipient level's |
400 / | | Donor's email doesn't match the email already on the recipient applicant | Resolve duplicate-applicant collision — different |
400 / | | Donor's phone doesn't match the phone already on the recipient applicant | Same as 10518 — resolve collision |
400 / | | Donor Sumsub ID account has no stored documents (email-only, KYC never completed) | Donor must complete full KYC on |
After the applicant is materialised, your webhook is the
authoritative signal for verification outcome — wire it up the same way as
for the WebSDK. See
for the
webhook material and
for the
sandbox webhook subscription setup.
applicantReviewedsumsub-integrate-websdksumsub-manage-webhooks所有响应均包含(整数)和(短横线分隔),适用于代码分支处理。为通用令牌有效性错误——无,仅包含。
400errorCodeerrorName401errorCodedescription| HTTP / errorCode | 名称 | 原因 | 操作 |
|---|---|---|---|
| 401 / — | | 共享令牌过期(超过TTL)、已被使用或格式错误 | 通过Stage 3a重新生成——使用仍有效的 |
400 / | | | 在控制台中将接收方添加为合作伙伴(仅Sumsub UI操作——无公开API) |
400 / | | 从共享令牌中提取的合作伙伴ID无效 | 重新检查 |
400 / | | 共享令牌格式错误或已过期 | 通过Stage 3a重新生成 |
400 / | | 共享令牌不适用于指定的 | 重新生成匹配scope的共享令牌,针对相同等级重试 |
400 / | | 工作区已禁用可复用KYC | 联系Sumsub支持团队为你的工作区启用可复用KYC |
400 / | | 通用回退——捐赠方不符合复用条件,且无更具体的原因匹配 | 在控制台中检查捐赠方信息;更换捐赠方或回退到WebSDK |
400 / | | 捐赠方申请人未处于已批准状态(待审核/已拒绝/暂停) | 等待捐赠方KYC审核通过;使用已批准的捐赠方 |
400 / | | 捐赠方申请人已失效(已停用/已封禁) | 使用其他捐赠方——重新激活需通过Sumsub侧操作 |
400 / | | 捐赠方的源审核类型不适用于复用 | 捐赠方需要标准KYC审核;特殊流程无法复用 |
400 / | | 捐赠方缺少必需的自拍或身份证件 | 选择无需该文档集的等级,或让捐赠方重新验证 |
400 / | | 接收方等级要求活体检测;捐赠方的自拍未进行活体采集 | 使用SELFIE项中无 |
400 / | | 捐赠方与接收方等级的必需文档类型无重叠 | 选择必需文档为捐赠方子集的接收方等级 |
400 / | | 接收方等级要求的文档类型捐赠方未提供 | 使用要求更低的等级,或回退到WebSDK进行完整采集 |
400 / | | 捐赠方的身份证件已过期,无法复用 | 捐赠方必须重新提交有效的身份证件,或放宽接收方等级的身份证件有效期要求 |
400 / | | 捐赠方的地址证明已过期,无法复用 | 捐赠方必须重新提交有效的地址证明,或放宽接收方等级的地址证明有效期要求 |
400 / | | 捐赠方不符合接收方等级的年龄要求 | 无法恢复——需更换用户 |
400 / | | 捐赠方与接收方的采集设置(文件上传vs实时相机等)不匹配 | 对齐接收方等级的 |
400 / | | 捐赠方的邮箱与接收方申请人已有的邮箱不匹配 | 解决重复申请人冲突——使用不同的 |
400 / | | 捐赠方的电话与接收方申请人已有的电话不匹配 | 与10518相同——解决冲突 |
400 / | | 捐赠方Sumsub ID账户无存储的文档(仅含邮箱,未完成KYC) | 捐赠方必须先在 |
申请人信息落地后, webhook是验证结果的权威信号——请按照WebSDK的方式配置。Webhook相关内容请参考,沙箱webhook订阅设置请参考。
applicantReviewedsumsub-integrate-websdksumsub-manage-webhooksGoing-live checklist
上线清单
When the user says "we're ready for prod":
- Sumsub support has enabled Sumsub ID Connect for the production
(separate from sandbox).
clientId - Production s (both authorize and shareLink callbacks) are in the Dashboard allowlist on the prod workspace.
redirectUri - Backend token-exchange endpoint is auth-gated (only the authenticated user can exchange a code minted for them).
- signature is verified against the prod JWKS.
id_token - is stable per real user (not email, not display name).
externalUserId - is called before
/reuse/previewso compatibility errors surface as UX, not 4xx noise./reuse - Stage 3b shareLink flow is implemented and tested — don't ship code
that only handles .
sharingAllowed: true - In-memory state used for the shareLink callback is replaced with a real session store (Redis / encrypted cookie / DB).
- App Token + secret + OIDC are in the prod secret store, not committed and not in browser bundles.
client_secret - Refresh-token storage (if used) is server-side only, encrypted at rest, single-tenant.
当用户表示「我们准备好上线生产环境」时:
- Sumsub支持团队已为生产环境的启用Sumsub ID Connect(与沙箱环境分离)。
clientId - 生产环境的(授权和shareLink回调地址)已加入生产工作区的控制台白名单。
redirectUri - 后端令牌交换端点已添加认证限制(仅已认证用户可交换为其生成的代码)。
- 签名已通过生产环境JWKS验证。
id_token - 对每个真实用户稳定(非邮箱、非显示名称)。
externalUserId - 在调用前先调用
/reuse,以便兼容性错误以用户体验的形式呈现,而非4xx错误。/reuse/preview - 已实现并测试Stage 3b的shareLink流程——请勿仅处理的代码上线。
sharingAllowed: true - shareLink回调使用的内存状态已替换为真实的会话存储(Redis/加密Cookie/数据库)。
- App Token + 密钥 + OIDC 已存储在生产环境密钥管理系统中,未提交到代码库且未出现在浏览器包中。
client_secret - 刷新令牌(若使用)仅存储在服务器侧,静态加密,单租户隔离。
See also
另请参阅
- — full scope table, token-endpoint response shape, error codes.
references/scopes-and-claims.md - — preconditions validator.
scripts/preflight.sh - — Method 1 (
examples/oidc-button.html) — recommended.createButton - — Method 2 (
examples/oidc-modal.html).openModal - — Method 3 (raw OIDC redirect with PKCE).
examples/oidc-redirect.html - — full backend (Stages 2-4 incl. Stage 3b consent flow).
examples/express-callback.js - — auth signing reference.
sumsub-api-auth - — when the workspace has no recipient level yet.
sumsub-create-level - — sibling skill; reuse its webhook material once the applicant is materialised.
sumsub-integrate-websdk
- — 完整的scope列表、令牌端点响应格式、错误码。
references/scopes-and-claims.md - — 前置条件验证脚本。
scripts/preflight.sh - — 方法1(
examples/oidc-button.html)——推荐方案。createButton - — 方法2(
examples/oidc-modal.html)。openModal - — 方法3(带PKCE的原生OIDC重定向)。
examples/oidc-redirect.html - — 完整后端实现(Stages 2-4,含Stage 3b同意流程)。
examples/express-callback.js - — 认证签名参考。
sumsub-api-auth - — 当工作区无接收方等级时使用。
sumsub-create-level - — 同类方案;申请人信息落地后可复用其webhook内容。
sumsub-integrate-websdk