sumsub-api-auth

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Sumsub — API authentication (App Token)

Sumsub — API身份验证(App Token)

How to sign and send authenticated requests to
https://api.sumsub.com
, per the official reference.
如何按照官方文档
https://api.sumsub.com
进行签名并发送已认证的请求。

⚠️ Sandbox tokens only

⚠️ 仅使用沙箱令牌

Never share, paste, or use a production Sumsub App Token / secret with Claude. If the user offers a prod token, refuse and ask for the sandbox pair instead.
  • Sandbox tokens are created from the dashboard while it is in Sandbox mode. They are scoped to sandbox data only — leaking one cannot expose real applicant PII or move real money.
  • A production token grants full programmatic access to live applicants, including their identity documents. Treat it like a banking credential.
  • Sumsub locks tokens to the environment they were minted in: a sandbox token returns
    401
    against production data and vice versa, so insisting on sandbox is also the practical default.
If the user pastes what looks like a production secret into the conversation, flag it immediately, advise rotating it in the dashboard, and continue only with a freshly-generated sandbox pair.
切勿与Claude共享、粘贴或使用生产环境的Sumsub App Token/密钥。 如果用户提供生产环境令牌,请拒绝并要求提供沙箱令牌对。
  • 沙箱令牌是在仪表板处于沙箱模式时创建的。它们仅作用于沙箱数据——泄露沙箱令牌不会暴露真实申请人的个人身份信息(PII)或涉及真实资金操作。
  • 生产环境令牌授予对真实申请人的完全编程访问权限,包括他们的身份证件。请像对待银行凭证一样对待它。
  • Sumsub会将令牌锁定在其生成的环境中:沙箱令牌在生产环境数据中会返回
    401
    ,反之亦然,因此坚持使用沙箱也是实际操作中的默认选择。
如果用户在对话中粘贴了看起来像是生产环境密钥的内容,请立即指出,建议他们在仪表板中轮换密钥,并且仅使用新生成的沙箱令牌对继续操作。

What you need from the user

需要从用户处获取的信息

VarWhere it comes from
SUMSUB_APP_TOKEN
https://cockpit.sumsub.com/checkus/devSpace/appTokens — switch the workspace toggle to Sandbox first, then Connect Sumsub to your AI agent -> Build & configure -> Generate token . Shown once.
SUMSUB_SECRET_KEY
Same dialog as the token. Also shown once.
SUMSUB_BASE
https://api.sumsub.com
(same host for sandbox and prod — the token decides the mode).
⚠️ The token + secret are revealed exactly once at creation. Copy both into
.env
(or your secret store) before closing the dialog — there's no recovery flow, only re-generation.
Advise the user to store them in
.claude/settings.local.json
(gitignored, auto-loaded by Claude Code) or in
.env
:
json
// .claude/settings.local.json
{
  "env": {
    "SUMSUB_APP_TOKEN": "sbx:...",
    "SUMSUB_SECRET_KEY": "..."
  }
}
bash
undefined
变量获取来源
SUMSUB_APP_TOKEN
https://cockpit.sumsub.com/checkus/devSpace/appTokens — 先将工作区切换为沙箱模式,然后点击Connect Sumsub to your AI agent -> Build & configure -> Generate token。令牌仅显示一次。
SUMSUB_SECRET_KEY
与令牌在同一个对话框中生成。同样仅显示一次。
SUMSUB_BASE
https://api.sumsub.com
(沙箱和生产环境使用相同的主机——令牌决定使用的模式)。
⚠️ 令牌+密钥在创建时仅显示一次。在关闭对话框前,将两者复制到
.env
(或你的密钥存储中)——没有恢复流程,只能重新生成。
建议用户将它们存储在
.claude/settings.local.json
(已被Git忽略,由Claude Code自动加载)或
.env
中:
json
// .claude/settings.local.json
{
  "env": {
    "SUMSUB_APP_TOKEN": "sbx:...",
    "SUMSUB_SECRET_KEY": "..."
  }
}
bash
undefined

.env

.env

SUMSUB_APP_TOKEN=sbx:... SUMSUB_SECRET_KEY=...

If either credential is missing, stop and ask. Do not invent placeholders.
SUMSUB_APP_TOKEN=sbx:... SUMSUB_SECRET_KEY=...

如果缺少任一凭证,请停止操作并询问用户。不要使用占位符。

The three required headers

三个必填请求头

