关于 sumsub-api-auth
供 Claude 使用的 Sumsub API App Token 认证技能,介绍 HMAC-SHA256 请求签名与沙盒凭据安全,并提供生成请求头和发送签名请求的脚本。
主要功能
- 01HMAC-SHA256 请求签名
- 02沙盒凭据安全规范
- 03请求头生成脚本
- 04签名 curl 请求脚本
- 05多部分上传签名指南
适用场景
- 01排查 401 签名错误
- 02发送临时签名 API 请求
- 03上传申请人身份文件
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ sumsub/ agent-skills --skill sumsub-api-auth安装后在 Claude Code 中输入/sumsub-api-auth确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/signing-pitfalls.mdscripts/sumsub_curl.shsumsub_sign.py详细介绍
Sumsub — API 身份验证(App Token)
根据官方参考,介绍如何对请求签名并发送经过身份验证的请求到 https://api.sumsub.com。
⚠️ 仅使用沙盒令牌
切勿与 Claude 分享、粘贴或使用生产环境的 Sumsub App Token / 密钥。如果用户提供生产环境令牌,请拒绝,并要求提供沙盒令牌对,生成地址为 https://cockpit.sumsub.com/checkus/home?sbx=true(Connect Sumsub to your AI agent -> Build & configure -> Generate token)。
- 在仪表板处于沙盒模式时创建沙盒令牌。它们仅限访问沙盒数据——即使泄露,也无法暴露真实申请人的个人身份信息或转移真实资金。
- 生产环境令牌可对真实申请人进行完整的程序化访问,包括其身份证件。请将其视为银行凭证。
- Sumsub 会将令牌锁定在其创建时所处的环境中:沙盒令牌用于生产环境数据时会返回
401,反之亦然,因此坚持使用沙盒也是实际上的默认选择。
如果用户在对话中粘贴了看起来像生产环境密钥的内容,请立即指出,建议在仪表板中轮换该密钥,并且仅在获取新生成的沙盒令牌对后继续操作。
需要用户提供的内容
| 变量 | 来源 |
|---|---|
SUMSUB_APP_TOKEN | https://cockpit.sumsub.com/checkus/home?sbx=true — 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 中:
// .claude/settings.local.json
{
"env": {
"SUMSUB_APP_TOKEN": "sbx:...",
"SUMSUB_SECRET_KEY": "..."
}
}
# .env
SUMSUB_APP_TOKEN=sbx:...
SUMSUB_SECRET_KEY=...
如果缺少任一凭证,请停止并询问。不要编造占位符。
必需的三个标头
每个发送到 api.sumsub.com 的请求都必须携带:
| 标头 | 值 |
|---|---|
X-App-Token | App Token 原样填写。 |
X-App-Access-Ts | 当前 Unix 时间(秒,UTC)。必须与 Sumsub 时钟相差不超过 ±60 秒。 |
X-App-Access-Sig | 签名字符串的 HMAC-SHA256 小写十六进制值,使用密钥作为密钥。 |
必须使用 HTTPS——普通 http:// 请求会被拒绝。
签名字符串
按以下顺序拼接,不使用分隔符:
<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。- 请求正文 — 发送的原始字节。对于没有正文的
GET/DELETE请求,不追加任何内容(空字符串)。对于 JSON,请对将要传输的确切字节签名——之后重新序列化会导致签名失效。
然后计算 hex(hmac_sha256(secret, signing_string)),结果使用小写。
示例(摘自文档)
在 ts 为 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
参考实现
官方的多语言示例位于 sumsub/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>,这样无论当前工作目录是什么,都能正确解析路径。
快速检查 — 获取当前申请人数量
export SUMSUB_APP_TOKEN='sbx:...' # sandbox token, refuse prod
export SUMSUB_SECRET_KEY='...'
${CLAUDE_SKILL_DIR}/scripts/sumsub_curl.sh GET '/resources/applicants/-/count'
返回 200 和 JSON 正文即表示签名正确。若返回 401 和
{"description":"Invalid signature"},则说明签名字符串或密钥有误——请按以下顺序重新检查:
- Token/密钥配对是否正确(复制粘贴时截断很常见)。
- 时间戳的单位是秒,而不是毫秒,并且时钟已同步。
- 路径是否包含开头的
/和完整查询字符串。 - 签名的正文内容是否与发送的字节完全一致(注意编辑器或 heredoc 可能会添加结尾换行符)。
- 方法是否为大写。
签署 multipart/form-data 请求
某些端点需要上传文件——最常见的是在
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 示例(规范做法)
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.5.0",
},
)
print(urlopen(req).read().decode())
基于 Curl 的备选方案(例如先将正文构建到磁盘,再使用
curl --data-binary @raw-multipart.bin)也可行,但正文中的边界必须与 Content-Type 中的边界完全一致——在代码中更容易保持两者同步。
生成 SDK 访问令牌(常见后续步骤)
身份验证成功后,最常被问到的端点是:
POST /resources/accessTokens?userId=<your_user_id>&levelName=<level>&ttlInSecs=600
正文为空。响应中包含 token——将其传递给 Web / Mobile SDK。
完整参考文档:https://docs.sumsub.com/reference/generate-access-token。
另请参阅
- references/signing-pitfalls.md — 所有会导致
401 Invalid signature的常见问题,以及如何发现这些问题。