Every request to
api.sumsub.com
must carry:
HeaderValue
X-App-Token
The App Token, verbatim.
X-App-Access-Ts
Current Unix time in seconds (UTC). Must be within ±60s of Sumsub's clock.
X-App-Access-Sig
Lowercase hex HMAC-SHA256 of the signing string, keyed by the secret.
HTTPS is mandatory — plain
http://
is rejected.
每个发送到
api.sumsub.com
的请求必须包含:
请求头
X-App-Token
App Token,原样填写。
X-App-Access-Ts
当前Unix时间(秒级,UTC时区)。必须与Sumsub的时钟误差在±60秒以内。
X-App-Access-Sig
签名字符串的小写十六进制HMAC-SHA256值,使用密钥进行加密。
必须使用HTTPS——普通的
http://
请求会被拒绝。

Signing string

签名字符串

Concatenate, with no separators:
<ts><HTTP_METHOD_UPPER><request_uri_with_query><body_bytes_or_empty>
  • ts
    — the exact value you put in
    X-App-Access-Ts
    (string of digits).
  • HTTP_METHOD_UPPER
    GET
    ,
    POST
    ,
    PATCH
    ,
    PUT
    ,
    DELETE
    — uppercase.
  • request_uri_with_query
    — path starting with
    /
    , including the query string if any. Examples:
    /resources/applicants/-/one
    ,
    /resources/accessTokens?userId=abc&levelName=basic-kyc-level
    .
  • Body — the raw bytes you send. For
    GET
    /
    DELETE
    with no body, append nothing (empty string). For JSON, sign the exact bytes you'll transmit — re-serializing later will break the signature.
Then
hex(hmac_sha256(secret, signing_string))
, lowercase.
将以下内容无分隔符拼接:
<ts><HTTP_METHOD_UPPER><request_uri_with_query><body_bytes_or_empty>
  • ts
    —— 你填入
    X-App-Access-Ts
    的精确值(数字字符串)。
  • HTTP_METHOD_UPPER
    ——
    GET
    POST
    PATCH
    PUT
    DELETE
    ——大写形式。
  • request_uri_with_query
    —— 以
    /
    开头的路径,包含查询字符串(如果有)。示例:
    /resources/applicants/-/one
    /resources/accessTokens?userId=abc&levelName=basic-kyc-level
  • Body —— 你发送的原始字节。对于无请求体的
    GET
    /
    DELETE
    请求,追加空字符串。对于JSON请求,签名你将传输的精确字节——后续重新序列化会破坏签名。
然后生成
hex(hmac_sha256(secret, signing_string))
,并转为小写。

Worked example (from the docs)

示例(来自官方文档)

Signing string for
POST /resources/accessTokens?userId=...&levelName=basic-kyc-level&ttlInSecs=600
with no body, at ts
1607551635
:
1607551635POST/resources/accessTokens?userId=cfd20712-24a2-4c7d-9ab0-146f3c142335&levelName=basic-kyc-level&ttlInSecs=600
在时间戳
1607551635
时,对
POST /resources/accessTokens?userId=...&levelName=basic-kyc-level&ttlInSecs=600
(无请求体)生成的签名字符串:
1607551635POST/resources/accessTokens?userId=cfd20712-24a2-4c7d-9ab0-146f3c142335&levelName=basic-kyc-level&ttlInSecs=600

Reference implementations

参考实现

The official multi-language examples live at SumSubstance/AppTokenUsageExamples (Java, JS, Python, Ruby, Go, PHP, C#). Use those for production integrations.
For one-off calls or debugging, this skill ships two small helpers:
  • scripts/sumsub_sign.py
    — print the three headers for a given method/path/body. No network calls.
  • scripts/sumsub_curl.sh
    — sign +
    curl
    in one shot. Reads
    SUMSUB_APP_TOKEN
    /
    SUMSUB_SECRET_KEY
    from the environment.
Run scripts using
${CLAUDE_SKILL_DIR}/scripts/<script>
so they resolve correctly regardless of the working directory.
官方多语言示例位于SumSubstance/AppTokenUsageExamples(Java、JS、Python、Ruby、Go、PHP、C#)。生产环境集成请使用这些示例。
对于一次性调用或调试,本技能提供两个小型工具:
  • scripts/sumsub_sign.py
    —— 针对给定的方法/路径/请求体输出三个请求头。无网络调用。
  • scripts/sumsub_curl.sh
    —— 一键完成签名+
    curl
    请求。从环境变量中读取
    SUMSUB_APP_TOKEN
    /
    SUMSUB_SECRET_KEY
使用
${CLAUDE_SKILL_DIR}/scripts/<script>
运行脚本,这样无论当前工作目录如何,脚本都能正确解析路径。

Quick check — fetch the current applicant count

快速检查——获取当前申请人数量

bash
export SUMSUB_APP_TOKEN='sbx:...'   # sandbox token, refuse prod
export SUMSUB_SECRET_KEY='...'

${CLAUDE_SKILL_DIR}/scripts/sumsub_curl.sh GET '/resources/applicants/-/count'
A
200
with a JSON body confirms the signature is correct. A
401
with
{"description":"Invalid signature"}
means the signing string or secret is off — re-check, in order:
  1. Token/secret pair matches (copy-paste truncation is common).
  2. Timestamp is in seconds, not milliseconds, and your clock is in sync.
  3. Path includes the leading
    /
    and the full query string.
  4. Body bytes signed are byte-identical to bytes sent (watch for trailing newlines added by editors / heredocs).
  5. Method is uppercase.
bash
export SUMSUB_APP_TOKEN='sbx:...'   # 沙箱令牌,拒绝使用生产环境令牌
export SUMSUB_SECRET_KEY='...'

${CLAUDE_SKILL_DIR}/scripts/sumsub_curl.sh GET '/resources/applicants/-/count'
返回
200
状态码和JSON响应体表示签名正确。返回
401
状态码和
{"description":"Invalid signature"}
表示签名字符串或密钥有误——请按以下顺序检查:
  1. 令牌/密钥对是否匹配(复制粘贴时截断是常见问题)。
  2. 时间戳是秒级,而非毫秒级,且你的时钟已同步。
  3. 路径包含开头的
    /
    完整的查询字符串。
  4. 签名的请求体字节与发送的字节完全一致(注意编辑器/ heredoc添加的换行符)。
  5. 请求方法是大写形式。

Signing
multipart/form-data
requests

multipart/form-data
请求进行签名

Some endpoints take a file upload — most commonly idDoc photo upload at
POST /resources/applicants/{applicantId}/info/idDoc
. For these:
  • Sign the full raw multipart body, byte-for-byte, including boundary markers, part headers, JSON metadata, and file bytes. There is no multipart-specific exemption — the rule is the same as for JSON: signing string =
    ts + METHOD + path + body_bytes
    .
  • The
    Content-Type
    header is
    multipart/form-data; boundary=<boundary>
    , where
    <boundary>
    matches the one woven into the body bytes you signed.
The pitfall: most HTTP libraries (
curl -F
,
requests
with
files=
,
fetch
with
FormData
) generate the boundary internally and never expose the exact bytes — so you cannot sign what they will send. Workaround: build the body in memory yourself, hash it, then transmit those exact bytes with
--data-binary
/ a raw send.
部分端点支持文件上传——最常见的是
POST /resources/applicants/{applicantId}/info/idDoc
接口上传身份证件照片。对于这类请求:
  • 对完整的原始multipart请求体进行签名,逐字节签名,包括边界标记、部分请求头、JSON元数据和文件字节。没有针对multipart的特殊豁免——规则与JSON请求相同:签名字符串 =
    ts + METHOD + path + body_bytes
  • Content-Type
    请求头为
    multipart/form-data; boundary=<boundary>
    ,其中
    <boundary>
    必须与你签名的请求体字节中使用的边界一致。
陷阱:大多数HTTP库(
curl -F
、带
files=
参数的
requests
、带
FormData
fetch
)会在内部生成边界,且不会暴露精确的字节——因此你无法对它们将发送的内容进行签名。解决方法:在内存中自行构建请求体,进行哈希,然后使用
--data-binary
/原始发送方式传输这些精确字节。

Python recipe (canonical)

Python示例(标准实现)

python
import hashlib, hmac, json, os, time, uuid
from pathlib import Path
from urllib.request import Request, urlopen

APP_TOKEN = os.environ["SUMSUB_APP_TOKEN"]
SECRET    = os.environ["SUMSUB_SECRET_KEY"]
APPLICANT = "6a170f852f9d88fe6eda2636"          # from create-applicant response
FILE      = Path("/path/to/passport.png")
METADATA  = {"idDocType": "PASSPORT", "country": "RUS"}

boundary = "----sumsub-" + uuid.uuid4().hex
crlf = b"\r\n"
parts = [
    b"--" + boundary.encode(),
    b'Content-Disposition: form-data; name="metadata"',
    b"Content-Type: application/json",
    b"",
    json.dumps(METADATA).encode(),
    b"--" + boundary.encode(),
    f'Content-Disposition: form-data; name="content"; filename="{FILE.name}"'.encode(),
    b"Content-Type: image/png",
    b"",
    FILE.read_bytes(),
    b"--" + boundary.encode() + b"--",
    b"",
]
body = crlf.join(parts)

method, url_path = "POST", f"/resources/applicants/{APPLICANT}/info/idDoc"
ts = str(int(time.time()))
sig = hmac.new(
    SECRET.encode(),
    ts.encode() + method.encode() + url_path.encode() + body,
    hashlib.sha256,
).hexdigest()

req = Request(
    "https://api.sumsub.com" + url_path,
    data=body, method="POST",
    headers={
        "X-App-Token": APP_TOKEN,
        "X-App-Access-Ts": ts,
        "X-App-Access-Sig": sig,
        "Content-Type": f"multipart/form-data; boundary={boundary}",
        "Content-Length": str(len(body)),
        "X-Agent-Source": "sumsub-skills",
        "X-Agent-Source-Ver": "1.2.0",
    },
)
print(urlopen(req).read().decode())
Curl-based fallbacks (e.g.
curl --data-binary @raw-multipart.bin
after pre-building the body to disk) work too, but the boundary in the body and in
Content-Type
must match exactly — easier to keep them in sync in code.
python
import hashlib, hmac, json, os, time, uuid
from pathlib import Path
from urllib.request import Request, urlopen

APP_TOKEN = os.environ["SUMSUB_APP_TOKEN"]
SECRET    = os.environ["SUMSUB_SECRET_KEY"]
APPLICANT = "6a170f852f9d88fe6eda2636"          # 来自创建申请人的响应
FILE      = Path("/path/to/passport.png")
METADATA  = {"idDocType": "PASSPORT", "country": "RUS"}

boundary = "----sumsub-" + uuid.uuid4().hex
crlf = b"\r\n"
parts = [
    b"--" + boundary.encode(),
    b'Content-Disposition: form-data; name="metadata"',
    b"Content-Type: application/json",
    b"",
    json.dumps(METADATA).encode(),
    b"--" + boundary.encode(),
    f'Content-Disposition: form-data; name="content"; filename="{FILE.name}"'.encode(),
    b"Content-Type: image/png",
    b"",
    FILE.read_bytes(),
    b"--" + boundary.encode() + b"--",
    b"",
]
body = crlf.join(parts)

method, url_path = "POST", f"/resources/applicants/{APPLICANT}/info/idDoc"
ts = str(int(time.time()))
sig = hmac.new(
    SECRET.encode(),
    ts.encode() + method.encode() + url_path.encode() + body,
    hashlib.sha256,
).hexdigest()

req = Request(
    "https://api.sumsub.com" + url_path,
    data=body, method="POST",
    headers={
        "X-App-Token": APP_TOKEN,
        "X-App-Access-Ts": ts,
        "X-App-Access-Sig": sig,
        "Content-Type": f"multipart/form-data; boundary={boundary}",
        "Content-Length": str(len(body)),
        "X-Agent-Source": "sumsub-skills",
        "X-Agent-Source-Ver": "1.2.0",
    },
)
print(urlopen(req).read().decode())
基于Curl的替代方案(例如在预先构建好请求体到磁盘后使用
curl --data-binary @raw-multipart.bin
)也可行,但请求体中的边界必须与
Content-Type
中的边界完全一致——在代码中保持同步会更容易。

Generating an SDK access token (common follow-up)

生成SDK访问令牌(常见后续操作)

The most-asked endpoint after auth works:
POST /resources/accessTokens?userId=<your_user_id>&levelName=<level>&ttlInSecs=600
Body is empty. Response contains
token
— pass that to the Web / Mobile SDK. Full reference: https://docs.sumsub.com/reference/generate-access-token.
认证成功后最常调用的端点:
POST /resources/accessTokens?userId=<your_user_id>&levelName=<level>&ttlInSecs=600
请求体为空。响应包含
token
——将其传递给Web/移动SDK。完整文档:https://docs.sumsub.com/reference/generate-access-token

See also

另请参阅

  • references/signing-pitfalls.md — every gotcha that produces
    401 Invalid signature
    and how to spot it.
  • references/signing-pitfalls.md —— 所有会导致
    401 Invalid signature
    的陷阱以及如何识别它们。