开放 API 文档
开放 API 供你的业务服务端调用,目前提供两项服务:短信服务(申请签名、资质与模板,发送验证码与通知短信,查询发送记录和余额)与人脸实名认证服务(基础版:姓名、证件号码与人脸照片的人证比对;增强版:H5 采集页录制动作视频,视频活体检测后再做人证比对)。所有接口都使用应用的 AppId 与 AppSecret 签名鉴权,签名方式相同。
- 接口地址
-
https://am.siyun223.com/api/open/v1 - 请求方式
- HTTPS;请求体与响应均为 JSON(UTF-8 编码),上传材料图片接口的请求体为图片原始字节
- 鉴权方式
- AppId + AppSecret,HMAC-SHA256 签名
- 调用频率
- 每个应用每分钟最多 60 次
- 调用方式
- 仅限服务端调用,不支持浏览器跨域请求
- 时间格式
- UTC 时间,ISO 8601 格式,如 2026-10-08T08:00:01.120586Z(北京时间需加 8 小时)
- 金额格式
- 字符串,单位为元,保留 4 位小数
接入指南
快速开始
-
按业务创建应用
短信服务在「短信服务 › 我的应用」中创建,人脸实名认证在「人脸实名认证服务 › 我的应用」中创建,获取 AppId 与 AppSecret。完整的 AppSecret 只在创建成功和重置后显示一次,请立即保存到服务端配置。
-
配置 IP 白名单(建议)
在应用中填写服务器的公网出口 IP,只允许你的服务器调用。
相关章节: 安全建议
-
实现签名
按「鉴权与签名」计算 X-Signature。两种业务的签名方式完全相同,可以共用同一份签名代码。
相关章节: 鉴权与签名
-
调用人脸实名认证接口
基础版应用调用 /face/basic/*:自行采集照片时调用「人脸实名认证」同步返回结果,或使用平台托管的 H5 采集页。增强版应用调用 /face/enhanced/*,只支持 H5 采集页(录制动作视频)。H5 采集页都是先创建认证会话,用户刷脸完成后平台向 notify_url 发送异步通知,也可主动查询认证会话。
相关章节: 人脸实名认证(基础版)· 人脸实名认证 人脸实名认证(基础版)· H5 采集页接入 人脸实名认证(增强版)· 接入流程
应用按业务线区分
每个应用只属于一条业务线,只能调用该业务线的接口。两种应用的签名方式完全相同。
| 业务线 | 在用户中心创建 | 可调用的接口 |
|---|---|---|
| 短信服务 | 短信服务 › 我的应用 |
/api/open/v1/sms/*、/api/open/v1/account/balance
|
| 人脸实名认证(基础版) | 人脸实名认证服务 › 我的应用(类型选择基础版) |
/api/open/v1/face/basic/*
|
| 人脸实名认证(增强版) | 人脸实名认证服务 › 我的应用(类型选择增强版) |
/api/open/v1/face/enhanced/*
|
混用会返回 HTTP 403,error_code 为 app_service_mismatch。例如用短信应用调用人脸接口时提示:「该应用不能调用本接口,请使用「人脸实名认证」下创建的应用。」
接入指南
鉴权与签名
每个请求都要在请求头中携带 AppId、时间戳、随机串与签名。签名使用 HMAC-SHA256,AppSecret 只参与本地计算,不随请求传输。短信服务与人脸实名认证的签名方式完全相同,只需使用对应业务线应用的 AppId 与 AppSecret。
公共请求头
| 请求头 | 类型 | 必填 | 说明 |
|---|---|---|---|
X-App-Id
|
string | 是 | 应用的 AppId,在应用所属业务的「我的应用」(短信服务 › 我的应用 / 人脸实名认证服务 › 我的应用)中查看,格式如 ak0123456789abcdef012345。 |
X-Timestamp
|
string | 是 | 当前 Unix 时间戳(秒,10 位数字),与服务器时间相差不能超过 300 秒。 |
X-Nonce
|
string | 是 | 16–64 位随机串,只能包含字母、数字、- 和 _;每次请求都必须重新生成,重试也不例外。 |
X-Signature
|
string | 是 | 请求签名,64 位十六进制小写,计算方法见下文。 |
Accept
|
string | 建议 | 固定为 application/json。 |
Content-Type
|
string | 有请求体时 | JSON 请求体为 application/json(UTF-8 编码);上传材料图片接口为 image/jpeg 或 image/png,请求体为图片原始字节。 |
签名步骤
- 计算请求体 SHA256:对实际发送的原始请求体字节计算 SHA256,结果为 64 位十六进制小写;GET 请求没有请求体,按空字符串计算。
- 拼接待签名串:按下表顺序拼接 6 行,行与行之间用换行符 \n(LF)连接,最后一行末尾不加换行。
- 计算签名:以 AppSecret 为密钥,对待签名串计算 HMAC-SHA256,结果转为 64 位十六进制小写,放入 X-Signature 请求头。
| 顺序 | 内容 | 规则 | 示例 |
|---|---|---|---|
| 第 1 行 | 请求方法 | 大写,如 GET、POST |
POST
|
| 第 2 行 | 请求路径 | 不含域名与查询串,必须与实际请求的路径完全一致(区分大小写) |
/api/open/v1/sms/send
|
| 第 3 行 | 规范查询串 | 没有查询参数时为空字符串,即该行为空行 | (空) |
| 第 4 行 | 时间戳 | X-Timestamp 的值 |
1760000000
|
| 第 5 行 | 随机串 | X-Nonce 的值 |
5f2b8c4e9a1d4e7f8b3c6a9d0e1f2a3b
|
| 第 6 行 | 请求体 SHA256 | 实际发送的原始请求体字节的 SHA256,十六进制小写;没有请求体时对空字符串计算 |
905ebaf8fc3cdab4a244e57d71daa5866b2849b5622af86c26b4e12b34a0e294
|
规范查询串
没有查询参数时,待签名串第 3 行为空字符串即可。上传材料图片(slot、position)与申请列表(status、page、per_page)等接口带有查询参数,需要按以下规则参与签名:
- 先对参数名和参数值做 URL 解码,再按 RFC 3986 重新编码(空格编码为 %20);
- 按参数名的字节序升序排列,参数名相同时按参数值升序排列;
- 参数名与参数值用 = 连接,参数之间用 & 连接,如
a=1&b=x%20y。
签名示例
下面是一次发送短信请求的完整计算过程,可用来校验你的签名实现:输入相同时,计算结果应完全一致。示例中的凭证只用于演示,不能调用接口。
- AppSecret
- DocsDemoSecret0123456789abcdefghijklmnopqrstuvwx
- 请求方法
- POST
- 请求路径
- /api/open/v1/sms/send
- X-Timestamp
- 1760000000
- X-Nonce
- 5f2b8c4e9a1d4e7f8b3c6a9d0e1f2a3b
- 请求体
- {"template_variant_id":12,"signature_id":2,"phone_numbers":["13800000000","13900000000"],"template_params":{"code":"123456"},"tag":"login-otp"}
- 请求体 SHA256
- 905ebaf8fc3cdab4a244e57d71daa5866b2849b5622af86c26b4e12b34a0e294
- X-Signature
- 3ad4b9307b11e9a777ab9c1f1be2ef10fe5006d95e04a94fbda5fc92f28d8ab9
待签名串(第 3 行为空行)
POST
/api/open/v1/sms/send
1760000000
5f2b8c4e9a1d4e7f8b3c6a9d0e1f2a3b
905ebaf8fc3cdab4a244e57d71daa5866b2849b5622af86c26b4e12b34a0e294
完整请求
POST /api/open/v1/sms/send HTTP/1.1
Host: am.siyun223.com
Accept: application/json
Content-Type: application/json
X-App-Id: ak0123456789abcdef012345
X-Timestamp: 1760000000
X-Nonce: 5f2b8c4e9a1d4e7f8b3c6a9d0e1f2a3b
X-Signature: 3ad4b9307b11e9a777ab9c1f1be2ef10fe5006d95e04a94fbda5fc92f28d8ab9
Idempotency-Key: order-20261008-0001
{"template_variant_id":12,"signature_id":2,"phone_numbers":["13800000000","13900000000"],"template_params":{"code":"123456"},"tag":"login-otp"}
接入指南
接口列表
下表路径均拼接在接口地址 https://am.siyun223.com/api/open/v1 之后。每组接口只能使用对应业务线的应用调用。
短信服务
| 方法 | 路径 | 接口 | 说明 |
|---|---|---|---|
| GET | /sms/templates |
查询可用短信模板 | 返回可以直接发送的模板变体 ID、签名 ID 与模板正文。 |
| POST | /sms/send |
发送短信 | 同一组模板参数发送给 1–200 个手机号,需要携带 Idempotency-Key。 |
| GET | /sms/batches/{batch_uuid} |
查询发送记录 | 查询批次与每个号码的发送状态、计费明细。 |
| GET | /account/balance |
查询余额 | 查询账户余额与短信流量包可用条数。 |
短信服务 · 资质 / 签名 / 模板申请
| 方法 | 路径 | 接口 | 说明 |
|---|---|---|---|
| POST | /sms/qualifications/files |
上传资质材料 | 请求体为图片原始字节,返回 file_token,申请资质时引用。 |
| POST | /sms/qualifications/verify-codes |
获取手机验证码 | 向经办人或责任人手机发送验证码,申请资质时填写。 |
| POST | /sms/qualifications |
申请企业实名资质 | 提交企业实名资质,进入「待平台审核」。 |
| GET | /sms/qualifications |
查询资质列表 | 本账号名下的资质(本人提交与平台分配),可按状态筛选。 |
| GET | /sms/qualifications/{id} |
查询资质详情 | 资质信息(敏感字段脱敏)、材料凭证与审核进度。 |
| PUT | /sms/qualifications/{id} |
修改企业实名资质 | 驳回或撤回后修改并重新提交。 |
| POST | /sms/qualifications/{id}/withdraw |
撤回企业实名资质申请 | 平台开始初审前撤回申请。 |
| POST | /sms/signatures/files |
上传签名材料 | 请求体为图片原始字节,返回 file_token,申请签名时引用。 |
| POST | /sms/signatures |
申请短信签名 | 使用已通过的资质申请签名,进入「待平台审核」。 |
| GET | /sms/signatures |
查询签名列表 | 本账号申请的签名列表,可按状态筛选。 |
| GET | /sms/signatures/{id} |
查询签名详情 | 签名信息、材料凭证与审核进度。 |
| PUT | /sms/signatures/{id} |
修改短信签名 | 驳回或撤回后修改并重新提交。 |
| POST | /sms/signatures/{id}/withdraw |
撤回短信签名申请 | 平台开始审核前撤回申请。 |
| POST | /sms/templates/applications |
申请短信模板 | 选择签名申请模板,进入「待平台审核」。 |
| GET | /sms/templates/applications |
查询模板申请列表 | 本账号申请的模板列表,可按状态筛选。 |
| GET | /sms/templates/applications/{id} |
查询模板申请详情 | 模板内容、审核进度;通过后返回发送用的 template_variant_id。 |
| PUT | /sms/templates/applications/{id} |
修改短信模板 | 驳回或撤回后修改并重新提交。 |
| POST | /sms/templates/applications/{id}/withdraw |
撤回短信模板申请 | 平台开始审核前撤回申请。 |
人脸实名认证(基础版)
| 方法 | 路径 | 接口 | 说明 |
|---|---|---|---|
| POST | /face/basic/verify |
人脸实名认证 | 提交姓名、证件号码与一张人脸照片,同步返回人证比对与各项检测结果。 |
| GET | /face/basic/verifications/{verify_no} |
查询认证结果 | 按平台流水号查询本应用的认证记录。 |
| GET | /face/basic/account/balance |
查询余额与次数 | 查询账户余额、基础版次数包可用次数与按次单价。 |
| POST | /face/basic/sessions |
创建认证会话 | 传入姓名、证件号码与业务单号,返回 H5 采集页链接。 |
| GET | /face/basic/sessions/{session_no} |
查询认证会话 | 按平台会话号查询 H5 会话的状态与认证结果。 |
人脸实名认证(增强版)
| 方法 | 路径 | 接口 | 说明 |
|---|---|---|---|
| POST | /face/enhanced/sessions |
创建认证会话 | 传入姓名、证件号码与业务单号,返回增强版 H5 采集页链接。 |
| GET | /face/enhanced/sessions/{session_no} |
查询认证会话 | 按平台会话号查询增强版会话的状态与认证结果。 |
| GET | /face/enhanced/verifications/{verify_no} |
查询认证结果 | 按平台流水号查询增强版的单次认证记录。 |
| GET | /face/enhanced/account/balance |
查询余额与次数 | 查询账户余额、增强版次数包可用次数与按次单价。 |
接入指南
状态码与错误码
失败时响应体均为 JSON,包含中文 message;鉴权、计费、发送、查询与路径错误另有 error_code,参数错误(HTTP 422)另有 errors 字段。请优先按 HTTP 状态码与 error_code 判断,不要依赖 message 的具体文字。
本节为两种业务通用的状态码与鉴权、访问控制错误码;短信计费、发送与资质 / 签名 / 模板申请的错误码见 短信服务错误码,人脸实名认证的业务错误码与结果码见 基础版结果码与错误码、增强版结果码与错误码。
HTTP 状态码
| 状态码 | 说明 |
|---|---|
200
|
请求成功。人脸实名认证未通过时也返回 200,是否通过以 data.passed 为准。 |
401
|
鉴权失败:缺少请求头、签名错误、时间戳过期或 Nonce 重复。 |
402
|
余额与流量包(次数包)不足,或未设置单价,本次未发送 / 未认证、未扣费。 |
403
|
应用已停用或被封禁、账户不可用、来源 IP 不在白名单内、应用不属于该接口的业务线(app_service_mismatch),或平台开启强制实名认证时账户尚未实名(kyc_required)。 |
404
|
批次或认证记录不存在(只能查询本应用的数据),或接口路径错误。响应体为 JSON。 |
405
|
接口不支持该请求方法(error_code 为 method_not_allowed)。 |
409
|
短信发送失败、结果待核实,或相同请求(相同 Idempotency-Key / biz_no)正在处理;申请类接口为当前状态不允许该操作或超过数量上限。 |
413 / 415
|
上传材料图片时图片过大,或 Content-Type 不是 image/jpeg、image/png。 |
422
|
请求参数错误,本次未发送 / 未认证、未扣费。 |
429
|
调用频率超限,或同一 IP 鉴权失败次数过多,按响应头 Retry-After 的秒数等待后重试。 |
502
|
人脸实名认证服务暂时异常或超时,本次不计费,可用同一个 biz_no 重试。 |
5xx
|
服务暂时不可用,请稍后重试;发送短信重试时保持 Idempotency-Key 不变,人脸认证重试时保持 biz_no 不变。 |
错误码:鉴权与访问控制
| error_code | HTTP 状态码 | 说明 |
|---|---|---|
auth_missing
|
401 | 缺少 X-App-Id、X-Timestamp、X-Nonce、X-Signature 请求头,或格式不正确。 |
timestamp_expired
|
401 | X-Timestamp 与服务器时间相差超过 300 秒;响应中的 server_time 为服务器当前时间。 |
invalid_signature
|
401 | AppId 不存在、应用已删除,或签名不正确。 |
nonce_reused
|
401 | X-Nonce 已经使用过;每次请求(包括重试)都要生成新的 Nonce 并重新签名。 |
app_disabled
|
403 | 应用已停用,可在应用所属业务的「我的应用」(短信服务 › 我的应用 / 人脸实名认证服务 › 我的应用)中重新启用。 |
app_blocked
|
403 | 应用已被平台封禁,请联系平台管理员。 |
account_disabled
|
403 | 账户已停用。 |
account_banned
|
403 | 账户已被封禁。 |
channel_mismatch
|
403 | 应用与接口地址不匹配:代理商平台创建的应用只能调用代理商平台的接口地址,主站创建的应用只能调用主站接口地址。 |
agent_inactive
|
403 | 代理商资格尚未开通(入驻审核中或未完成预存)或已被冻结,仅代理商平台返回。 |
ip_not_allowed
|
403 | 来源 IP 不在应用白名单内;响应中的 client_ip 为平台识别到的来源 IP。 |
app_service_mismatch
|
403 | 应用不属于该接口的业务线:短信应用只能调用短信接口,人脸应用只能调用人脸接口。如提示「该应用不能调用本接口,请使用「人脸实名认证」下创建的应用」。 |
rate_limited
|
429 | 超过每个应用每分钟 60 次的调用上限;或同一来源 IP 每分钟鉴权失败超过 60 次,该 IP 暂停调用约 1 分钟。 |
service_unavailable
|
503 | 鉴权服务暂时不可用,请稍后重试。 |
错误码:查询与通用
| error_code | HTTP 状态码 | 说明 |
|---|---|---|
batch_not_found
|
404 | 短信批次不存在,或不是本应用发出的批次(详见「短信服务错误码」)。 |
not_found
|
404 | 接口路径错误,请检查是否包含 /api/open/v1 前缀。 |
method_not_allowed
|
405 | 接口不支持该请求方法,如用 GET 调用了发送接口。 |
接入指南
安全建议
- AppSecret 只保存在服务端配置或密钥管理系统中,不要写进网页、小程序、App 安装包或代码仓库。
- 在应用中配置 IP 白名单,只允许服务器的公网出口 IP 调用。
- 怀疑 AppSecret 泄露时,立即在应用所属业务的「我的应用」(短信服务 › 我的应用 / 人脸实名认证服务 › 我的应用)中重置,旧密钥会立即失效。
- 只通过 HTTPS 调用,并保持证书校验开启。
- 不同业务或环境(如测试、生产)使用不同的应用,便于单独停用和排查。
- 不再使用的应用请及时停用或删除。
短信服务接入指南
申请流程与限制
不使用平台共享签名时,可以通过开放 API 为应用所属账号申请自己的资质、签名和模板。申请记录与网页端「资质管理 / 签名管理 / 模板管理」是同一份数据,提交后进入相同的审核流程:待平台审核 → 平台审核通过后提交短信通道审核 → 同步审核结果。审核结果没有回调,请按需调用查询接口获取。
-
提交资质
上传营业证件、身份证件与授权委托书照片,获取经办人手机验证码后提交资质。资质归属应用所属账号,与网页端「资质管理」是同一份数据。
-
等待资质审核
平台初审通过后提交短信通道审核,按资质详情的 status 查询进度;驳回时 rejection_reason 给出原因,可修改后重新提交。
-
申请签名
资质 usable_for_signature 为 true 后,用它的 id 提交签名申请;平台审核通过后提交短信通道审核。
-
申请模板
选择本人已通过的签名或平台共享签名提交模板;审核通过后详情的 send_options 给出发送短信时使用的 template_variant_id 与 signature_id。
-
发送短信
模板通过后也会出现在「查询可用短信模板」中,按「发送短信」接口发送即可。
调用限制
| 项目 | 限制 |
|---|---|
| 调用频率 | 每个应用每分钟最多 20 次,与发送接口分开计数 |
| 图片上传 | 每个应用每小时最多 60 张,单张不超过 10MB |
| 手机验证码 | 每个应用每天最多 20 次;同一账号每天 10 次、同一手机号每天 5 次,同一手机号 60 秒内只能获取一次 |
| 资质 | 同时审核中最多 5 个,每天最多提交 3 次(含修改后重新提交) |
| 签名 | 每个账号最多 5 个,每天最多提交 10 次(含修改后重新提交) |
| 模板 | 每个账号最多 20 个,每天最多提交 10 次(含修改后重新提交) |
通用约定
- 鉴权方式与发送接口相同,只能使用「短信服务 › 我的应用」中创建的应用调用。
- 申请与修改接口的请求体为 JSON,只能包含文档列出的字段,出现其他字段时返回 HTTP 422(errors._unknown_fields);上传材料接口的请求体为图片原始字节。
- 申请成功返回 HTTP 201,修改、撤回成功返回 HTTP 200,data 均为该记录的详情。
- 只有 editable 为 true(驳回或已撤回)时可以修改重新提交;只有 withdrawable 为 true(待平台审核)时可以撤回。
- 证件号码、手机号、统一社会信用代码在响应中脱敏,证件照片不通过 API 返回。
- 状态值一律同时返回 status(英文取值)与 status_label(中文名称),请按 status 判断。
- 列表接口的 status、page、per_page 为查询参数,参与签名,见「鉴权与签名 › 规范查询串」。
列表接口的查询参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
status
|
string | 否 | 按状态筛选,取值见各接口的「列表筛选」;不传或取值无效时返回全部。 |
page
|
integer | 否 | 页码,从 1 开始,默认 1。 |
per_page
|
integer | 否 | 每页条数,默认 20,最大 50。 |
短信服务接入指南
计费说明
- 计费字数 = 「【签名】」+ 替换变量后的正文,中英文、数字、标点和空格都按 1 个字计算。
- 每 60 字计 1 条,不足 60 字按 1 条计算;本次计费条数 = 每个号码的条数 × 号码数。
- 扣费顺序:先扣生效中的流量包(先到期的先用),不足部分按通道单价从余额扣除。余额与流量包合计不足时返回 HTTP 402,不发送、不扣费。
- 通道明确未受理的号码(整批失败或部分号码被拒),对应费用自动退还:先退余额,再退流量包。
- 通道响应异常、无法确定是否已发出时,扣费暂不退还,计费状态为「待核实」,由平台核实后处理。
- 申请资质、签名与模板不收费。
计费示例
以签名「云信短信」为例:
| 场景 | 计费文本 | 字数 | 每个号码计费 |
|---|---|---|---|
| 验证码 | 【云信短信】您的验证码为123456,5分钟内有效,请勿泄露给他人。 | 34 字 | 1 条 |
| 订单通知 | 【云信短信】尊敬的会员,您的订单20261008001已发货,快递单号SF1234567890,预计2天内送达。如有疑问请联系在线客服,感谢您的支持! | 75 字 | 2 条 |
短信服务接入指南
幂等与重试
发送短信必须携带 Idempotency-Key 请求头,用来保证同一笔业务只发送一次。建议直接使用业务单号,如 order-20261008-0001。
- Idempotency-Key 按应用隔离:同一个应用内,相同的 Key 只会发送一次,且长期有效,请勿复用。
- 相同 Key、相同请求内容:直接返回首次请求的结果,不会重复发送或扣费;即使首次发送后模板或签名已下线,也照常返回首次结果。
- 比较请求内容时,手机号按 11 位号码比较,13800000000 与 +8613800000000 视为相同。
- 相同 Key、不同请求内容(号码、模板、签名、参数或 tag 不同):返回 HTTP 422「Idempotency-Key 已用于其他短信请求。」
- 首次请求仍在处理中:返回 HTTP 409,error_code 为 request_in_progress;超过 15 分钟仍未完成时,error_code 为 result_unknown,结果待核实。
- 首次请求返回 HTTP 402 或 422 时没有生成批次,修正后可以继续使用同一个 Key。
重试建议
- 网络超时或 HTTP 5xx:使用相同的 Idempotency-Key 重试。
- 每次重试都要重新生成 X-Timestamp 与 X-Nonce,并重新计算签名。
- HTTP 409 且响应带有 batch_uuid:先调用「查询发送记录」确认,不要换新的 Key 重发,以免用户收到重复短信。
- HTTP 429:按响应头 Retry-After 的秒数等待后再重试。
- 签名、资质、模板的申请接口没有 Idempotency-Key:超时后请先调用对应的查询接口确认是否已提交,再决定是否重新提交。
短信服务接入指南
示例代码(发送短信)
每份示例都实现了同一套签名,并依次演示查询余额、查询可用短信模板、发送短信与查询发送记录。把「你的 AppId」「你的 AppSecret」替换为「短信服务 › 我的应用」中的真实值,并把模板与签名 ID 换成「查询可用短信模板」返回的值即可运行。
PHP
<?php
// 短信开放 API 调用示例(PHP 7.4 及以上,需要 curl 扩展)
const APP_ID = '你的 AppId';
const APP_SECRET = '你的 AppSecret'; // 只保存在服务端,切勿写进网页、小程序或 App
const API_ORIGIN = 'https://am.siyun223.com';
const API_PREFIX = '/api/open/v1';
/**
* 签名并发送请求,返回 [HTTP 状态码, 响应数组]
*/
function openApiRequest(string $method, string $path, ?array $payload = null, array $extraHeaders = []): array
{
$method = strtoupper($method);
$fullPath = API_PREFIX . $path;
$body = $payload === null ? '' : json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$timestamp = (string) time();
$nonce = bin2hex(random_bytes(16));
// 待签名串:方法、路径、规范查询串(无查询参数时为空)、时间戳、随机串、请求体 SHA256,用 \n 连接
$stringToSign = implode("\n", [$method, $fullPath, '', $timestamp, $nonce, hash('sha256', $body)]);
$signature = hash_hmac('sha256', $stringToSign, APP_SECRET);
$headers = array_merge([
'Accept: application/json',
'X-App-Id: ' . APP_ID,
'X-Timestamp: ' . $timestamp,
'X-Nonce: ' . $nonce,
'X-Signature: ' . $signature,
], $extraHeaders);
$ch = curl_init(API_ORIGIN . $fullPath);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
if ($payload !== null) {
$headers[] = 'Content-Type: application/json';
curl_setopt($ch, CURLOPT_POSTFIELDS, $body); // 发送的必须是参与签名的同一个字符串
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$response = curl_exec($ch);
if ($response === false) {
throw new RuntimeException('请求失败:' . curl_error($ch));
}
return [(int) curl_getinfo($ch, CURLINFO_HTTP_CODE), json_decode($response, true)];
}
// 1. 查询余额
[$status, $result] = openApiRequest('GET', '/account/balance');
echo $status, ' ', json_encode($result, JSON_UNESCAPED_UNICODE), PHP_EOL;
// 2. 获取可用模板:发送时使用 items[].variant.id 与 items[].signature.id
[$status, $result] = openApiRequest('GET', '/sms/templates');
echo $status, ' ', json_encode($result, JSON_UNESCAPED_UNICODE), PHP_EOL;
// 3. 发送短信:Idempotency-Key 建议使用业务单号,网络异常重试时保持不变
[$status, $result] = openApiRequest('POST', '/sms/send', [
'template_variant_id' => 12,
'signature_id' => 2,
'phone_numbers' => ['13800000000'],
'template_params' => ['code' => '123456'],
], ['Idempotency-Key: order-20261008-0001']);
echo $status, ' ', json_encode($result, JSON_UNESCAPED_UNICODE), PHP_EOL;
// 4. 查询发送结果
if ($status === 200 && !empty($result['data']['batch_uuid'])) {
[$status, $result] = openApiRequest('GET', '/sms/batches/' . $result['data']['batch_uuid']);
echo $status, ' ', json_encode($result, JSON_UNESCAPED_UNICODE), PHP_EOL;
}
Python
# 短信开放 API 调用示例(Python 3.6 及以上,只使用标准库)
import hashlib
import hmac
import json
import secrets
import time
import urllib.error
import urllib.request
APP_ID = "你的 AppId"
APP_SECRET = "你的 AppSecret" # 只保存在服务端,切勿写进网页、小程序或 App
API_ORIGIN = "https://am.siyun223.com"
API_PREFIX = "/api/open/v1"
def open_api_request(method, path, payload=None, headers=None):
"""签名并发送请求,返回 (HTTP 状态码, 响应内容)"""
method = method.upper()
full_path = API_PREFIX + path
body = b"" if payload is None else json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
timestamp = str(int(time.time()))
nonce = secrets.token_hex(16)
# 待签名串:方法、路径、规范查询串(无查询参数时为空)、时间戳、随机串、请求体 SHA256,用 \n 连接
string_to_sign = "\n".join([method, full_path, "", timestamp, nonce, hashlib.sha256(body).hexdigest()])
signature = hmac.new(APP_SECRET.encode("utf-8"), string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
request_headers = {
"Accept": "application/json",
"X-App-Id": APP_ID,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature": signature,
}
if payload is not None:
request_headers["Content-Type"] = "application/json"
request_headers.update(headers or {})
request = urllib.request.Request(
API_ORIGIN + full_path,
data=body if payload is not None else None, # 发送的必须是参与签名的同一份字节
headers=request_headers,
method=method,
)
try:
with urllib.request.urlopen(request, timeout=15) as response:
return response.status, _parse(response.read())
except urllib.error.HTTPError as error:
return error.code, _parse(error.read())
def _parse(raw):
try:
return json.loads(raw.decode("utf-8"))
except ValueError:
return raw.decode("utf-8", "replace")
if __name__ == "__main__":
# 1. 查询余额
print(open_api_request("GET", "/account/balance"))
# 2. 获取可用模板:发送时使用 items[].variant.id 与 items[].signature.id
print(open_api_request("GET", "/sms/templates"))
# 3. 发送短信:Idempotency-Key 建议使用业务单号,网络异常重试时保持不变
status, result = open_api_request("POST", "/sms/send", {
"template_variant_id": 12,
"signature_id": 2,
"phone_numbers": ["13800000000"],
"template_params": {"code": "123456"},
}, {"Idempotency-Key": "order-20261008-0001"})
print(status, result)
# 4. 查询发送结果
if status == 200 and isinstance(result, dict) and result.get("data", {}).get("batch_uuid"):
print(open_api_request("GET", "/sms/batches/" + result["data"]["batch_uuid"]))
Java
// 短信开放 API 调用示例(Java 11 及以上,只使用 JDK 标准库)
// 保存为 OpenApiDemo.java,执行:javac -encoding UTF-8 OpenApiDemo.java && java OpenApiDemo
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.time.Duration;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class OpenApiDemo {
private static final String APP_ID = "你的 AppId";
private static final String APP_SECRET = "你的 AppSecret"; // 只保存在服务端,切勿写进网页、小程序或 App
private static final String API_ORIGIN = "https://am.siyun223.com";
private static final String API_PREFIX = "/api/open/v1";
private static final HttpClient CLIENT = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
private static final SecureRandom RANDOM = new SecureRandom();
public static void main(String[] args) throws Exception {
// 1. 查询余额
HttpResponse<String> balance = request("GET", "/account/balance", null, null);
System.out.println(balance.statusCode() + " " + balance.body());
// 2. 获取可用模板:发送时使用 items[].variant.id 与 items[].signature.id
HttpResponse<String> templates = request("GET", "/sms/templates", null, null);
System.out.println(templates.statusCode() + " " + templates.body());
// 3. 发送短信:请求体可以用 Jackson、Gson 等生成,签名与发送必须使用同一个字符串
String body = "{\"template_variant_id\":12,\"signature_id\":2,"
+ "\"phone_numbers\":[\"13800000000\"],\"template_params\":{\"code\":\"123456\"}}";
HttpResponse<String> sent = request("POST", "/sms/send", body, "order-20261008-0001");
System.out.println(sent.statusCode() + " " + sent.body());
// 4. 查询发送结果(示例用正则提取 batch_uuid,生产代码请用 JSON 库解析)
Matcher matcher = Pattern.compile("\"batch_uuid\":\"([0-9a-fA-F-]{36})\"").matcher(sent.body());
if (sent.statusCode() == 200 && matcher.find()) {
HttpResponse<String> batch = request("GET", "/sms/batches/" + matcher.group(1), null, null);
System.out.println(batch.statusCode() + " " + batch.body());
}
}
/** 签名并发送请求 */
static HttpResponse<String> request(String method, String path, String body, String idempotencyKey) throws Exception {
String fullPath = API_PREFIX + path;
String payload = body == null ? "" : body;
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
String nonce = randomHex(16);
// 待签名串:方法、路径、规范查询串(无查询参数时为空)、时间戳、随机串、请求体 SHA256,用 \n 连接
String stringToSign = String.join("\n", method, fullPath, "", timestamp, nonce, sha256Hex(payload));
HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(API_ORIGIN + fullPath))
.timeout(Duration.ofSeconds(15))
.header("Accept", "application/json")
.header("X-App-Id", APP_ID)
.header("X-Timestamp", timestamp)
.header("X-Nonce", nonce)
.header("X-Signature", hmacSha256Hex(APP_SECRET, stringToSign));
if (idempotencyKey != null) {
builder.header("Idempotency-Key", idempotencyKey);
}
if (body == null) {
builder.method(method, HttpRequest.BodyPublishers.noBody());
} else {
builder.header("Content-Type", "application/json")
.method(method, HttpRequest.BodyPublishers.ofString(payload, StandardCharsets.UTF_8));
}
return CLIENT.send(builder.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
}
static String sha256Hex(String value) throws Exception {
return toHex(MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8)));
}
static String hmacSha256Hex(String secret, String value) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
return toHex(mac.doFinal(value.getBytes(StandardCharsets.UTF_8)));
}
static String randomHex(int bytes) {
byte[] buffer = new byte[bytes];
RANDOM.nextBytes(buffer);
return toHex(buffer);
}
static String toHex(byte[] bytes) {
StringBuilder hex = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
hex.append(String.format("%02x", b));
}
return hex.toString();
}
}
Node.js
// 短信开放 API 调用示例(Node.js 18 及以上,使用内置 fetch 与 crypto,无需安装依赖)
// 保存为 open-api-demo.mjs,执行:node open-api-demo.mjs
import { createHash, createHmac, randomBytes } from 'node:crypto';
const APP_ID = '你的 AppId';
const APP_SECRET = '你的 AppSecret'; // 只保存在服务端,切勿写进网页、小程序或 App
const API_ORIGIN = 'https://am.siyun223.com';
const API_PREFIX = '/api/open/v1';
/** 签名并发送请求,返回 { status, data } */
async function openApiRequest(method, path, payload = null, extraHeaders = {}) {
const fullPath = API_PREFIX + path;
const body = payload === null ? '' : JSON.stringify(payload);
const timestamp = String(Math.floor(Date.now() / 1000));
const nonce = randomBytes(16).toString('hex');
// 待签名串:方法、路径、规范查询串(无查询参数时为空)、时间戳、随机串、请求体 SHA256,用 \n 连接
const bodyHash = createHash('sha256').update(body, 'utf8').digest('hex');
const stringToSign = [method, fullPath, '', timestamp, nonce, bodyHash].join('\n');
const signature = createHmac('sha256', APP_SECRET).update(stringToSign, 'utf8').digest('hex');
const headers = {
Accept: 'application/json',
'X-App-Id': APP_ID,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': signature,
...extraHeaders,
};
if (payload !== null) {
headers['Content-Type'] = 'application/json';
}
const response = await fetch(API_ORIGIN + fullPath, {
method,
headers,
body: payload === null ? undefined : body, // 发送的必须是参与签名的同一个字符串
signal: AbortSignal.timeout(15000),
});
const text = await response.text();
let data = text;
try {
data = JSON.parse(text);
} catch {
// 非 JSON 响应原样返回
}
return { status: response.status, data };
}
// 1. 查询余额
console.log(JSON.stringify(await openApiRequest('GET', '/account/balance')));
// 2. 获取可用模板:发送时使用 items[].variant.id 与 items[].signature.id
console.log(JSON.stringify(await openApiRequest('GET', '/sms/templates')));
// 3. 发送短信:Idempotency-Key 建议使用业务单号,网络异常重试时保持不变
const sent = await openApiRequest('POST', '/sms/send', {
template_variant_id: 12,
signature_id: 2,
phone_numbers: ['13800000000'],
template_params: { code: '123456' },
}, { 'Idempotency-Key': 'order-20261008-0001' });
console.log(JSON.stringify(sent));
// 4. 查询发送结果
if (sent.status === 200 && sent.data?.data?.batch_uuid) {
console.log(JSON.stringify(await openApiRequest('GET', `/sms/batches/${sent.data.data.batch_uuid}`)));
}
Shell
#!/usr/bin/env bash
# 短信开放 API 调用示例(bash、curl 与 openssl)
# 保存为 open-api-demo.sh,执行:bash open-api-demo.sh
set -euo pipefail
APP_ID='你的 AppId'
APP_SECRET='你的 AppSecret' # 只保存在服务端,切勿写进网页、小程序或 App
API_ORIGIN='https://am.siyun223.com'
API_PREFIX='/api/open/v1'
# 用法:open_api_request 方法 路径 [JSON 请求体] [Idempotency-Key]
open_api_request() {
local method="$1" path="${API_PREFIX}$2" body="${3:-}" idempotency_key="${4:-}"
local timestamp nonce body_hash signature
timestamp=$(date +%s)
nonce=$(openssl rand -hex 16)
body_hash=$(printf '%s' "$body" | openssl dgst -sha256 -hex | awk '{print $NF}')
# 待签名串:方法、路径、规范查询串(无查询参数时为空行)、时间戳、随机串、请求体 SHA256
signature=$(printf '%s\n%s\n\n%s\n%s\n%s' "$method" "$path" "$timestamp" "$nonce" "$body_hash" \
| openssl dgst -sha256 -hmac "$APP_SECRET" -hex | awk '{print $NF}')
local args=(-sS -X "$method" "${API_ORIGIN}${path}"
-H 'Accept: application/json'
-H "X-App-Id: ${APP_ID}"
-H "X-Timestamp: ${timestamp}"
-H "X-Nonce: ${nonce}"
-H "X-Signature: ${signature}"
-w '\nHTTP %{http_code}\n')
if [ -n "$body" ]; then
args+=(-H 'Content-Type: application/json' --data-binary "$body")
fi
if [ -n "$idempotency_key" ]; then
args+=(-H "Idempotency-Key: ${idempotency_key}")
fi
curl "${args[@]}"
}
# 1. 查询余额
open_api_request GET /account/balance
# 2. 获取可用模板:发送时使用 items[].variant.id 与 items[].signature.id
open_api_request GET /sms/templates
# 3. 发送短信:Idempotency-Key 建议使用业务单号,网络异常重试时保持不变
open_api_request POST /sms/send \
'{"template_variant_id":12,"signature_id":2,"phone_numbers":["13800000000"],"template_params":{"code":"123456"}}' \
'order-20261008-0001'
# 4. 查询发送结果:把发送接口返回的 data.batch_uuid 填到路径末尾
# open_api_request GET /sms/batches/替换为batch_uuid
短信服务接入指南
示例代码(资质 / 签名 / 模板申请)
签名实现与发送短信的示例代码相同,另外演示了带查询参数的签名与图片原始字节上传:依次上传签名材料、申请短信签名、申请短信模板并查询模板审核结果。企业实名资质的上传与提交方式相同,只是接口路径与字段不同。把「你的 AppId」「你的 AppSecret」、qualification_id 与 signature_id 换成实际值,并在同一目录放一张 proof.jpg 即可运行。
PHP
<?php
// 短信签名 / 模板申请示例(PHP 7.4 及以上,需要 curl 扩展)
const APP_ID = '你的 AppId';
const APP_SECRET = '你的 AppSecret'; // 只保存在服务端,切勿写进网页、小程序或 App
const API_ORIGIN = 'https://am.siyun223.com';
const API_PREFIX = '/api/open/v1';
/** 规范查询串:按参数名字节序升序,RFC 3986 编码 */
function canonicalQuery(array $query): string
{
ksort($query, SORT_STRING);
$pairs = [];
foreach ($query as $key => $value) {
$pairs[] = rawurlencode((string) $key) . '=' . rawurlencode((string) $value);
}
return implode('&', $pairs);
}
/**
* 签名并发送请求,$body 为原始请求体字节(JSON 字符串或图片字节),返回 [HTTP 状态码, 响应数组]
*/
function openApiRequest(string $method, string $path, array $query = [], string $body = '', string $contentType = 'application/json'): array
{
$method = strtoupper($method);
$fullPath = API_PREFIX . $path;
$queryString = canonicalQuery($query);
$timestamp = (string) time();
$nonce = bin2hex(random_bytes(16));
$stringToSign = implode("\n", [$method, $fullPath, $queryString, $timestamp, $nonce, hash('sha256', $body)]);
$headers = [
'Accept: application/json',
'X-App-Id: ' . APP_ID,
'X-Timestamp: ' . $timestamp,
'X-Nonce: ' . $nonce,
'X-Signature: ' . hash_hmac('sha256', $stringToSign, APP_SECRET),
];
$ch = curl_init(API_ORIGIN . $fullPath . ($queryString === '' ? '' : '?' . $queryString));
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
if ($body !== '') {
$headers[] = 'Content-Type: ' . $contentType;
curl_setopt($ch, CURLOPT_POSTFIELDS, $body); // 发送的必须是参与签名的同一份字节
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$response = curl_exec($ch);
if ($response === false) {
throw new RuntimeException('请求失败:' . curl_error($ch));
}
return [(int) curl_getinfo($ch, CURLINFO_HTTP_CODE), json_decode($response, true)];
}
function json(array $data): string
{
return json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
// 1. 上传签名证明材料:请求体为图片原始字节,slot / position 为查询参数并参与签名
[$status, $result] = openApiRequest('POST', '/sms/signatures/files', ['slot' => 'other', 'position' => 0], file_get_contents(__DIR__ . '/proof.jpg'), 'image/jpeg');
echo $status, ' ', json($result), PHP_EOL;
$fileToken = $result['data']['file_token'] ?? '';
// 2. 提交签名申请:qualification_id 取「资质列表」中 usable_for_signature 为 true 的资质
[$status, $result] = openApiRequest('POST', '/sms/signatures', [], json([
'qualification_id' => 8,
'content' => '示例科技',
'sign_source' => 1,
'scene' => '用于向注册用户发送登录验证码与订单通知',
'files' => ['other' => [$fileToken]],
]));
echo $status, ' ', json($result), PHP_EOL;
// 3. 提交模板申请:signature_id 可用本人已通过的签名或平台共享签名
[$status, $result] = openApiRequest('POST', '/sms/templates/applications', [], json([
'name' => '登录验证码',
'channel_type' => 'CN_OTP',
'signature_id' => 23,
'content' => '您的验证码为${code},5分钟内有效,请勿泄露给他人。',
]));
echo $status, ' ', json($result), PHP_EOL;
// 4. 查询模板审核结果:status 为 approved 后,send_options 给出发送用的 template_variant_id 与 signature_id
if ($status === 201) {
[$status, $result] = openApiRequest('GET', '/sms/templates/applications/' . $result['data']['id']);
echo $status, ' ', json($result), PHP_EOL;
}
Python
# 短信签名 / 模板申请示例(Python 3.6 及以上,只使用标准库)
import hashlib
import hmac
import json
import secrets
import time
import urllib.error
import urllib.parse
import urllib.request
APP_ID = "你的 AppId"
APP_SECRET = "你的 AppSecret" # 只保存在服务端,切勿写进网页、小程序或 App
API_ORIGIN = "https://am.siyun223.com"
API_PREFIX = "/api/open/v1"
def canonical_query(query):
"""规范查询串:按参数名字节序升序,RFC 3986 编码"""
pairs = sorted((str(k), str(v)) for k, v in (query or {}).items())
return "&".join(urllib.parse.quote(k, safe="-_.~") + "=" + urllib.parse.quote(v, safe="-_.~") for k, v in pairs)
def open_api_request(method, path, query=None, body=b"", content_type="application/json"):
"""签名并发送请求,body 为原始请求体字节,返回 (HTTP 状态码, 响应内容)"""
method = method.upper()
full_path = API_PREFIX + path
query_string = canonical_query(query)
timestamp = str(int(time.time()))
nonce = secrets.token_hex(16)
string_to_sign = "\n".join([method, full_path, query_string, timestamp, nonce, hashlib.sha256(body).hexdigest()])
headers = {
"Accept": "application/json",
"X-App-Id": APP_ID,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature": hmac.new(APP_SECRET.encode(), string_to_sign.encode(), hashlib.sha256).hexdigest(),
}
if body:
headers["Content-Type"] = content_type
url = API_ORIGIN + full_path + ("?" + query_string if query_string else "")
request = urllib.request.Request(url, data=body or None, headers=headers, method=method)
try:
with urllib.request.urlopen(request, timeout=30) as response:
return response.status, json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as error:
return error.code, json.loads(error.read().decode("utf-8") or "{}")
def to_json(data):
return json.dumps(data, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
if __name__ == "__main__":
# 1. 上传签名证明材料:请求体为图片原始字节,slot / position 为查询参数并参与签名
with open("proof.jpg", "rb") as image:
status, result = open_api_request("POST", "/sms/signatures/files", {"slot": "other", "position": 0}, image.read(), "image/jpeg")
print(status, result)
file_token = (result.get("data") or {}).get("file_token", "")
# 2. 提交签名申请:qualification_id 取「资质列表」中 usable_for_signature 为 true 的资质
print(open_api_request("POST", "/sms/signatures", body=to_json({
"qualification_id": 8,
"content": "示例科技",
"sign_source": 1,
"scene": "用于向注册用户发送登录验证码与订单通知",
"files": {"other": [file_token]},
})))
# 3. 提交模板申请:signature_id 可用本人已通过的签名或平台共享签名
status, result = open_api_request("POST", "/sms/templates/applications", body=to_json({
"name": "登录验证码",
"channel_type": "CN_OTP",
"signature_id": 23,
"content": "您的验证码为${code},5分钟内有效,请勿泄露给他人。",
}))
print(status, result)
# 4. 查询模板审核结果:status 为 approved 后,send_options 给出发送用的 template_variant_id 与 signature_id
if status == 201:
print(open_api_request("GET", "/sms/templates/applications/%d" % result["data"]["id"]))
Java
// 短信签名 / 模板申请示例(Java 11 及以上,只使用 JDK 标准库)
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.time.Duration;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class SmsApplyDemo {
static final String APP_ID = "你的 AppId";
static final String APP_SECRET = "你的 AppSecret"; // 只保存在服务端,切勿写进网页、小程序或 App
static final String API_ORIGIN = "https://am.siyun223.com";
static final String API_PREFIX = "/api/open/v1";
static final HttpClient CLIENT = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
/** RFC 3986 编码:URLEncoder 把空格编码为 +,这里换成 %20,并保留 ~ */
static String rfc3986(String value) {
return URLEncoder.encode(value, StandardCharsets.UTF_8).replace("+", "%20").replace("%7E", "~").replace("*", "%2A");
}
/** 规范查询串:TreeMap 按参数名升序(参数名为 ASCII 时与字节序一致) */
static String canonicalQuery(Map<String, String> query) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : new TreeMap<>(query).entrySet()) {
if (sb.length() > 0) sb.append('&');
sb.append(rfc3986(entry.getKey())).append('=').append(rfc3986(entry.getValue()));
}
return sb.toString();
}
/** 签名并发送请求,body 为原始请求体字节,返回 "HTTP 状态码 响应体" */
static String request(String method, String path, Map<String, String> query, byte[] body, String contentType) throws Exception {
String fullPath = API_PREFIX + path;
String queryString = canonicalQuery(query);
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
byte[] nonceBytes = new byte[16];
new SecureRandom().nextBytes(nonceBytes);
String nonce = hex(nonceBytes);
String bodyHash = hex(MessageDigest.getInstance("SHA-256").digest(body));
String stringToSign = String.join("\n", method, fullPath, queryString, timestamp, nonce, bodyHash);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(APP_SECRET.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
String signature = hex(mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8)));
HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(API_ORIGIN + fullPath + (queryString.isEmpty() ? "" : "?" + queryString)))
.timeout(Duration.ofSeconds(30))
.header("Accept", "application/json")
.header("X-App-Id", APP_ID)
.header("X-Timestamp", timestamp)
.header("X-Nonce", nonce)
.header("X-Signature", signature);
if (body.length > 0) {
builder.header("Content-Type", contentType).method(method, HttpRequest.BodyPublishers.ofByteArray(body));
} else {
builder.method(method, HttpRequest.BodyPublishers.noBody());
}
HttpResponse<String> response = CLIENT.send(builder.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
return response.statusCode() + " " + response.body();
}
static String hex(byte[] bytes) {
StringBuilder hex = new StringBuilder();
for (byte b : bytes) {
hex.append(String.format("%02x", b));
}
return hex.toString();
}
static byte[] json(String text) {
return text.getBytes(StandardCharsets.UTF_8);
}
static String extract(String response, String field) {
Matcher matcher = Pattern.compile("\"" + field + "\":\"?([A-Za-z0-9]+)").matcher(response);
return matcher.find() ? matcher.group(1) : "";
}
public static void main(String[] args) throws Exception {
// 1. 上传签名证明材料:请求体为图片原始字节,slot / position 为查询参数并参与签名
String upload = request("POST", "/sms/signatures/files", Map.of("slot", "other", "position", "0"),
Files.readAllBytes(Path.of("proof.jpg")), "image/jpeg");
System.out.println(upload);
String fileToken = extract(upload, "file_token");
// 2. 提交签名申请:qualification_id 取「资质列表」中 usable_for_signature 为 true 的资质
System.out.println(request("POST", "/sms/signatures", Map.of(), json(
"{\"qualification_id\":8,\"content\":\"示例科技\",\"sign_source\":1,"
+ "\"scene\":\"用于向注册用户发送登录验证码与订单通知\",\"files\":{\"other\":[\"" + fileToken + "\"]}}"), "application/json"));
// 3. 提交模板申请:signature_id 可用本人已通过的签名或平台共享签名
String template = request("POST", "/sms/templates/applications", Map.of(), json(
"{\"name\":\"登录验证码\",\"channel_type\":\"CN_OTP\",\"signature_id\":23,"
+ "\"content\":\"您的验证码为${code},5分钟内有效,请勿泄露给他人。\"}"), "application/json");
System.out.println(template);
// 4. 查询模板审核结果:status 为 approved 后,send_options 给出发送用的 template_variant_id 与 signature_id
if (template.startsWith("201")) {
System.out.println(request("GET", "/sms/templates/applications/" + extract(template, "id"), Map.of(), new byte[0], ""));
}
}
}
Node.js
// 短信签名 / 模板申请示例(Node.js 18 及以上,无需安装依赖)
import { createHash, createHmac, randomBytes } from 'node:crypto';
import { readFileSync } from 'node:fs';
const APP_ID = '你的 AppId';
const APP_SECRET = '你的 AppSecret'; // 只保存在服务端,切勿写进网页、小程序或 App
const API_ORIGIN = 'https://am.siyun223.com';
const API_PREFIX = '/api/open/v1';
// RFC 3986 编码:encodeURIComponent 不编码 !'()*,需要补上
const rfc3986 = (value) => encodeURIComponent(String(value)).replace(/[!'()*]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase());
/** 规范查询串:按参数名字节序升序 */
function canonicalQuery(query = {}) {
return Object.keys(query)
.sort((a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)))
.map((key) => `${rfc3986(key)}=${rfc3986(query[key])}`)
.join('&');
}
/** 签名并发送请求,body 为 Buffer(JSON 或图片字节),返回 [HTTP 状态码, 响应内容] */
async function openApiRequest(method, path, { query = {}, body = Buffer.alloc(0), contentType = 'application/json' } = {}) {
method = method.toUpperCase();
const fullPath = API_PREFIX + path;
const queryString = canonicalQuery(query);
const timestamp = String(Math.floor(Date.now() / 1000));
const nonce = randomBytes(16).toString('hex');
const stringToSign = [method, fullPath, queryString, timestamp, nonce, createHash('sha256').update(body).digest('hex')].join('\n');
const headers = {
Accept: 'application/json',
'X-App-Id': APP_ID,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': createHmac('sha256', APP_SECRET).update(stringToSign).digest('hex'),
};
if (body.length > 0) headers['Content-Type'] = contentType;
const url = API_ORIGIN + fullPath + (queryString ? `?${queryString}` : '');
const response = await fetch(url, { method, headers, body: body.length > 0 ? body : undefined });
return [response.status, await response.json()];
}
const json = (data) => Buffer.from(JSON.stringify(data), 'utf8');
// 1. 上传签名证明材料:请求体为图片原始字节,slot / position 为查询参数并参与签名
let [status, result] = await openApiRequest('POST', '/sms/signatures/files', {
query: { slot: 'other', position: 0 },
body: readFileSync('proof.jpg'),
contentType: 'image/jpeg',
});
console.log(status, result);
const fileToken = result?.data?.file_token ?? '';
// 2. 提交签名申请:qualification_id 取「资质列表」中 usable_for_signature 为 true 的资质
console.log(await openApiRequest('POST', '/sms/signatures', { body: json({
qualification_id: 8,
content: '示例科技',
sign_source: 1,
scene: '用于向注册用户发送登录验证码与订单通知',
files: { other: [fileToken] },
}) }));
// 3. 提交模板申请:signature_id 可用本人已通过的签名或平台共享签名
[status, result] = await openApiRequest('POST', '/sms/templates/applications', { body: json({
name: '登录验证码',
channel_type: 'CN_OTP',
signature_id: 23,
content: '您的验证码为${code},5分钟内有效,请勿泄露给他人。',
}) });
console.log(status, result);
// 4. 查询模板审核结果:status 为 approved 后,send_options 给出发送用的 template_variant_id 与 signature_id
if (status === 201) {
console.log(await openApiRequest('GET', `/sms/templates/applications/${result.data.id}`));
}
Shell
#!/usr/bin/env bash
# 短信签名 / 模板申请示例(bash、curl、openssl)
set -euo pipefail
APP_ID='你的 AppId'
APP_SECRET='你的 AppSecret' # 只保存在服务端,切勿写进网页、小程序或 App
API_ORIGIN='https://am.siyun223.com'
API_PREFIX='/api/open/v1'
# 签名并发送请求:open_api_request 方法 路径 规范查询串 请求体文件 Content-Type
# 规范查询串需按参数名升序、RFC 3986 编码后传入;请求体放在文件中,按原始字节计算 SHA256
open_api_request() {
local method="$1" path="$2" query="${3:-}" body_file="${4:-}" content_type="${5:-application/json}"
local full_path="${API_PREFIX}${path}"
local timestamp nonce body_hash signature url
timestamp="$(date +%s)"
nonce="$(openssl rand -hex 16)"
if [ -n "$body_file" ]; then
body_hash="$(openssl dgst -sha256 -hex < "$body_file" | sed 's/^.* //')"
else
body_hash="$(printf '' | openssl dgst -sha256 -hex | sed 's/^.* //')"
fi
signature="$(printf '%s\n%s\n%s\n%s\n%s\n%s' "$method" "$full_path" "$query" "$timestamp" "$nonce" "$body_hash" \
| openssl dgst -sha256 -hmac "$APP_SECRET" -hex | sed 's/^.* //')"
url="${API_ORIGIN}${full_path}${query:+?$query}"
local args=(-sS -X "$method" "$url"
-H 'Accept: application/json'
-H "X-App-Id: ${APP_ID}" -H "X-Timestamp: ${timestamp}" -H "X-Nonce: ${nonce}" -H "X-Signature: ${signature}")
if [ -n "$body_file" ]; then
args+=(-H "Content-Type: ${content_type}" --data-binary "@${body_file}")
fi
curl "${args[@]}"
echo
}
# 1. 上传签名证明材料:请求体为图片原始字节,slot / position 为查询参数并参与签名
open_api_request POST /sms/signatures/files 'position=0&slot=other' proof.jpg image/jpeg
# 2. 提交签名申请:把上一步返回的 file_token 填到 files.other 中
cat > signature.json <<'JSON'
{"qualification_id":8,"content":"示例科技","sign_source":1,"scene":"用于向注册用户发送登录验证码与订单通知","files":{"other":["上一步返回的 file_token"]}}
JSON
open_api_request POST /sms/signatures '' signature.json
# 3. 提交模板申请:signature_id 可用本人已通过的签名或平台共享签名
cat > template.json <<'JSON'
{"name":"登录验证码","channel_type":"CN_OTP","signature_id":23,"content":"您的验证码为${code},5分钟内有效,请勿泄露给他人。"}
JSON
open_api_request POST /sms/templates/applications '' template.json
# 4. 查询模板审核结果:把 70 换成上一步返回的 data.id
open_api_request GET /sms/templates/applications/70
短信资质 API
申请企业实名资质
企业实名资质是 申请短信签名 的前提。提交后为「待平台审核」,平台初审通过后提交短信通道审核,结果通过 查询企业实名资质 获取。经办人(以及手机号与经办人不同的责任人)需要完成手机验证码验证,验证码只发到本人手机,请向本人获取后填写。
第一步:上传材料图片
/api/open/v1/sms/qualifications/files?slot={slot}&position={position}
材料图片逐张上传,换取 file_token,再在申请企业实名资质时引用。请求体直接放图片的原始字节,按普通请求签名即可:待签名串第 6 行为图片字节的 SHA256,slot 与 position 作为查询参数参与第 3 行。
- 只支持 JPG、PNG,单张不超过 10MB,宽和高都不小于 300 像素;平台会重新编码并加密保存。
- file_token 只能被上传它的账号引用;24 小时内没有被提交引用即失效,需要重新上传。
- 修改后重新提交时,详情 files[] 中已有的 file_token 可以继续使用,只需上传要替换的图片。
请求参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
slot
|
string | 是 | 材料位置(查询参数),取值见下方「材料位置」。 |
position
|
integer | 否 | 同一位置的第几张,从 0 开始,默认 0(查询参数)。 |
Content-Type
|
string | 是 | 请求头,image/jpeg 或 image/png;请求体为图片的原始字节,不要用 multipart 表单或 base64。 |
材料位置(slot)
| 取值 | 名称 | 是否必传 |
|---|---|---|
business_license
|
营业证件 | 必传 |
operator_front
|
经办人身份证人像面 | 必传 |
operator_back
|
经办人身份证国徽面 | 必传 |
responsible_front
|
责任人身份证人像面 | responsible_same 为 false 时必传 |
responsible_back
|
责任人身份证国徽面 | responsible_same 为 false 时必传 |
legal_front
|
法人证件人像面 | legal_same 为 false 时必传 |
legal_back
|
法人证件国徽面 | legal_same 为 false 且法人证件为居民身份证时必传 |
power_of_attorney
|
授权委托书 | 必传,加盖公章,1–3 页,position 为 0–2 |
响应字段(HTTP 201)
| 字段 | 类型 | 说明 |
|---|---|---|
data.file_token
|
string | 材料凭证(48 位),提交资质或签名时放进 files 中引用。 |
data.slot
|
string | 材料位置。 |
data.slot_label
|
string | 材料位置的中文名称。 |
data.position
|
integer | 同一位置的第几张。 |
data.width / data.height
|
integer | 平台重新编码后的图片宽高(像素)。 |
data.size
|
integer | 平台重新编码后的大小(字节)。 |
data.expires_at
|
string | null | 凭证失效时间:超过该时间仍未被提交引用即失效;已被资质或签名引用时为 null。 |
{
"ok": true,
"data": {
"file_token": "Q7hT2mKp9vXc4LbN8sWd3RfY6gJz1aHe5uPk0oVi7nMqBt2C",
"slot": "other",
"slot_label": "其他证明材料",
"position": 0,
"width": 1600,
"height": 1200,
"size": 286512,
"expires_at": "2026-10-10T02:15:30.000000Z"
}
}
第二步:获取手机验证码
/api/open/v1/sms/qualifications/verify-codes
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
role
|
string | 是 | operator(经办人)或 responsible(责任人)。 |
mobile
|
string | 是 | 接收验证码的 11 位手机号,须与提交资质时填写的经办人 / 责任人手机号一致。 |
验证码 5 分钟内有效,最多可输错 5 次;同一手机号 60 秒内只能获取一次,每个应用每天最多 20 次。响应中的 resend_after 为可再次获取的等待秒数,expires_in 为验证码有效秒数。
{
"role": "operator",
"mobile": "13800000000"
}
{
"ok": true,
"data": {
"role": "operator",
"role_label": "经办人",
"mobile": "138****0000",
"resend_after": 60,
"expires_in": 300
}
}
第三步:提交资质
/api/open/v1/sms/qualifications
请求参数(JSON 请求体)
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
material_name
|
string | 是 | 资质名称,2–20 字,便于区分多份资质。 |
company_name
|
string | 是 | 企业名称,与营业证件一致,2–80 字。 |
credit_code
|
string | 是 | 18 位统一社会信用代码,平台按校验位核对。 |
business_cert_type
|
integer | 是 | 营业证件类型,取值见下方「营业证件类型」。 |
valid_from
|
string | 是 | 证件有效期起始日,格式 YYYY-MM-DD,不晚于今天。 |
valid_long_term
|
boolean | 否 | 证件是否长期有效,默认 false。 |
valid_to
|
string | 非长期时 | 证件有效期截止日,格式 YYYY-MM-DD,须晚于今天。 |
effect_signatures
|
string | string[] | 是 | 计划使用该资质申请的签名,字符串用逗号分隔或直接传数组,不需要【】。 |
operator_name
|
string | 是 | 经办人姓名。 |
operator_id_number
|
string | 是 | 经办人 18 位居民身份证号码。 |
operator_mobile
|
string | 是 | 经办人本人实名登记的 11 位手机号。 |
operator_mobile_code
|
string | 见说明 | 经办人手机收到的验证码。验证通过后 60 分钟内同一手机号再次提交(如修正其他字段)可不填;提交成功后需重新获取。 |
responsible_same
|
boolean | 否 | 责任人是否与经办人为同一人,默认 false。 |
responsible_name / responsible_id_number / responsible_mobile
|
string | responsible_same 为 false 时 | 责任人姓名、身份证号码与手机号。 |
responsible_mobile_code
|
string | 见说明 | 责任人手机号与经办人不同时需要,规则同 operator_mobile_code。 |
legal_same
|
boolean | 否 | 法人是否与责任人为同一人,默认 false。 |
legal_cert_type
|
integer | legal_same 为 false 时 | 法人证件类型,取值见下方「法人证件类型」。 |
legal_name / legal_id_number / legal_mobile
|
string | legal_same 为 false 时 | 法人姓名、证件号码与手机号。 |
files
|
object | 是 | 材料凭证:键为材料位置,值为 file_token;power_of_attorney 为 file_token 数组(按页序)。必传位置见「材料位置」。 |
营业证件类型(business_cert_type)
| 取值 | 名称 |
|---|---|
1
|
企业营业执照 |
4
|
社会信用代码证书 |
7
|
事业单位法人证书 |
6
|
其他证件 |
法人证件类型(legal_cert_type)
| 取值 | 名称 |
|---|---|
0
|
居民身份证 |
1
|
护照 |
2
|
港澳居民来往内地通行证 |
3
|
台湾居民来往大陆通行证 |
4
|
港澳台居民居住证 |
请求示例
示例中的证件号码与信用代码仅作格式演示,平台会校验校验位,不能直接提交。
{
"material_name": "示例科技资质",
"company_name": "示例科技有限公司",
"credit_code": "91110000000000000X",
"business_cert_type": 1,
"valid_from": "2020-01-01",
"valid_long_term": true,
"effect_signatures": [
"示例科技"
],
"operator_name": "张三",
"operator_id_number": "110101199001011234",
"operator_mobile": "13800000000",
"operator_mobile_code": "123456",
"responsible_same": true,
"legal_same": true,
"files": {
"business_license": "file_token",
"operator_front": "file_token",
"operator_back": "file_token",
"power_of_attorney": [
"file_token"
]
}
}
成功响应(HTTP 201)
data 为资质详情,字段见 查询企业实名资质。
验证码错误(HTTP 422)
{
"ok": false,
"error_code": "invalid_parameter",
"message": "验证码不正确,还可以再试 4 次。",
"errors": {
"operator_mobile_code": [
"验证码不正确,还可以再试 4 次。"
]
}
}
短信资质 API
查询企业实名资质
/api/open/v1/sms/qualifications?status={status}&page={page}&per_page={per_page}
/api/open/v1/sms/qualifications/{id}
查询本账号的资质与审核进度。列表包含本人提交的资质与平台分配给本账号的资质,usable_for_signature 为 true 的资质可以用于 申请短信签名。
查询参数(列表)
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
status
|
string | 否 | 按状态筛选,取值见各接口的「列表筛选」;不传或取值无效时返回全部。 |
page
|
integer | 否 | 页码,从 1 开始,默认 1。 |
per_page
|
integer | 否 | 每页条数,默认 20,最大 50。 |
status 取值
| 取值 | 名称 |
|---|---|
reviewing
|
审核中 |
approved
|
已通过 |
rejected
|
已驳回 |
withdrawn
|
已撤回 |
unavailable
|
已失效 |
列表响应为 data.items[] 与分页信息 data.pagination(page、per_page、total、last_page)。
响应字段(详情)
| 字段 | 类型 | 说明 |
|---|---|---|
data.id
|
integer | 资质 ID,提交签名时作为 qualification_id。 |
data.source
|
string | user(本人提交)或 platform(平台分配,不能修改或撤回,详情不含人员信息)。 |
data.status / data.status_label
|
string | 审核状态及中文名称,见下方「资质状态」。 |
data.status_hint
|
string | 当前状态的说明(下一步会发生什么)。 |
data.rejection_reason
|
string | null | 驳回原因(平台驳回或通道驳回时)。 |
data.usable_for_signature
|
boolean | 为 true 时可以用于申请签名。 |
data.editable / data.withdrawable
|
boolean | 当前是否可以修改重新提交 / 撤回。 |
data.credit_code
|
string | null | 统一社会信用代码(脱敏)。 |
data.persons
|
object | null | 经办人 operator、责任人 responsible、法人 legal;证件号码与手机号脱敏。平台分配的资质为 null。 |
data.files[]
|
array | 已引用的材料凭证(file_token、slot、position),修改重新提交时可以直接沿用。证件照片本身不通过 API 返回。 |
data.history[]
|
array | 处理记录:event、label(中文)、at。 |
data.submitted_at / approved_at / rejected_at
|
string | null | 提交、通过、驳回时间。 |
资质状态(data.status)
| 取值 | 名称 | 说明 |
|---|---|---|
pending_review
|
待平台审核 | 已提交,等待平台初审;此时可以撤回。 |
platform_rejected
|
平台驳回 | 平台初审未通过,可按 rejection_reason 修改后重新提交。 |
submitting
|
平台处理中 | 平台正在提交短信通道(submit_failed、timeout 也显示为「平台处理中」),无需操作。 |
submitted
|
通道审核中 | 短信通道审核中,预计 4 小时内出结果(9:00–21:00)。 |
approved
|
已通过 | 审核通过,可用于申请签名。 |
rejected
|
通道驳回 | 短信通道审核未通过,可修改后重新提交。 |
withdrawn
|
已撤回 | 已撤回,可修改后重新提交。 |
unavailable
|
已失效 | 资质在短信通道已不可用(关闭或过期)。 |
详情响应示例
{
"ok": true,
"data": {
"id": 8,
"material_name": "示例科技资质",
"company_name": "示例科技有限公司",
"credit_code": "9111**********000X",
"source": "user",
"source_label": "本人提交",
"status": "pending_review",
"status_label": "待平台审核",
"status_hint": "平台正在初审资料,通常 1 个工作日内完成;初审开始前可以撤回修改。",
"rejection_reason": null,
"usable_for_signature": false,
"editable": false,
"withdrawable": true,
"revision": 1,
"submitted_at": "2026-10-09T02:15:30.000000Z",
"approved_at": null,
"rejected_at": null,
"created_at": "2026-10-09T02:15:30.000000Z",
"updated_at": "2026-10-09T02:15:30.000000Z",
"business_cert_type": 1,
"business_cert_type_label": "企业营业执照",
"valid_from": "2020-01-01",
"valid_to": null,
"valid_long_term": true,
"effect_signatures": [
"示例科技"
],
"persons": {
"operator": {
"name": "张三",
"id_number": "110***********1234",
"mobile": "138****0000"
},
"responsible": {
"name": "张三",
"id_number": "110***********1234",
"mobile": "138****0000",
"same_as_operator": true
},
"legal": {
"name": "张三",
"id_number": "110***********1234",
"mobile": "138****0000",
"cert_type": 0,
"cert_type_label": "居民身份证",
"same_as_responsible": true
}
},
"files": [
{
"file_token": "Q7hT2mKp9vXc4LbN8sWd3RfY6gJz1aHe5uPk0oVi7nMqBt2C",
"slot": "business_license",
"slot_label": "营业证件",
"position": 0,
"width": 1600,
"height": 1200,
"size": 286512,
"expires_at": null
}
],
"history": [
{
"event": "submitted",
"label": "用户提交资质",
"at": "2026-10-09T02:15:30.000000Z"
}
]
}
}
短信资质 API
修改企业实名资质
/api/open/v1/sms/qualifications/{id}
资质被驳回(平台驳回或通道驳回)或已撤回后,按 rejection_reason 修改并重新提交。平台分配的资质(source 为 platform)由平台维护,不能修改。
- 只有详情中 editable 为 true(平台驳回、通道驳回或已撤回)时可以修改。
- 请求体与 申请企业实名资质 › 第三步:提交资质 相同,须提交完整资料(不是部分更新)。
- 详情 files[] 中已有的 file_token 可以继续使用,只需上传要替换的照片;被替换下来的旧照片会被删除。
- 经办人、责任人的手机号需要重新获取验证码验证(上一次提交成功后验证凭证即失效)。
- 修改后重新提交也计入每天 3 次的提交次数。
- 成功返回 HTTP 200,data 为修改后的详情:状态重新变为「待平台审核」,revision 加 1。
- 状态不允许修改时返回 HTTP 409,error_code 为 invalid_state;记录不存在或不属于本账号时返回 HTTP 404。
请求参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
id
|
integer | 是 | 资质 ID(路径参数);请求体字段见「申请企业实名资质 › 第三步:提交资质」。 |
成功响应(HTTP 200)
{
"ok": true,
"data": {
"id": 8,
"material_name": "示例科技资质",
"company_name": "示例科技有限公司",
"credit_code": "9111**********000X",
"source": "user",
"source_label": "本人提交",
"status": "pending_review",
"status_label": "待平台审核",
"status_hint": "平台正在初审资料,通常 1 个工作日内完成;初审开始前可以撤回修改。",
"rejection_reason": null,
"usable_for_signature": false,
"editable": false,
"withdrawable": true,
"revision": 1,
"submitted_at": "2026-10-09T02:15:30.000000Z",
"approved_at": null,
"rejected_at": null,
"created_at": "2026-10-09T02:15:30.000000Z",
"updated_at": "2026-10-09T02:15:30.000000Z",
"business_cert_type": 1,
"business_cert_type_label": "企业营业执照",
"valid_from": "2020-01-01",
"valid_to": null,
"valid_long_term": true,
"effect_signatures": [
"示例科技"
],
"persons": {
"operator": {
"name": "张三",
"id_number": "110***********1234",
"mobile": "138****0000"
},
"responsible": {
"name": "张三",
"id_number": "110***********1234",
"mobile": "138****0000",
"same_as_operator": true
},
"legal": {
"name": "张三",
"id_number": "110***********1234",
"mobile": "138****0000",
"cert_type": 0,
"cert_type_label": "居民身份证",
"same_as_responsible": true
}
},
"files": [
{
"file_token": "Q7hT2mKp9vXc4LbN8sWd3RfY6gJz1aHe5uPk0oVi7nMqBt2C",
"slot": "business_license",
"slot_label": "营业证件",
"position": 0,
"width": 1600,
"height": 1200,
"size": 286512,
"expires_at": null
}
],
"history": [
{
"event": "submitted",
"label": "用户提交资质",
"at": "2026-10-09T02:15:30.000000Z"
}
]
}
}
HTTP 409 · 当前状态不能修改
{
"ok": false,
"error_code": "invalid_state",
"message": "当前状态的资质不能修改。"
}
短信资质 API
撤回企业实名资质申请
/api/open/v1/sms/qualifications/{id}/withdraw
撤回尚未开始审核的资质申请。只有详情中 withdrawable 为 true(状态为 pending_review「待平台审核」)时可以撤回;平台开始审核后不能撤回,可等待审核结果。
- 请求体为空,路径中的 {id} 为资质申请的 data.id。
- 撤回成功返回 HTTP 200,data 为撤回后的详情:status 为 withdrawn(已撤回),editable 变为 true。
- 撤回后可以按「修改企业实名资质」修改重新提交;撤回的资质不再占用「同时审核中」的名额。
- 状态不允许撤回时返回 HTTP 409,error_code 为 invalid_state,原因见 message。
- 记录不存在或不属于本账号时返回 HTTP 404。
请求参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
id
|
integer | 是 | 资质申请 ID(路径参数)。 |
成功响应(HTTP 200)
{
"ok": true,
"data": {
"id": 8,
"material_name": "示例科技资质",
"company_name": "示例科技有限公司",
"credit_code": "9111**********000X",
"source": "user",
"source_label": "本人提交",
"status": "withdrawn",
"status_label": "已撤回",
"status_hint": "申请已撤回,可以修改后重新提交。",
"rejection_reason": null,
"usable_for_signature": false,
"editable": true,
"withdrawable": false,
"revision": 1,
"submitted_at": "2026-10-09T02:15:30.000000Z",
"approved_at": null,
"rejected_at": null,
"created_at": "2026-10-09T02:15:30.000000Z",
"updated_at": "2026-10-09T02:30:05.000000Z",
"business_cert_type": 1,
"business_cert_type_label": "企业营业执照",
"valid_from": "2020-01-01",
"valid_to": null,
"valid_long_term": true,
"effect_signatures": [
"示例科技"
],
"persons": {
"operator": {
"name": "张三",
"id_number": "110***********1234",
"mobile": "138****0000"
},
"responsible": {
"name": "张三",
"id_number": "110***********1234",
"mobile": "138****0000",
"same_as_operator": true
},
"legal": {
"name": "张三",
"id_number": "110***********1234",
"mobile": "138****0000",
"cert_type": 0,
"cert_type_label": "居民身份证",
"same_as_responsible": true
}
},
"files": [
{
"file_token": "Q7hT2mKp9vXc4LbN8sWd3RfY6gJz1aHe5uPk0oVi7nMqBt2C",
"slot": "business_license",
"slot_label": "营业证件",
"position": 0,
"width": 1600,
"height": 1200,
"size": 286512,
"expires_at": null
}
],
"history": [
{
"event": "submitted",
"label": "用户提交资质",
"at": "2026-10-09T02:15:30.000000Z"
},
{
"event": "withdrawn",
"label": "用户撤回申请",
"at": "2026-10-09T02:30:05.000000Z"
}
]
}
}
HTTP 409 · 当前状态不能撤回
{
"ok": false,
"error_code": "invalid_state",
"message": "平台已开始处理该资质,不能撤回。"
}
短信签名 API
申请短信签名
使用本账号名下已通过的企业实名资质申请自己的短信签名。提交后为「待平台审核」,平台审核通过后提交短信通道审核,结果通过 查询短信签名 获取。同一签名内容在平台内只能申请一次;已是平台共享签名的内容无需申请,可直接使用。还没有资质时,请先 申请企业实名资质。
第一步:上传材料图片
/api/open/v1/sms/signatures/files?slot={slot}&position={position}
材料图片逐张上传,换取 file_token,再在申请短信签名时引用。请求体直接放图片的原始字节,按普通请求签名即可:待签名串第 6 行为图片字节的 SHA256,slot 与 position 作为查询参数参与第 3 行。
- 只支持 JPG、PNG,单张不超过 10MB,宽和高都不小于 300 像素;平台会重新编码并加密保存。
- file_token 只能被上传它的账号引用;24 小时内没有被提交引用即失效,需要重新上传。
- 修改后重新提交时,详情 files[] 中已有的 file_token 可以继续使用,只需上传要替换的图片。
请求参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
slot
|
string | 是 | 材料位置(查询参数),取值见下方「材料位置」。 |
position
|
integer | 否 | 同一位置的第几张,从 0 开始,默认 0(查询参数)。 |
Content-Type
|
string | 是 | 请求头,image/jpeg 或 image/png;请求体为图片的原始字节,不要用 multipart 表单或 base64。 |
材料位置(slot)
| 取值 | 名称 | 是否必传 |
|---|---|---|
app_icp
|
APP 备案截图 | sign_source 为 2(APP 全称)时必传;每个位置最多 3 张 |
trademark
|
商标注册证书 | sign_source 为 3(商标全称)时必传;每个位置最多 3 张 |
other
|
其他证明材料 | 选传,如营业执照、官网截图等辅助证明;每个位置最多 3 张 |
响应字段(HTTP 201)
| 字段 | 类型 | 说明 |
|---|---|---|
data.file_token
|
string | 材料凭证(48 位),提交资质或签名时放进 files 中引用。 |
data.slot
|
string | 材料位置。 |
data.slot_label
|
string | 材料位置的中文名称。 |
data.position
|
integer | 同一位置的第几张。 |
data.width / data.height
|
integer | 平台重新编码后的图片宽高(像素)。 |
data.size
|
integer | 平台重新编码后的大小(字节)。 |
data.expires_at
|
string | null | 凭证失效时间:超过该时间仍未被提交引用即失效;已被资质或签名引用时为 null。 |
{
"ok": true,
"data": {
"file_token": "Q7hT2mKp9vXc4LbN8sWd3RfY6gJz1aHe5uPk0oVi7nMqBt2C",
"slot": "other",
"slot_label": "其他证明材料",
"position": 0,
"width": 1600,
"height": 1200,
"size": 286512,
"expires_at": "2026-10-10T02:15:30.000000Z"
}
}
第二步:提交签名申请
/api/open/v1/sms/signatures
请求参数(JSON 请求体)
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
qualification_id
|
integer | 是 | 资质 ID,须为本账号名下 usable_for_signature 为 true 的资质。 |
content
|
string | 是 | 签名内容,2–12 字,只能包含中文、英文字母和数字,不能是纯数字,不需要【】。 |
sign_source
|
integer | 是 | 签名来源,取值见下方「签名来源」。 |
scene
|
string | 是 | 业务场景,说明签名用于发送哪些短信,5–200 字。 |
description
|
string | 否 | 申请说明,最多 200 字。 |
app_icp_filling
|
string | sign_source 为 2 时 | APP 备案号,4–60 个字符。 |
trademark_cn / trademark_en
|
string | sign_source 为 3 时至少一项 | 商标中文名 / 英文名,最多 60 字。 |
trademark_number
|
string | sign_source 为 3 时 | 商标注册号,4–30 位字母、数字或短横线。 |
files
|
object | 见说明 | 材料凭证:键为材料位置,值为 file_token 数组。必传位置见「材料位置」。 |
签名来源(sign_source)
| 取值 | 名称 |
|---|---|
1
|
公司全称 / 简称 |
2
|
APP 全称 |
3
|
商标全称 |
请求示例
{
"qualification_id": 8,
"content": "示例科技",
"sign_source": 1,
"scene": "用于向注册用户发送登录验证码与订单通知",
"files": {
"other": [
"file_token"
]
}
}
成功响应(HTTP 201)
data 为签名详情,字段见 查询短信签名。
{
"ok": true,
"data": {
"id": 23,
"content": "示例科技",
"sign_source": 1,
"sign_source_label": "公司全称 / 简称",
"status": "pending_review",
"status_label": "待平台审核",
"status_hint": "平台正在审核签名资料,通常 1 个工作日内完成;审核开始前可以撤回修改。",
"rejection_reason": null,
"usable_for_sending": false,
"editable": false,
"withdrawable": true,
"expires_at": null,
"submitted_at": "2026-10-09T02:15:30.000000Z",
"created_at": "2026-10-09T02:15:30.000000Z",
"updated_at": "2026-10-09T02:15:30.000000Z",
"qualification": {
"id": 8,
"material_name": "示例科技资质",
"company_name": "示例科技有限公司"
},
"scene": "用于向注册用户发送登录验证码与订单通知",
"description": null,
"materials": {
"app_icp_filling": null,
"trademark_cn": null,
"trademark_en": null,
"trademark_number": null
},
"files": [
{
"file_token": "Q7hT2mKp9vXc4LbN8sWd3RfY6gJz1aHe5uPk0oVi7nMqBt2C",
"slot": "other",
"slot_label": "其他证明材料",
"position": 0,
"width": 1600,
"height": 1200,
"size": 286512,
"expires_at": null
}
],
"revision": 1,
"approved_at": null,
"rejected_at": null,
"history": [
{
"event": "submitted",
"label": "用户提交签名申请",
"at": "2026-10-09T02:15:30.000000Z"
}
]
}
}
短信签名 API
查询短信签名
/api/open/v1/sms/signatures?status={status}&page={page}&per_page={per_page}
/api/open/v1/sms/signatures/{id}
查询本账号申请的签名及审核进度。只看可以使用的签名时传 status=approved,或按 usable_for_sending 为 true 判断;这些签名可以用于 申请短信模板。平台共享签名不在此列表中,见 查询可用短信模板。
查询参数(列表)
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
status
|
string | 否 | 按状态筛选,取值见各接口的「列表筛选」;不传或取值无效时返回全部。 |
page
|
integer | 否 | 页码,从 1 开始,默认 1。 |
per_page
|
integer | 否 | 每页条数,默认 20,最大 50。 |
status 取值
| 取值 | 名称 |
|---|---|
reviewing
|
审核中 |
approved
|
已通过 |
rejected
|
已驳回 |
withdrawn
|
已撤回 |
invalid
|
已失效 / 已过期 |
列表响应为 data.items[] 与分页信息 data.pagination(page、per_page、total、last_page);items 不含 qualification、materials、files、history,需要时查询详情。
响应字段(详情)
| 字段 | 类型 | 说明 |
|---|---|---|
data.id
|
integer | 签名 ID,提交模板时作为 signature_id。 |
data.content
|
string | 签名内容。 |
data.status / data.status_label
|
string | 审核状态及中文名称,见下方「签名状态」;已通过但已过期时为 expired。 |
data.usable_for_sending
|
boolean | 为 true 时签名已通过且未过期,可用于申请模板。 |
data.editable / data.withdrawable
|
boolean | 当前是否可以修改重新提交 / 撤回。 |
data.qualification
|
object | null | 所用资质的 id、material_name、company_name(详情返回)。 |
data.materials
|
object | APP 备案号与商标信息(详情返回)。 |
data.files[]
|
array | 已引用的材料凭证,修改重新提交时可以直接沿用(详情返回)。 |
data.rejection_reason
|
string | null | 驳回原因。 |
data.expires_at
|
string | null | 签名到期时间,没有期限时为 null。 |
data.history[]
|
array | 处理记录(详情返回)。 |
签名状态(data.status)
| 取值 | 名称 | 说明 |
|---|---|---|
pending_review
|
待平台审核 | 已提交,等待平台审核;此时可以撤回。 |
platform_rejected
|
平台驳回 | 平台审核未通过,可修改后重新提交。 |
submitting
|
平台处理中 | 平台正在提交短信通道(submit_failed、timeout 也显示为「平台处理中」),无需操作。 |
submitted
|
通道审核中 | 短信通道审核中,通常 2 小时内出结果(9:00–21:00)。 |
approved
|
已通过 | 审核通过,可用于申请模板。 |
rejected
|
通道驳回 | 短信通道审核未通过,可修改后重新提交。 |
withdrawn
|
已撤回 | 已撤回,可修改后重新提交。 |
unavailable
|
已失效 | 签名在短信通道已不可用。 |
expired
|
已过期 | 签名已过期。 |
详情响应示例
{
"ok": true,
"data": {
"id": 23,
"content": "示例科技",
"sign_source": 1,
"sign_source_label": "公司全称 / 简称",
"status": "pending_review",
"status_label": "待平台审核",
"status_hint": "平台正在审核签名资料,通常 1 个工作日内完成;审核开始前可以撤回修改。",
"rejection_reason": null,
"usable_for_sending": false,
"editable": false,
"withdrawable": true,
"expires_at": null,
"submitted_at": "2026-10-09T02:15:30.000000Z",
"created_at": "2026-10-09T02:15:30.000000Z",
"updated_at": "2026-10-09T02:15:30.000000Z",
"qualification": {
"id": 8,
"material_name": "示例科技资质",
"company_name": "示例科技有限公司"
},
"scene": "用于向注册用户发送登录验证码与订单通知",
"description": null,
"materials": {
"app_icp_filling": null,
"trademark_cn": null,
"trademark_en": null,
"trademark_number": null
},
"files": [
{
"file_token": "Q7hT2mKp9vXc4LbN8sWd3RfY6gJz1aHe5uPk0oVi7nMqBt2C",
"slot": "other",
"slot_label": "其他证明材料",
"position": 0,
"width": 1600,
"height": 1200,
"size": 286512,
"expires_at": null
}
],
"revision": 1,
"approved_at": null,
"rejected_at": null,
"history": [
{
"event": "submitted",
"label": "用户提交签名申请",
"at": "2026-10-09T02:15:30.000000Z"
}
]
}
}
短信签名 API
修改短信签名
/api/open/v1/sms/signatures/{id}
签名被驳回(平台驳回或通道驳回)或已撤回后,按 rejection_reason 修改并重新提交。
- 只有详情中 editable 为 true(平台驳回、通道驳回或已撤回)时可以修改。
- 请求体与 申请短信签名 › 第二步:提交签名申请 相同,须提交完整资料(不是部分更新)。
- 详情 files[] 中已有的 file_token 可以继续使用,只需上传要替换的图片。
- 修改后重新提交也计入每天 10 次的提交次数。
- 成功返回 HTTP 200,data 为修改后的详情:状态重新变为「待平台审核」,revision 加 1。
- 状态不允许修改时返回 HTTP 409,error_code 为 invalid_state;记录不存在或不属于本账号时返回 HTTP 404。
请求参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
id
|
integer | 是 | 签名 ID(路径参数);请求体字段见「申请短信签名 › 第二步:提交签名申请」。 |
成功响应(HTTP 200)
{
"ok": true,
"data": {
"id": 23,
"content": "示例科技",
"sign_source": 1,
"sign_source_label": "公司全称 / 简称",
"status": "pending_review",
"status_label": "待平台审核",
"status_hint": "平台正在审核签名资料,通常 1 个工作日内完成;审核开始前可以撤回修改。",
"rejection_reason": null,
"usable_for_sending": false,
"editable": false,
"withdrawable": true,
"expires_at": null,
"submitted_at": "2026-10-09T02:15:30.000000Z",
"created_at": "2026-10-09T02:15:30.000000Z",
"updated_at": "2026-10-09T02:15:30.000000Z",
"qualification": {
"id": 8,
"material_name": "示例科技资质",
"company_name": "示例科技有限公司"
},
"scene": "用于向注册用户发送登录验证码与订单通知",
"description": null,
"materials": {
"app_icp_filling": null,
"trademark_cn": null,
"trademark_en": null,
"trademark_number": null
},
"files": [
{
"file_token": "Q7hT2mKp9vXc4LbN8sWd3RfY6gJz1aHe5uPk0oVi7nMqBt2C",
"slot": "other",
"slot_label": "其他证明材料",
"position": 0,
"width": 1600,
"height": 1200,
"size": 286512,
"expires_at": null
}
],
"revision": 1,
"approved_at": null,
"rejected_at": null,
"history": [
{
"event": "submitted",
"label": "用户提交签名申请",
"at": "2026-10-09T02:15:30.000000Z"
}
]
}
}
HTTP 409 · 当前状态不能修改
{
"ok": false,
"error_code": "invalid_state",
"message": "当前状态的签名不能修改。"
}
短信签名 API
撤回短信签名申请
/api/open/v1/sms/signatures/{id}/withdraw
撤回尚未开始审核的签名申请。只有详情中 withdrawable 为 true(状态为 pending_review「待平台审核」)时可以撤回;平台开始审核后不能撤回,可等待审核结果。
- 请求体为空,路径中的 {id} 为签名申请的 data.id。
- 撤回成功返回 HTTP 200,data 为撤回后的详情:status 为 withdrawn(已撤回),editable 变为 true。
- 撤回后可以按「修改短信签名」修改重新申请;撤回的签名仍占用签名名额。
- 状态不允许撤回时返回 HTTP 409,error_code 为 invalid_state,原因见 message。
- 记录不存在或不属于本账号时返回 HTTP 404。
请求参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
id
|
integer | 是 | 签名申请 ID(路径参数)。 |
成功响应(HTTP 200)
{
"ok": true,
"data": {
"id": 23,
"content": "示例科技",
"sign_source": 1,
"sign_source_label": "公司全称 / 简称",
"status": "withdrawn",
"status_label": "已撤回",
"status_hint": "申请已撤回,可以修改后重新提交。",
"rejection_reason": null,
"usable_for_sending": false,
"editable": true,
"withdrawable": false,
"expires_at": null,
"submitted_at": "2026-10-09T02:15:30.000000Z",
"created_at": "2026-10-09T02:15:30.000000Z",
"updated_at": "2026-10-09T02:30:05.000000Z",
"qualification": {
"id": 8,
"material_name": "示例科技资质",
"company_name": "示例科技有限公司"
},
"scene": "用于向注册用户发送登录验证码与订单通知",
"description": null,
"materials": {
"app_icp_filling": null,
"trademark_cn": null,
"trademark_en": null,
"trademark_number": null
},
"files": [
{
"file_token": "Q7hT2mKp9vXc4LbN8sWd3RfY6gJz1aHe5uPk0oVi7nMqBt2C",
"slot": "other",
"slot_label": "其他证明材料",
"position": 0,
"width": 1600,
"height": 1200,
"size": 286512,
"expires_at": null
}
],
"revision": 1,
"approved_at": null,
"rejected_at": null,
"history": [
{
"event": "submitted",
"label": "用户提交签名申请",
"at": "2026-10-09T02:15:30.000000Z"
},
{
"event": "withdrawn",
"label": "用户撤回申请",
"at": "2026-10-09T02:30:05.000000Z"
}
]
}
}
HTTP 409 · 当前状态不能撤回
{
"ok": false,
"error_code": "invalid_state",
"message": "平台已开始处理该签名,不能撤回。"
}
短信模板 API
申请短信模板
/api/open/v1/sms/templates/applications
选择本人已通过的签名(见 查询短信签名)或平台共享签名申请短信模板。提交后为「待平台审核」,平台审核通过后提交短信通道审核;通过后模板会出现在 查询可用短信模板 中,申请详情的 send_options 给出发送短信使用的 template_variant_id 与 signature_id。
请求参数(JSON 请求体)
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
name
|
string | 是 | 模板名称,2–30 字。 |
channel_type
|
string | 是 | 模板类型,取值见下方「模板类型」。 |
signature_id
|
integer | 是 | 签名 ID:本人 usable_for_sending 为 true 的签名,或「查询可用短信模板」中出现的平台共享签名。 |
content
|
string | 是 | 模板正文,2–500 字,不要包含【签名】(平台按所选签名自动添加)。变量写成 ${变量名},变量名以英文字母开头,只能包含字母、数字和下划线,最长 30 位,最多 10 个;验证码模板至少包含 1 个变量。 |
description
|
string | 否 | 申请说明,最多 200 字。 |
模板类型(channel_type)
| 取值 | 名称 |
|---|---|
CN_OTP
|
验证码 |
CN_NTC
|
通知 |
CN_MKT
|
营销 |
请求示例
{
"name": "登录验证码",
"channel_type": "CN_OTP",
"signature_id": 23,
"content": "您的验证码为${code},5分钟内有效,请勿泄露给他人。"
}
成功响应(HTTP 201)
data 为模板申请详情,字段见 查询短信模板申请。刚提交时 status 为 pending_review,send_options 为空数组。
短信模板 API
查询短信模板申请
/api/open/v1/sms/templates/applications?status={status}&page={page}&per_page={per_page}
/api/open/v1/sms/templates/applications/{id}
查询本账号申请的模板及审核进度。status 为 approved 时 usable_for_sending 为 true,send_options 给出发送用的 template_variant_id 与 signature_id,与 查询可用短信模板 中的 items[].variant.id、items[].signature.id 一致。分页参数同 查询短信签名。
status 取值
| 取值 | 名称 |
|---|---|
reviewing
|
审核中 |
approved
|
已通过 |
rejected
|
已驳回 |
withdrawn
|
已撤回 |
invalid
|
已失效 / 已过期 |
响应字段(详情)
| 字段 | 类型 | 说明 |
|---|---|---|
data.id
|
integer | 模板申请 ID。 |
data.channel_type / data.channel_type_label
|
string | 模板类型及中文名称。 |
data.signature
|
object | null | 所选签名:id、content、shared(是否平台共享签名)。 |
data.status / data.status_label
|
string | 审核状态及中文名称,见下方「模板状态」。 |
data.usable_for_sending
|
boolean | 为 true 时已可发送。 |
data.send_options[]
|
array | 发送短信时使用的 template_variant_id 与 signature_id;审核通过前为空数组。 |
data.editable / data.withdrawable
|
boolean | 当前是否可以修改重新提交 / 撤回。 |
data.content / data.variables
|
string / string[] | 模板正文与解析出的变量名(详情返回)。 |
data.rejection_reason
|
string | null | 驳回原因。 |
data.history[]
|
array | 处理记录(详情返回)。 |
模板状态(data.status)
| 取值 | 名称 | 说明 |
|---|---|---|
pending_review
|
待平台审核 | 已提交,等待平台审核;此时可以撤回。 |
platform_rejected
|
平台驳回 | 平台审核未通过,可修改后重新提交。 |
submitting
|
平台处理中 | 平台正在提交短信通道(submit_failed、timeout 也显示为「平台处理中」),无需操作。 |
submitted
|
通道审核中 | 短信通道审核中,通常 2 小时内出结果(9:00–21:00)。 |
approved
|
已通过 | 审核通过,按 send_options 发送短信。 |
rejected
|
通道驳回 | 短信通道审核未通过,可修改后重新提交。 |
withdrawn
|
已撤回 | 已撤回,可修改后重新提交。 |
unavailable
|
已失效 | 模板所用签名或模板在短信通道已不可用。 |
列表响应示例
{
"ok": true,
"data": {
"items": [
{
"id": 70,
"name": "登录验证码",
"channel_type": "CN_OTP",
"channel_type_label": "验证码",
"status": "approved",
"status_label": "已通过",
"usable_for_sending": true,
"send_options": [
{
"template_variant_id": 31,
"signature_id": 23
}
],
"editable": false,
"withdrawable": false
}
],
"pagination": {
"page": 1,
"per_page": 20,
"total": 1,
"last_page": 1
}
}
}
详情响应示例(审核通过后)
{
"ok": true,
"data": {
"id": 70,
"name": "登录验证码",
"channel_type": "CN_OTP",
"channel_type_label": "验证码",
"signature": {
"id": 23,
"content": "示例科技",
"shared": false
},
"status": "approved",
"status_label": "已通过",
"status_hint": "模板已通过审核,可在开放 API 或站内发送中使用。",
"rejection_reason": null,
"usable_for_sending": true,
"send_options": [
{
"template_variant_id": 31,
"signature_id": 23
}
],
"editable": false,
"withdrawable": false,
"submitted_at": "2026-10-09T02:15:30.000000Z",
"created_at": "2026-10-09T02:15:30.000000Z",
"updated_at": "2026-10-09T05:40:12.000000Z",
"content": "您的验证码为${code},5分钟内有效,请勿泄露给他人。",
"variables": [
"code"
],
"description": null,
"revision": 1,
"approved_at": "2026-10-09T05:40:12.000000Z",
"rejected_at": null,
"history": [
{
"event": "submitted",
"label": "用户提交模板申请",
"at": "2026-10-09T02:15:30.000000Z"
},
{
"event": "platform_approved",
"label": "平台审核通过,提交短信通道",
"at": "2026-10-09T03:02:44.000000Z"
}
]
}
}
短信模板 API
修改短信模板
/api/open/v1/sms/templates/applications/{id}
模板被驳回(平台驳回或通道驳回)或已撤回后,按 rejection_reason 修改并重新提交。
- 只有详情中 editable 为 true(平台驳回、通道驳回或已撤回)时可以修改。
- 请求体与 申请短信模板 相同,须提交完整资料(不是部分更新)。
- 修改后上一次生成的发送组合(send_options)失效,审核通过后重新生成。
- 修改后重新提交也计入每天 10 次的提交次数。
- 成功返回 HTTP 200,data 为修改后的详情:状态重新变为「待平台审核」,revision 加 1。
- 状态不允许修改时返回 HTTP 409,error_code 为 request_rejected;记录不存在或不属于本账号时返回 HTTP 404。
请求参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
id
|
integer | 是 | 模板 ID(路径参数);请求体字段见「申请短信模板」。 |
HTTP 409 · 当前状态不能修改
{
"ok": false,
"error_code": "request_rejected",
"message": "当前状态的模板不能修改。"
}
短信模板 API
撤回短信模板申请
/api/open/v1/sms/templates/applications/{id}/withdraw
撤回尚未开始审核的模板申请。只有详情中 withdrawable 为 true(状态为 pending_review「待平台审核」)时可以撤回;平台开始审核后不能撤回,可等待审核结果。
- 请求体为空,路径中的 {id} 为模板申请的 data.id。
- 撤回成功返回 HTTP 200,data 为撤回后的详情:status 为 withdrawn(已撤回),editable 变为 true。
- 撤回后可以按「修改短信模板」修改重新申请;撤回的模板仍占用模板名额。
- 状态不允许撤回时返回 HTTP 409,error_code 为 request_rejected,原因见 message。
- 记录不存在或不属于本账号时返回 HTTP 404。
请求参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
id
|
integer | 是 | 模板申请 ID(路径参数)。 |
成功响应(HTTP 200)
{
"ok": true,
"data": {
"id": 70,
"name": "登录验证码",
"channel_type": "CN_OTP",
"channel_type_label": "验证码",
"signature": {
"id": 23,
"content": "示例科技",
"shared": false
},
"status": "withdrawn",
"status_label": "已撤回",
"status_hint": "申请已撤回,可以修改后重新提交。",
"rejection_reason": null,
"usable_for_sending": false,
"send_options": [],
"editable": true,
"withdrawable": false,
"submitted_at": "2026-10-09T02:15:30.000000Z",
"created_at": "2026-10-09T02:15:30.000000Z",
"updated_at": "2026-10-09T02:30:05.000000Z",
"content": "您的验证码为${code},5分钟内有效,请勿泄露给他人。",
"variables": [
"code"
],
"description": null,
"revision": 1,
"approved_at": null,
"rejected_at": null,
"history": [
{
"event": "submitted",
"label": "用户提交模板申请",
"at": "2026-10-09T02:15:30.000000Z"
},
{
"event": "withdrawn",
"label": "用户撤回申请",
"at": "2026-10-09T02:30:05.000000Z"
}
]
}
}
HTTP 409 · 当前状态不能撤回
{
"ok": false,
"error_code": "request_rejected",
"message": "平台已开始处理该模板,不能撤回。"
}
短信发送 API
查询可用短信模板
/api/open/v1/sms/templates
返回当前可以直接发送的模板变体及其绑定的签名,包括平台共享模板与本人申请并审核通过的模板。发送短信时,把 items[].variant.id 作为 template_variant_id、items[].signature.id 作为 signature_id 传入。模板与签名由平台审核维护,变化不频繁,建议在你的服务端缓存结果。查询本人模板申请的审核进度,见 查询短信模板申请。
请求参数
无。
响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
ok
|
boolean | 请求成功时为 true。 |
items
|
array | 可以直接发送的模板列表,按创建时间倒序;没有可用模板时为空数组。 |
items[].variant.id
|
integer | 模板变体 ID,发送短信时作为 template_variant_id 传入。 |
items[].variant.content
|
string | 短信正文,其中的 ${变量名} 需要通过 template_params 传入。 |
items[].variant.variant_key
|
string | 变体标识,由通道类型与签名组成,如 CN_OTP:sig2;同一模板绑定多个签名时,每个签名对应一个变体,仅供核对。 |
items[].variant.remote_variant_id
|
string | 通道侧的模板编号,仅供核对,发送时不需要传入。 |
items[].template.id
|
integer | 模板 ID。 |
items[].template.shared
|
boolean | true 表示平台共享模板,false 表示你本人申请并审核通过的模板。 |
items[].template.name
|
string | 模板名称。 |
items[].template.type
|
string | 模板类型,如 CN_OTP(验证码)、CN_NTC(通知)、CN_MKT(营销)。 |
items[].template.variables
|
array | 模板变量说明,可能为空数组;需要传入哪些变量以 variant.content 中的 ${变量名} 为准。 |
items[].signature.id
|
integer | 签名 ID,发送短信时作为 signature_id 传入。 |
items[].signature.name
|
string | 签名名称。 |
items[].signature.content
|
string | 签名内容,短信中显示为【签名内容】,并计入计费字数。 |
items[].supplier.id
|
integer | 短信通道 ID。 |
items[].supplier.name
|
string | 短信通道名称。 |
响应示例
{
"ok": true,
"items": [
{
"variant": {
"id": 12,
"remote_variant_id": "S1T_7a3c1e20",
"variant_key": "CN_OTP:sig2",
"content": "您的验证码为${code},5分钟内有效,请勿泄露给他人。"
},
"template": {
"id": 3,
"shared": true,
"name": "登录验证码",
"type": "CN_OTP",
"variables": []
},
"signature": {
"id": 2,
"name": "云信短信",
"content": "云信短信"
},
"supplier": {
"id": 1,
"name": "默认短信通道"
}
}
]
}
短信发送 API
发送短信
/api/open/v1/sms/send
使用同一组模板参数,向 1–200 个手机号发送同一条短信,返回发送批次。不同号码需要不同内容时,请分多次调用。
请求头
除公共请求头外,还需要:
| 请求头 | 类型 | 必填 | 说明 |
|---|---|---|---|
Content-Type
|
string | 是 | 固定为 application/json。 |
Idempotency-Key
|
string | 是 | 幂等键,1–128 位,只能包含字母、数字及 . _ : -;建议使用业务单号,详见「幂等与重试」。 |
请求参数(JSON 请求体)
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
template_variant_id
|
integer | 是 | 模板变体 ID,取「查询可用短信模板」返回的 items[].variant.id。 |
signature_id
|
integer | 是 | 签名 ID,必须是该模板绑定的签名,取 items[].signature.id。平台开启强制实名认证时,账户需要先完成实名认证(否则返回 kyc_required)。 |
phone_numbers
|
string[] | 是 | 接收短信的中国大陆手机号,1–200 个;支持 13800000000、+8613800000000、8613800000000 三种写法,平台统一按 11 位号码处理,同一号码的不同写法也算重复,同一请求内不能重复。 |
template_params
|
object | 是 | 模板变量,键为变量名、值为字符串或数字(不支持布尔值、null 与嵌套对象),最多 20 项,单个值不超过 500 字;必须包含正文中的全部 ${变量名}。验证码模板的参数须为 4–8 位数字或字母;平台共享模板的参数不超过 35 个字符,不能包含网址、【】或换行(否则返回 param_not_allowed)。 |
tag
|
string | 否 | 自定义标签,便于业务区分,1–128 位,只能包含字母、数字及 . _ : -。 |
- 请求体只能包含以上字段,出现其他字段会返回 HTTP 422。
- 模板变量名以字母开头,只能包含字母、数字及 _ . -,最长 80 位。
- template_params 至少需要 1 项,当前暂不支持没有变量的模板。
请求示例
{
"template_variant_id": 12,
"signature_id": 2,
"phone_numbers": [
"13800000000",
"13900000000"
],
"template_params": {
"code": "123456"
},
"tag": "login-otp"
}
成功响应(HTTP 200)
data 的字段与「查询发送记录」相同。status 为 submitted 表示已提交到通道,是否送达请稍后查询;replayed 为 true 表示这是相同 Idempotency-Key 的重复请求,返回的是首次请求的结果,不会重复发送或扣费。
HTTP 200 时也请检查 data.billing.status:为 pending_review 表示通道已受理请求但没有返回消息编号,无法确定是否发出,扣费暂不退还、由平台核实。这类批次收不到运营商回执,号码会在 6 小时后记为 timeout,请不要换新的 Idempotency-Key 重发。
{
"ok": true,
"replayed": false,
"data": {
"batch_id": 1024,
"batch_uuid": "9b2f6c1e-3d4a-4f8b-9c0d-1e2f3a4b5c6d",
"status": "submitted",
"total_count": 2,
"accepted_count": 2,
"rejected_count": 0,
"submitted_at": "2026-10-08T08:00:01.120586Z",
"completed_at": null,
"error_code": null,
"error_summary": null,
"messages": [
{
"id": 5001,
"remote_message_id": "2026100816000100001",
"status": "submitted",
"provider_status": null,
"sent_at": "2026-10-08T08:00:01.120586Z",
"delivered_at": null,
"failed_at": null,
"error_code": null,
"error_summary": null
},
{
"id": 5002,
"remote_message_id": "2026100816000100002",
"status": "submitted",
"provider_status": null,
"sent_at": "2026-10-08T08:00:01.120586Z",
"delivered_at": null,
"failed_at": null,
"error_code": null,
"error_summary": null
}
],
"billing": {
"status": "charged",
"status_label": "已扣费",
"chars_per_message": 34,
"segments_per_message": 1,
"phone_count": 2,
"total_segments": 2,
"package_segments": 1,
"balance_segments": 1,
"unit_price": "0.0450",
"balance_amount": "0.0450",
"refunded_segments": 0,
"refunded_amount": "0.0000"
}
}
}
失败响应
HTTP 402 · 余额与流量包不足
本次未发送、未扣费,也没有生成批次。充值或购买流量包后,可以继续使用同一个 Idempotency-Key 提交。
{
"ok": false,
"error_code": "insufficient_funds",
"message": "余额和流量包额度不足:本次需 2 条,流量包可用 0 条,余额可支付 0 条(单价 ¥0.045/条)。"
}
HTTP 409 · 发送失败
通道明确未受理,本次扣费已自动退还,billing.status 为 refunded。修正问题后,请使用新的 Idempotency-Key 重新发送。
{
"ok": false,
"batch_id": 1025,
"batch_uuid": "0c7d2a4e-6b1f-4e3a-8d9c-2f4b6a8c0e1d",
"error_code": "send_failed",
"message": "短信发送失败。本次扣费已退还。",
"billing": {
"status": "refunded",
"status_label": "已全额退还",
"chars_per_message": 34,
"segments_per_message": 1,
"phone_count": 2,
"total_segments": 2,
"package_segments": 1,
"balance_segments": 1,
"unit_price": "0.0450",
"balance_amount": "0.0450",
"refunded_segments": 2,
"refunded_amount": "0.0450"
}
}
HTTP 409 · 结果待核实
通道响应异常,无法确定短信是否已发出,扣费暂不退还,billing.status 为 pending_review。请按 batch_uuid 查询结果,不要换新的 Idempotency-Key 重发,以免重复发送。
{
"ok": false,
"batch_id": 1026,
"batch_uuid": "5e8a1c3d-2b4f-4a6e-9c1d-3e5f7a9b1c2d",
"error_code": "timeout",
"message": "短信通道响应异常,发送结果待核实,本次扣费暂不退还。",
"billing": {
"status": "pending_review",
"status_label": "待核实",
"chars_per_message": 34,
"segments_per_message": 1,
"phone_count": 2,
"total_segments": 2,
"package_segments": 1,
"balance_segments": 1,
"unit_price": "0.0450",
"balance_amount": "0.0450",
"refunded_segments": 0,
"refunded_amount": "0.0000"
}
}
HTTP 409 · 相同请求正在处理
相同 Idempotency-Key 的首次请求还没有完成,稍后按 batch_uuid 查询结果即可,不会重复发送。
{
"ok": false,
"batch_id": 1024,
"batch_uuid": "9b2f6c1e-3d4a-4f8b-9c0d-1e2f3a4b5c6d",
"error_code": "request_in_progress",
"message": "相同 Idempotency-Key 的请求正在处理中,请稍后查询批次结果。",
"billing": {
"status": "charged",
"status_label": "已扣费",
"chars_per_message": 34,
"segments_per_message": 1,
"phone_count": 2,
"total_segments": 2,
"package_segments": 1,
"balance_segments": 1,
"unit_price": "0.0450",
"balance_amount": "0.0450",
"refunded_segments": 0,
"refunded_amount": "0.0000"
}
}
HTTP 422 · 参数错误
本次未发送、未扣费。字段校验失败时 errors 按字段列出原因(请求体包含未允许的字段时,键为 _unknown_fields);业务校验失败(如签名与模板不匹配、缺少模板参数)只返回 message。
{
"message": "手机号格式无效。 (另有 1 项错误)",
"errors": {
"phone_numbers.0": [
"手机号格式无效。"
],
"template_params": [
"模板参数不能为空。"
]
}
}
HTTP 500 · 服务内部异常
平台内部异常,短信未提交到通道。请保持 Idempotency-Key 不变稍后重试:如果首次请求已生成批次,重试会直接返回该批次的结果,不会重复发送。
{
"ok": false,
"error_code": "internal_error",
"message": "服务暂时异常,请保持 Idempotency-Key 不变,稍后重试。"
}
短信发送 API
查询发送记录
/api/open/v1/sms/batches/{batch_uuid}
按发送接口返回的 batch_uuid,查询批次与每个号码的发送状态。只能查询本应用发出的批次,查询不到时返回 HTTP 404。运营商状态回执是异步返回的,建议发送后间隔 10 秒以上再查询。
路径参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
batch_uuid
|
string | 是 | 批次号,取发送接口返回的 data.batch_uuid。 |
响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
ok
|
boolean | 查询成功时为 true。 |
data
|
object | 批次信息;发送接口成功时返回的 data 与此相同。 |
data.batch_id
|
integer | 批次编号。 |
data.batch_uuid
|
string | 批次号,用于查询发送结果。 |
data.status
|
string | 批次状态,见下方「批次状态」。 |
data.total_count
|
integer | 本批次的号码数。 |
data.accepted_count
|
integer | 通道受理的号码数。 |
data.rejected_count
|
integer | 通道未受理的号码数(整批失败时等于 total_count),对应费用已退还。 |
data.submitted_at
|
string | null | 提交到通道的时间。 |
data.completed_at
|
string | null | 全部号码得到最终状态的时间。 |
data.error_code
|
string | null | 批次失败或待核实时的错误码:通道错误码(如 timeout、network);result_unknown 表示发送结果登记超时、待核实;not_sent 表示经平台核实未发出、费用已退还。 |
data.error_summary
|
string | null | 批次失败或待核实的原因。 |
data.messages
|
array | 每个号码一条记录,顺序与请求中的 phone_numbers 一致;出于隐私保护不返回手机号。批次状态为 timeout(结果待核实)时无法确定每个号码的结果,可能为空数组。 |
data.messages[].id
|
integer | 消息编号。 |
data.messages[].remote_message_id
|
string | null | 通道消息编号,反馈问题时请提供。 |
data.messages[].status
|
string | 号码的发送状态,见下方「号码状态」。 |
data.messages[].provider_status
|
string | null | 运营商回执的原始状态码:0 表示发送成功,其他(如 CR:0217)表示失败;未收到回执时为 null,仅供排查。 |
data.messages[].sent_at
|
string | null | 提交时间。 |
data.messages[].delivered_at
|
string | null | 送达时间。 |
data.messages[].failed_at
|
string | null | 失败时间。 |
data.messages[].error_code
|
string | null | 失败错误码:rejected 表示通道未受理该号码、本条费用已退还;not_sent 表示经平台核实未发出、费用已退还;其他为运营商回执状态码(如 CR:0217),已发出的短信不退费。 |
data.messages[].error_summary
|
string | null | 失败或回执超时的原因。 |
data.billing
|
object | null | 计费明细,不计费的批次为 null。 |
data.billing.status
|
string | 计费状态,见下方「计费状态」。 |
data.billing.status_label
|
string | 计费状态的中文名称。 |
data.billing.chars_per_message
|
integer | 每个号码的计费字数(含【签名】)。 |
data.billing.segments_per_message
|
integer | 每个号码的计费条数。 |
data.billing.phone_count
|
integer | 计费的号码数。 |
data.billing.total_segments
|
integer | 本批次的总计费条数。 |
data.billing.package_segments
|
integer | 从流量包扣除的条数。 |
data.billing.balance_segments
|
integer | 从余额扣费的条数。 |
data.billing.unit_price
|
string | null | 余额扣费单价(元/条);全部由流量包支付时为 null。 |
data.billing.balance_amount
|
string | 余额扣费金额(元)。 |
data.billing.refunded_segments
|
integer | 已退还的条数。 |
data.billing.refunded_amount
|
string | 已退还的余额金额(元)。 |
批次状态(data.status)
| 取值 | 名称 | 说明 |
|---|---|---|
pending
|
处理中 | 已收到请求,正在提交到通道。 |
submitted
|
已提交 | 已提交到通道,等待运营商状态回执;每个号码的结果以 messages[].status 为准。 |
completed
|
已完成 | 全部号码都已得到最终状态(已送达、发送失败或回执超时)。 |
failed
|
发送失败 | 确定未发出,费用已全额退还;原因见 error_code 与 error_summary。 |
timeout
|
发送超时 | 通道响应异常或超时,无法确定是否已发出;计费状态为待核实,由平台核实后处理(核实未发出时改为 failed 并退费)。 |
号码状态(data.messages[].status)
| 取值 | 名称 | 说明 |
|---|---|---|
submitted
|
已提交 | 已提交到通道,等待运营商状态回执。 |
sending
|
发送中 | 运营商正在下发。 |
sent
|
已发送 | 运营商已处理,等待最终回执。 |
delivered
|
已送达 | 运营商回执发送成功(provider_status 为 0)。 |
failed
|
发送失败 | 未能送达或通道未受理,原因见 error_code 与 error_summary。 |
timeout
|
回执超时 | 提交后超过 6 小时未收到运营商状态回执;之后如果收到回执,会更新为 delivered 或 failed。 |
计费状态(data.billing.status)
| 取值 | 名称 | 说明 |
|---|---|---|
charged
|
已扣费 | 已按本批次的计费条数扣费。 |
partially_refunded
|
部分退还 | 部分号码未被通道受理,对应费用已退还。 |
refunded
|
已全额退还 | 本批次未发出,费用已全部退还。 |
pending_review
|
待核实 | 无法确定是否已发出,扣费暂不退还,由平台核实后处理。 |
settled
|
已确认扣费 | 平台核实后确认扣费。 |
判断每个号码是否送达请以 messages[].status 为准。批次 status 为 completed 表示全部号码都已有最终结果,可以停止查询;号码超过 6 小时未收到回执时记为 timeout,批次随之变为 completed。
响应示例
{
"ok": true,
"data": {
"batch_id": 1024,
"batch_uuid": "9b2f6c1e-3d4a-4f8b-9c0d-1e2f3a4b5c6d",
"status": "completed",
"total_count": 2,
"accepted_count": 2,
"rejected_count": 0,
"submitted_at": "2026-10-08T08:00:01.120586Z",
"completed_at": "2026-10-08T08:00:06.031000Z",
"error_code": null,
"error_summary": null,
"messages": [
{
"id": 5001,
"remote_message_id": "2026100816000100001",
"status": "delivered",
"provider_status": "0",
"sent_at": "2026-10-08T08:00:01.120586Z",
"delivered_at": "2026-10-08T08:00:04.513000Z",
"failed_at": null,
"error_code": null,
"error_summary": null
},
{
"id": 5002,
"remote_message_id": "2026100816000100002",
"status": "failed",
"provider_status": "CR:0217",
"sent_at": "2026-10-08T08:00:01.120586Z",
"delivered_at": null,
"failed_at": "2026-10-08T08:00:06.027000Z",
"error_code": "CR:0217",
"error_summary": "运营商侧针对文案中含有网址和联系方式的内容进行管控拦截"
}
],
"billing": {
"status": "charged",
"status_label": "已扣费",
"chars_per_message": 34,
"segments_per_message": 1,
"phone_count": 2,
"total_segments": 2,
"package_segments": 1,
"balance_segments": 1,
"unit_price": "0.0450",
"balance_amount": "0.0450",
"refunded_segments": 0,
"refunded_amount": "0.0000"
}
}
}
短信发送 API
查询余额
/api/open/v1/account/balance
查询应用所属账户的余额与流量包可用条数。发送时先扣流量包,不足部分按通道单价从余额扣除。
请求参数
无。
响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
ok
|
boolean | 查询成功时为 true。 |
data
|
object | 账户信息。 |
data.balance
|
string | 账户余额(元,4 位小数)。 |
data.currency
|
string | 币种,固定为 CNY。 |
data.available_segments
|
integer | 生效中流量包的剩余可用条数,不含余额可支付的条数。 |
响应示例
{
"ok": true,
"data": {
"balance": "149.0000",
"currency": "CNY",
"available_segments": 1000
}
}
短信服务
短信服务错误码
失败时响应体为 JSON,包含中文 message 与 error_code;字段校验失败(HTTP 422)另有 errors 字段。请按 HTTP 状态码与 error_code 判断,不要依赖 message 的具体文字。缺少签名、签名错误、应用停用、IP 白名单、限流等鉴权与访问控制错误码(401 / 403 / 429)对所有接口通用,见 状态码与错误码。
短信计费
| error_code | HTTP 状态码 | 说明 |
|---|---|---|
insufficient_funds
|
402 | 余额与流量包额度合计不足,本次未发送、未扣费。 |
price_not_set
|
402 | 流量包额度不足,且通道未设置单价、无法用余额补足,本次未发送、未扣费。 |
短信发送
| error_code | HTTP 状态码 | 说明 |
|---|---|---|
kyc_required
|
403 | 平台开启了强制实名认证,账户尚未完成实名认证时短信与人脸实名认证的全部接口都返回该错误;在控制台「实名认证」完成个人实名认证后即可调用。本次未发送、未扣费。 |
param_not_allowed
|
422 | 模板参数不符合平台内容规范:验证码模板的参数须为 4–8 位数字或字母;共享模板的参数不超过 35 个字符,不能包含网址、【】或换行。本次未发送、未扣费。 |
request_in_progress
|
409 | 相同 Idempotency-Key 的首次请求仍在处理中,请稍后按 batch_uuid 查询。 |
result_unknown
|
409 | 相同 Idempotency-Key 的请求超过 15 分钟仍未完成,结果待核实,请勿换新的 Key 重发。 |
settlement_pending
|
409 | 短信可能已经提交,但结果登记失败;请勿重复发送,稍后按 batch_uuid 查询。 |
send_failed
|
409 | 发送失败,未发出部分的费用已自动退还。 |
| 其他错误码 | 409 | 通道返回的原始错误码(如 timeout、network),含义见 message;结果无法确定时计费状态为待核实。 |
internal_error
|
500 | 平台内部异常,短信未提交到通道;保持 Idempotency-Key 不变稍后重试即可。 |
短信查询
| error_code | HTTP 状态码 | 说明 |
|---|---|---|
batch_not_found
|
404 | 短信批次不存在,或不是本应用发出的批次。 |
资质 / 签名 / 模板申请
| error_code | HTTP 状态码 | 说明 |
|---|---|---|
invalid_parameter
|
422 | 业务校验未通过,如资质不可用、签名重名、材料凭证失效、验证码错误;errors 按字段给出原因。字段格式错误时为标准 422(message + errors)。 |
invalid_state
|
409 | 当前状态不允许该操作,如平台已开始审核后撤回、审核中修改。 |
limit_reached
|
409 | 超过数量或每日提交次数上限。 |
request_rejected
|
409 | 模板申请被拒绝(状态不允许、名额或每日次数已满),原因见 message。 |
qualification_not_found / signature_not_found / template_not_found
|
404 | 记录不存在或不属于本账号。 |
unsupported_media_type
|
415 | 上传接口的 Content-Type 不是 image/jpeg 或 image/png。 |
payload_too_large
|
413 | 图片超过 10MB。 |
rate_limited
|
429 | 超过每分钟 20 次、每小时上传 60 张或验证码发送上限,按 message 或 Retry-After 等待后重试。 |
service_unavailable
|
503 | 平台暂未开通资质提交或验证码通道,请联系平台客服。 |
upstream_error
|
502 | 短信通道服务暂时异常,请稍后重试。 |
人脸实名认证(基础版)
产品说明
人脸实名认证(基础版)基于姓名、证件号码和一张人脸照片,与公安权威库进行人证比对,判断照片中的人是否为证件本人。同一次调用内完成质量检测、静默活体检测(识别翻拍)与合成图检测,接口同步返回结果。对防冒用要求更高时,可使用 增强版(录制动作视频做视频活体检测)。
基础版的接口路径均以 /api/open/v1/face/basic/ 开头,只接受类型为「基础版」的人脸实名认证应用。
- 认证方式
- 姓名 + 证件号码 + 一张人脸照片,与公安权威库进行人证比对
- 附带检测
- 同一次调用内完成质量检测、静默活体检测(识别翻拍)与合成图检测
- 接入方式
- 开放接口自行采集照片(同步返回结果),或创建会话后使用平台 H5 采集页(查询或异步通知获取结果)
- 数据保存
- 平台不保存姓名、证件号码与人脸图片,只保存认证结果、分数与计费信息
- 通过规则
- verify_status 为 0 且 score ≥ threshold(默认 80)时认证通过
- 签名鉴权
- 与短信服务完全相同,使用「人脸实名认证」业务线的应用
安全边界
通过开放接口自行采集时,照片由调用方采集后上传,平台只对收到的照片做检测与比对,无法保证照片的来源(如是否为本人现场拍摄)。调用前请取得用户对人脸信息处理的单独同意,并自行负责采集、传输环节的安全。使用 H5 采集页 时,由平台采集页现场拍摄并征得用户授权。
- 只能由你的服务端调用,AppSecret 不能出现在网页、小程序或 App 中。
- 平台不保存姓名、证件号码和人脸图片,认证记录只保存结果、分数、计费信息与调用来源。
- 使用「人脸实名认证服务 › 我的应用」中类型为「基础版」的应用调用;短信应用调用会返回 HTTP 403 app_service_mismatch,增强版应用调用会返回 HTTP 403 app_product_mismatch。
人脸实名认证(基础版)
计费说明
- 仅认证通过计费,未通过与异常不计费。每次认证通过计 1 次:先扣生效中的人脸实名认证次数包(先到期的先用),没有可用次数时按平台单价从账户余额扣除。
- 次数包已用完且余额不足时返回 HTTP 402 insufficient_funds;次数包已用完且平台未设置单价时返回 HTTP 402 price_not_set。两种情况都不会发起认证,也不扣费。
- 调用前会先预扣 1 次,认证未通过或调用异常时自动退还到原来的次数包或余额。响应中的 charged、charge_source、charge_amount 为最终的计费结果。
- 传入 biz_no 后重复提交,直接返回第一次的认证结果(idempotent 为 true),不会重复认证,也不会重复扣费;同一单号仍在处理中时返回 HTTP 409 processing。
- H5 采集页的一个认证会话内可多次尝试,最多计费 1 次(认证通过的那一次)。
计费的情况
仅认证通过计费,未通过与异常不计费:
- 认证通过(result_code 为 passed)。
不计费的情况
除认证通过外,其余情况都不计费,已预扣的费用会自动退还:
- 认证未通过:人脸比对分数低于通过阈值、证件号码与姓名不匹配、权威库查无、身份核验未通过;
- 检测未通过:未检测到人脸,质量检测、活体检测、合成图检测未通过;
- 参数校验失败(HTTP 422),以及余额不足、未定价、服务未开放等在发起认证之前就返回的错误;
- 认证服务异常或超时(HTTP 502);
- 数据源风险拦截(同一身份信息短时间内重复认证);
- 证件号码、姓名或图片格式被认证服务判定为不合法。
每个结果码是否计费,见 结果码与错误码。
人脸实名认证(基础版)
接入建议
- 图片建议先压缩到约 1280×720 的 JPEG,大小控制在几百 KB 以内,可以明显缩短上传与比对时间。
- 人脸认证需要调用公安权威库,HTTP 客户端的超时时间建议不少于 15 秒。
- 每次认证都传入 biz_no(如你的业务订单号)。遇到 HTTP 502、超时或网络错误时,用同一个 biz_no 重试:如果第一次其实已经完成,会直接返回第一次的结果,不会重复扣费。
- 重试时仍要重新生成 X-Timestamp 与 X-Nonce 并重新签名。
- 认证结果只能以你的服务端从本接口拿到的结果为准,不要信任客户端上报的认证结果。
- 姓名、证件号码与照片只在内存中转发,不要写入日志;如果业务需要留存,请自行加密保存并遵守相关法律法规。
人脸实名认证(基础版)
示例代码
基础版:查询余额、发起认证与查询结果
签名实现与短信服务的示例代码相同,依次演示查询余额与次数、发起认证、查询认证结果。把「你的 AppId」「你的 AppSecret」替换为「人脸实名认证服务 › 我的应用」中的真实值,把姓名、证件号码与 face.jpg 换成实际数据即可运行。
PHP
<?php
// 人脸实名认证(基础版)调用示例(PHP 7.4 及以上,需要 curl 扩展)
// 使用「人脸实名认证服务 › 我的应用」中创建的基础版应用;签名方式与短信服务完全相同
const APP_ID = '你的 AppId';
const APP_SECRET = '你的 AppSecret'; // 只保存在服务端,切勿写进网页、小程序或 App
const API_ORIGIN = 'https://am.siyun223.com';
const API_PREFIX = '/api/open/v1';
/**
* 签名并发送请求,返回 [HTTP 状态码, 响应数组]
*/
function openApiRequest(string $method, string $path, ?array $payload = null): array
{
$method = strtoupper($method);
$fullPath = API_PREFIX . $path;
$body = $payload === null ? '' : json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$timestamp = (string) time();
$nonce = bin2hex(random_bytes(16));
// 待签名串:方法、路径、规范查询串(无查询参数时为空)、时间戳、随机串、请求体 SHA256,用 \n 连接
$stringToSign = implode("\n", [$method, $fullPath, '', $timestamp, $nonce, hash('sha256', $body)]);
$signature = hash_hmac('sha256', $stringToSign, APP_SECRET);
$headers = [
'Accept: application/json',
'X-App-Id: ' . APP_ID,
'X-Timestamp: ' . $timestamp,
'X-Nonce: ' . $nonce,
'X-Signature: ' . $signature,
];
$ch = curl_init(API_ORIGIN . $fullPath);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
curl_setopt($ch, CURLOPT_TIMEOUT, 20); // 人脸认证需要调用权威库,超时时间建议不少于 15 秒
if ($payload !== null) {
$headers[] = 'Content-Type: application/json';
curl_setopt($ch, CURLOPT_POSTFIELDS, $body); // 发送的必须是参与签名的同一个字符串
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$response = curl_exec($ch);
if ($response === false) {
throw new RuntimeException('请求失败:' . curl_error($ch));
}
return [(int) curl_getinfo($ch, CURLINFO_HTTP_CODE), json_decode($response, true)];
}
// 1. 查询余额与可用次数
[$status, $result] = openApiRequest('GET', '/face/basic/account/balance');
echo $status, ' ', json_encode($result, JSON_UNESCAPED_UNICODE), PHP_EOL;
// 2. 发起认证:图片建议先压缩到约 1280×720 的 JPEG;biz_no 建议使用业务单号,重试时保持不变
[$status, $result] = openApiRequest('POST', '/face/basic/verify', [
'name' => '张三',
'id_card_number' => '11010519491231002X',
'image' => base64_encode(file_get_contents(__DIR__ . '/face.jpg')), // 不带 data:image 前缀
'biz_no' => 'kyc-20261008-0001',
'liveness_control' => 'NORMAL',
'spoofing_control' => 'NORMAL',
]);
echo $status, ' ', json_encode($result, JSON_UNESCAPED_UNICODE), PHP_EOL;
if ($status === 200) {
// 是否通过以 data.passed 为准,未通过的原因见 data.result_code 与 data.result_msg
echo $result['data']['passed'] ? '认证通过' : '认证未通过:' . $result['data']['result_msg'], PHP_EOL;
// 3. 按平台流水号查询认证结果
[$status, $result] = openApiRequest('GET', '/face/basic/verifications/' . $result['data']['verify_no']);
echo $status, ' ', json_encode($result, JSON_UNESCAPED_UNICODE), PHP_EOL;
}
Python
# 人脸实名认证(基础版)调用示例(Python 3.6 及以上,只使用标准库)
# 使用「人脸实名认证服务 › 我的应用」中创建的基础版应用;签名方式与短信服务完全相同
import base64
import hashlib
import hmac
import json
import secrets
import time
import urllib.error
import urllib.request
APP_ID = "你的 AppId"
APP_SECRET = "你的 AppSecret" # 只保存在服务端,切勿写进网页、小程序或 App
API_ORIGIN = "https://am.siyun223.com"
API_PREFIX = "/api/open/v1"
def open_api_request(method, path, payload=None):
"""签名并发送请求,返回 (HTTP 状态码, 响应内容)"""
method = method.upper()
full_path = API_PREFIX + path
body = b"" if payload is None else json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
timestamp = str(int(time.time()))
nonce = secrets.token_hex(16)
# 待签名串:方法、路径、规范查询串(无查询参数时为空)、时间戳、随机串、请求体 SHA256,用 \n 连接
string_to_sign = "\n".join([method, full_path, "", timestamp, nonce, hashlib.sha256(body).hexdigest()])
signature = hmac.new(APP_SECRET.encode("utf-8"), string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
headers = {
"Accept": "application/json",
"X-App-Id": APP_ID,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature": signature,
}
if payload is not None:
headers["Content-Type"] = "application/json"
request = urllib.request.Request(
API_ORIGIN + full_path,
data=body if payload is not None else None, # 发送的必须是参与签名的同一份字节
headers=headers,
method=method,
)
try:
# 人脸认证需要调用权威库,超时时间建议不少于 15 秒
with urllib.request.urlopen(request, timeout=20) as response:
return response.status, _parse(response.read())
except urllib.error.HTTPError as error:
return error.code, _parse(error.read())
def _parse(raw):
try:
return json.loads(raw.decode("utf-8"))
except ValueError:
return raw.decode("utf-8", "replace")
if __name__ == "__main__":
# 1. 查询余额与可用次数
print(open_api_request("GET", "/face/basic/account/balance"))
# 2. 发起认证:图片建议先压缩到约 1280×720 的 JPEG;biz_no 建议使用业务单号,重试时保持不变
with open("face.jpg", "rb") as image_file:
image = base64.b64encode(image_file.read()).decode("ascii") # 不带 data:image 前缀
status, result = open_api_request("POST", "/face/basic/verify", {
"name": "张三",
"id_card_number": "11010519491231002X",
"image": image,
"biz_no": "kyc-20261008-0001",
"liveness_control": "NORMAL",
"spoofing_control": "NORMAL",
})
print(status, result)
if status == 200 and isinstance(result, dict):
data = result["data"]
# 是否通过以 data.passed 为准,未通过的原因见 data.result_code 与 data.result_msg
print("认证通过" if data["passed"] else "认证未通过:" + data["result_msg"])
# 3. 按平台流水号查询认证结果
print(open_api_request("GET", "/face/basic/verifications/" + data["verify_no"]))
Java
// 人脸实名认证(基础版)调用示例(Java 11 及以上,只使用 JDK 标准库)
// 使用「人脸实名认证服务 › 我的应用」中创建的基础版应用;签名方式与短信服务完全相同
// 保存为 FaceVerifyDemo.java,执行:javac -encoding UTF-8 FaceVerifyDemo.java && java FaceVerifyDemo
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.time.Duration;
import java.util.Base64;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class FaceVerifyDemo {
private static final String APP_ID = "你的 AppId";
private static final String APP_SECRET = "你的 AppSecret"; // 只保存在服务端,切勿写进网页、小程序或 App
private static final String API_ORIGIN = "https://am.siyun223.com";
private static final String API_PREFIX = "/api/open/v1";
private static final HttpClient CLIENT = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
private static final SecureRandom RANDOM = new SecureRandom();
public static void main(String[] args) throws Exception {
// 1. 查询余额与可用次数
HttpResponse<String> balance = request("GET", "/face/basic/account/balance", null);
System.out.println(balance.statusCode() + " " + balance.body());
// 2. 发起认证:图片建议先压缩到约 1280×720 的 JPEG;biz_no 建议使用业务单号,重试时保持不变
// 请求体可以用 Jackson、Gson 等生成,签名与发送必须使用同一个字符串
String image = Base64.getEncoder().encodeToString(Files.readAllBytes(Path.of("face.jpg"))); // 不带 data:image 前缀
String body = "{\"name\":\"张三\",\"id_card_number\":\"11010519491231002X\","
+ "\"image\":\"" + image + "\",\"biz_no\":\"kyc-20261008-0001\","
+ "\"liveness_control\":\"NORMAL\",\"spoofing_control\":\"NORMAL\"}";
HttpResponse<String> verified = request("POST", "/face/basic/verify", body);
System.out.println(verified.statusCode() + " " + verified.body());
// 3. 按平台流水号查询认证结果(示例用正则提取 verify_no,生产代码请用 JSON 库解析,并以 data.passed 判断是否通过)
Matcher matcher = Pattern.compile("\"verify_no\":\"(FV\\d{22})\"").matcher(verified.body());
if (verified.statusCode() == 200 && matcher.find()) {
HttpResponse<String> record = request("GET", "/face/basic/verifications/" + matcher.group(1), null);
System.out.println(record.statusCode() + " " + record.body());
}
}
/** 签名并发送请求 */
static HttpResponse<String> request(String method, String path, String body) throws Exception {
String fullPath = API_PREFIX + path;
String payload = body == null ? "" : body;
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
String nonce = randomHex(16);
// 待签名串:方法、路径、规范查询串(无查询参数时为空)、时间戳、随机串、请求体 SHA256,用 \n 连接
String stringToSign = String.join("\n", method, fullPath, "", timestamp, nonce, sha256Hex(payload));
HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(API_ORIGIN + fullPath))
.timeout(Duration.ofSeconds(20)) // 人脸认证需要调用权威库,超时时间建议不少于 15 秒
.header("Accept", "application/json")
.header("X-App-Id", APP_ID)
.header("X-Timestamp", timestamp)
.header("X-Nonce", nonce)
.header("X-Signature", hmacSha256Hex(APP_SECRET, stringToSign));
if (body == null) {
builder.method(method, HttpRequest.BodyPublishers.noBody());
} else {
builder.header("Content-Type", "application/json")
.method(method, HttpRequest.BodyPublishers.ofString(payload, StandardCharsets.UTF_8));
}
return CLIENT.send(builder.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
}
static String sha256Hex(String value) throws Exception {
return toHex(MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8)));
}
static String hmacSha256Hex(String secret, String value) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
return toHex(mac.doFinal(value.getBytes(StandardCharsets.UTF_8)));
}
static String randomHex(int bytes) {
byte[] buffer = new byte[bytes];
RANDOM.nextBytes(buffer);
return toHex(buffer);
}
static String toHex(byte[] bytes) {
StringBuilder hex = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
hex.append(String.format("%02x", b));
}
return hex.toString();
}
}
Node.js
// 人脸实名认证(基础版)调用示例(Node.js 18 及以上,使用内置 fetch 与 crypto,无需安装依赖)
// 使用「人脸实名认证服务 › 我的应用」中创建的基础版应用;签名方式与短信服务完全相同
// 保存为 face-verify-demo.mjs,执行:node face-verify-demo.mjs
import { createHash, createHmac, randomBytes } from 'node:crypto';
import { readFileSync } from 'node:fs';
const APP_ID = '你的 AppId';
const APP_SECRET = '你的 AppSecret'; // 只保存在服务端,切勿写进网页、小程序或 App
const API_ORIGIN = 'https://am.siyun223.com';
const API_PREFIX = '/api/open/v1';
/** 签名并发送请求,返回 { status, data } */
async function openApiRequest(method, path, payload = null) {
const fullPath = API_PREFIX + path;
const body = payload === null ? '' : JSON.stringify(payload);
const timestamp = String(Math.floor(Date.now() / 1000));
const nonce = randomBytes(16).toString('hex');
// 待签名串:方法、路径、规范查询串(无查询参数时为空)、时间戳、随机串、请求体 SHA256,用 \n 连接
const bodyHash = createHash('sha256').update(body, 'utf8').digest('hex');
const stringToSign = [method, fullPath, '', timestamp, nonce, bodyHash].join('\n');
const signature = createHmac('sha256', APP_SECRET).update(stringToSign, 'utf8').digest('hex');
const headers = {
Accept: 'application/json',
'X-App-Id': APP_ID,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': signature,
};
if (payload !== null) {
headers['Content-Type'] = 'application/json';
}
const response = await fetch(API_ORIGIN + fullPath, {
method,
headers,
body: payload === null ? undefined : body, // 发送的必须是参与签名的同一个字符串
signal: AbortSignal.timeout(20000), // 人脸认证需要调用权威库,超时时间建议不少于 15 秒
});
const text = await response.text();
let data = text;
try {
data = JSON.parse(text);
} catch {
// 非 JSON 响应原样返回
}
return { status: response.status, data };
}
// 1. 查询余额与可用次数
console.log(JSON.stringify(await openApiRequest('GET', '/face/basic/account/balance')));
// 2. 发起认证:图片建议先压缩到约 1280×720 的 JPEG;biz_no 建议使用业务单号,重试时保持不变
const verified = await openApiRequest('POST', '/face/basic/verify', {
name: '张三',
id_card_number: '11010519491231002X',
image: readFileSync('face.jpg').toString('base64'), // 不带 data:image 前缀
biz_no: 'kyc-20261008-0001',
liveness_control: 'NORMAL',
spoofing_control: 'NORMAL',
});
console.log(JSON.stringify(verified));
if (verified.status === 200 && verified.data?.data) {
const result = verified.data.data;
// 是否通过以 data.passed 为准,未通过的原因见 data.result_code 与 data.result_msg
console.log(result.passed ? '认证通过' : `认证未通过:${result.result_msg}`);
// 3. 按平台流水号查询认证结果
console.log(JSON.stringify(await openApiRequest('GET', `/face/basic/verifications/${result.verify_no}`)));
}
Shell
#!/usr/bin/env bash
# 人脸实名认证(基础版)调用示例(bash、curl、openssl 与 base64)
# 使用「人脸实名认证服务 › 我的应用」中创建的基础版应用;签名方式与短信服务完全相同
# 保存为 face-verify-demo.sh,执行:bash face-verify-demo.sh
set -euo pipefail
APP_ID='你的 AppId'
APP_SECRET='你的 AppSecret' # 只保存在服务端,切勿写进网页、小程序或 App
API_ORIGIN='https://am.siyun223.com'
API_PREFIX='/api/open/v1'
# 用法:open_api_request 方法 路径 [JSON 请求体文件]
# 图片的 Base64 较长,请求体写入文件后再签名和发送,避免超出命令行参数长度
open_api_request() {
local method="$1" path="${API_PREFIX}$2" body_file="${3:-}"
local timestamp nonce body_hash signature
timestamp=$(date +%s)
nonce=$(openssl rand -hex 16)
if [ -n "$body_file" ]; then
body_hash=$(openssl dgst -sha256 -hex < "$body_file" | awk '{print $NF}')
else
body_hash=$(printf '' | openssl dgst -sha256 -hex | awk '{print $NF}')
fi
# 待签名串:方法、路径、规范查询串(无查询参数时为空行)、时间戳、随机串、请求体 SHA256
signature=$(printf '%s\n%s\n\n%s\n%s\n%s' "$method" "$path" "$timestamp" "$nonce" "$body_hash" \
| openssl dgst -sha256 -hmac "$APP_SECRET" -hex | awk '{print $NF}')
# 人脸认证需要调用权威库,超时时间建议不少于 15 秒
local args=(-sS -X "$method" "${API_ORIGIN}${path}" --max-time 20
-H 'Accept: application/json'
-H "X-App-Id: ${APP_ID}"
-H "X-Timestamp: ${timestamp}"
-H "X-Nonce: ${nonce}"
-H "X-Signature: ${signature}"
-w '\nHTTP %{http_code}\n')
if [ -n "$body_file" ]; then
args+=(-H 'Content-Type: application/json' --data-binary "@${body_file}")
fi
curl "${args[@]}"
}
# 1. 查询余额与可用次数
open_api_request GET /face/basic/account/balance
# 2. 发起认证:图片建议先压缩到约 1280×720 的 JPEG;biz_no 建议使用业务单号,重试时保持不变
body_file=$(mktemp)
trap 'rm -f "$body_file"' EXIT
image=$(base64 < face.jpg | tr -d '\n') # 不带 data:image 前缀
printf '{"name":"%s","id_card_number":"%s","image":"%s","biz_no":"%s","liveness_control":"NORMAL","spoofing_control":"NORMAL"}' \
'张三' '11010519491231002X' "$image" 'kyc-20261008-0001' > "$body_file"
open_api_request POST /face/basic/verify "$body_file"
# 3. 按平台流水号查询认证结果:把认证接口返回的 data.verify_no 填到路径末尾
# open_api_request GET /face/basic/verifications/替换为verify_no
H5 采集页:创建认证会话与验签异步通知
演示创建认证会话并取得 h5_url,以及在 notify_url 对应的接口中校验通知签名。通知的签名算法与调用开放接口完全相同,路径取 notify_url 的 path。
PHP
<?php
// 人脸实名认证(基础版)H5 采集页示例(PHP 7.4 及以上,需要 curl 扩展)
// 一、创建认证会话,把 h5_url 交给用户打开;二、在 notify_url 对应的接口中验签异步通知
const APP_ID = '你的 AppId';
const APP_SECRET = '你的 AppSecret'; // 只保存在服务端,切勿写进网页、小程序或 App
const API_ORIGIN = 'https://am.siyun223.com';
const API_PREFIX = '/api/open/v1';
/** 签名并发送请求,返回 [HTTP 状态码, 响应数组](与人脸实名认证示例相同) */
function openApiRequest(string $method, string $path, ?array $payload = null): array
{
$fullPath = API_PREFIX . $path;
$body = $payload === null ? '' : json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$timestamp = (string) time();
$nonce = bin2hex(random_bytes(16));
$stringToSign = implode("\n", [$method, $fullPath, '', $timestamp, $nonce, hash('sha256', $body)]);
$headers = [
'Accept: application/json',
'Content-Type: application/json',
'X-App-Id: ' . APP_ID,
'X-Timestamp: ' . $timestamp,
'X-Nonce: ' . $nonce,
'X-Signature: ' . hash_hmac('sha256', $stringToSign, APP_SECRET),
];
$ch = curl_init(API_ORIGIN . $fullPath);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => $headers,
]);
if ($payload !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
}
$response = curl_exec($ch);
if ($response === false) {
throw new RuntimeException('请求失败:' . curl_error($ch));
}
return [(int) curl_getinfo($ch, CURLINFO_HTTP_CODE), json_decode($response, true)];
}
// ==================== 一、创建认证会话 ====================
// return_url、notify_url 的域名必须是应用登记的业务域名或其子域名
[$status, $result] = openApiRequest('POST', '/face/basic/sessions', [
'name' => '张三',
'id_card_number' => '11010519491231002X',
'biz_no' => 'kyc-20261008-0101', // 每个会话使用新的业务单号
'return_url' => 'https://www.example.com/kyc/return',
'notify_url' => 'https://api.example.com/kyc/notify',
'max_attempts' => 3,
]);
if ($status === 201) {
// 把 h5_url 交给用户打开(跳转、短信或二维码),保存 session_no 用于查询
echo $result['data']['session_no'], ' ', $result['data']['h5_url'], PHP_EOL;
} else {
echo $status, ' ', $result['error_code'] ?? '', ' ', $result['message'] ?? '', PHP_EOL;
}
// ==================== 二、验签异步通知(notify_url 对应的接口) ====================
/**
* 校验平台发来的通知,通过返回解析后的数组,失败返回 null。
* 路径取 notify_url 的 path;notify_url 带查询参数时,按规范查询串参与签名(见「签名方法」),这里假设不带。
*/
function verifyFaceNotify(string $path, array $headers, string $rawBody): ?array
{
$timestamp = $headers['x-timestamp'] ?? '';
$nonce = $headers['x-nonce'] ?? '';
$signature = $headers['x-signature'] ?? '';
if (($headers['x-app-id'] ?? '') !== APP_ID || abs(time() - (int) $timestamp) > 300) {
return null;
}
$stringToSign = implode("\n", ['POST', $path, '', $timestamp, $nonce, hash('sha256', $rawBody)]);
if (!hash_equals(hash_hmac('sha256', $stringToSign, APP_SECRET), $signature)) {
return null;
}
// 建议再用 Redis 等记录 nonce,5 分钟内重复的直接拒绝,防止重放
return json_decode($rawBody, true);
}
// 在通知接口中:
// $headers = array_change_key_case(getallheaders(), CASE_LOWER);
// $notice = verifyFaceNotify('/kyc/notify', $headers, file_get_contents('php://input'));
// if ($notice === null) { http_response_code(401); exit; }
// $data = $notice['data']; // 按 session_no 去重;data.passed 为 true 表示认证通过
// http_response_code(200); // 返回任意 2xx 即视为通知成功
Python
# 人脸实名认证(基础版)H5 采集页示例(Python 3.6 及以上,只使用标准库)
# 一、创建认证会话,把 h5_url 交给用户打开;二、在 notify_url 对应的接口中验签异步通知
import hashlib
import hmac
import json
import secrets
import time
import urllib.error
import urllib.request
APP_ID = "你的 AppId"
APP_SECRET = "你的 AppSecret" # 只保存在服务端,切勿写进网页、小程序或 App
API_ORIGIN = "https://am.siyun223.com"
API_PREFIX = "/api/open/v1"
def sign(method, path, timestamp, nonce, body):
"""待签名串:方法、路径、规范查询串(无查询参数时为空)、时间戳、随机串、请求体 SHA256"""
string_to_sign = "\n".join([method, path, "", timestamp, nonce, hashlib.sha256(body).hexdigest()])
return hmac.new(APP_SECRET.encode("utf-8"), string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
def open_api_request(method, path, payload=None):
full_path = API_PREFIX + path
body = b"" if payload is None else json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
timestamp = str(int(time.time()))
nonce = secrets.token_hex(16)
request = urllib.request.Request(API_ORIGIN + full_path, data=body if payload is not None else None, method=method, headers={
"Accept": "application/json",
"Content-Type": "application/json",
"X-App-Id": APP_ID,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature": sign(method, full_path, timestamp, nonce, body),
})
try:
with urllib.request.urlopen(request, timeout=15) as response:
return response.status, json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as error:
return error.code, json.loads(error.read().decode("utf-8"))
def verify_notify(path, headers, raw_body):
"""校验平台发来的通知(headers 的键为小写),通过返回解析后的 dict,失败返回 None。
路径取 notify_url 的 path;notify_url 带查询参数时按规范查询串参与签名,这里假设不带。"""
timestamp = headers.get("x-timestamp", "")
if headers.get("x-app-id") != APP_ID or not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
return None
expected = sign("POST", path, timestamp, headers.get("x-nonce", ""), raw_body)
if not hmac.compare_digest(expected, headers.get("x-signature", "")):
return None
# 建议再记录 nonce,5 分钟内重复的直接拒绝,防止重放
return json.loads(raw_body.decode("utf-8"))
if __name__ == "__main__":
# 一、创建认证会话:return_url、notify_url 的域名必须是应用登记的业务域名或其子域名
status, result = open_api_request("POST", "/face/basic/sessions", {
"name": "张三",
"id_card_number": "11010519491231002X",
"biz_no": "kyc-20261008-0101",
"return_url": "https://www.example.com/kyc/return",
"notify_url": "https://api.example.com/kyc/notify",
"max_attempts": 3,
})
if status == 201:
print(result["data"]["session_no"], result["data"]["h5_url"]) # 把 h5_url 交给用户打开
else:
print(status, result.get("error_code"), result.get("message"))
# 二、在通知接口中(以 Flask 为例):
# notice = verify_notify("/kyc/notify", {k.lower(): v for k, v in request.headers.items()}, request.get_data())
# if notice is None: return "", 401
# data = notice["data"] # 按 session_no 去重;data["passed"] 为 True 表示认证通过
# return "", 200 # 返回任意 2xx 即视为通知成功
Java
// 人脸实名认证(基础版)H5 采集页示例(Java 11 及以上,只使用 JDK 标准库)
// 一、创建认证会话,把 h5_url 交给用户打开;二、在 8080 端口接收异步通知并验签
// 保存为 FaceH5Demo.java,执行:javac -encoding UTF-8 FaceH5Demo.java && java FaceH5Demo
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpServer;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.time.Duration;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class FaceH5Demo {
static final String APP_ID = "你的 AppId";
static final String APP_SECRET = "你的 AppSecret"; // 只保存在服务端,切勿写进网页、小程序或 App
static final String API_ORIGIN = "https://am.siyun223.com";
static final String API_PREFIX = "/api/open/v1";
static String hex(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
sb.append(Character.forDigit((b >> 4) & 0xF, 16)).append(Character.forDigit(b & 0xF, 16));
}
return sb.toString();
}
// 待签名串:方法、路径、规范查询串(无查询参数时为空)、时间戳、随机串、请求体 SHA256
static String sign(String method, String path, String timestamp, String nonce, byte[] body) throws Exception {
String bodyHash = hex(MessageDigest.getInstance("SHA-256").digest(body));
String stringToSign = String.join("\n", method, path, "", timestamp, nonce, bodyHash);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(APP_SECRET.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
return hex(mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8)));
}
// 校验平台发来的通知;路径取 notify_url 的 path,notify_url 带查询参数时按规范查询串参与签名,这里假设不带
static boolean verifyNotify(String path, Headers headers, byte[] rawBody) throws Exception {
String timestamp = headers.getFirst("X-Timestamp");
String nonce = headers.getFirst("X-Nonce");
String signature = headers.getFirst("X-Signature");
if (!APP_ID.equals(headers.getFirst("X-App-Id")) || timestamp == null || !timestamp.matches("\\d{1,12}")
|| Math.abs(System.currentTimeMillis() / 1000 - Long.parseLong(timestamp)) > 300
|| nonce == null || signature == null) {
return false;
}
byte[] expected = sign("POST", path, timestamp, nonce, rawBody).getBytes(StandardCharsets.UTF_8);
// 常量时间比较;建议再记录 nonce,5 分钟内重复的直接拒绝,防止重放
return MessageDigest.isEqual(expected, signature.getBytes(StandardCharsets.UTF_8));
}
public static void main(String[] args) throws Exception {
// ==================== 一、创建认证会话 ====================
// return_url、notify_url 的域名必须是应用登记的业务域名或其子域名;biz_no 每个会话使用新的业务单号
String path = API_PREFIX + "/face/basic/sessions";
String json = "{\"name\":\"张三\",\"id_card_number\":\"11010519491231002X\",\"biz_no\":\"kyc-20261008-0101\","
+ "\"return_url\":\"https://www.example.com/kyc/return\","
+ "\"notify_url\":\"https://api.example.com/kyc/notify\",\"max_attempts\":3}";
byte[] body = json.getBytes(StandardCharsets.UTF_8);
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
byte[] random = new byte[16];
new SecureRandom().nextBytes(random);
String nonce = hex(random);
HttpRequest request = HttpRequest.newBuilder(URI.create(API_ORIGIN + path))
.timeout(Duration.ofSeconds(15))
.header("Accept", "application/json")
.header("Content-Type", "application/json")
.header("X-App-Id", APP_ID)
.header("X-Timestamp", timestamp)
.header("X-Nonce", nonce)
.header("X-Signature", sign("POST", path, timestamp, nonce, body))
.POST(HttpRequest.BodyPublishers.ofByteArray(body))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
// HTTP 201 时 data.h5_url 为采集页链接,交给用户打开;保存 data.session_no 用于查询
System.out.println(response.statusCode() + " " + response.body());
// ==================== 二、接收异步通知并验签 ====================
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
server.createContext("/kyc/notify", exchange -> {
byte[] raw = exchange.getRequestBody().readAllBytes();
boolean ok;
try {
ok = "POST".equals(exchange.getRequestMethod())
&& verifyNotify("/kyc/notify", exchange.getRequestHeaders(), raw);
} catch (Exception e) {
ok = false;
}
if (ok) {
// 按 session_no 去重;解析 JSON 后 data.passed 为 true 表示认证通过
System.out.println(new String(raw, StandardCharsets.UTF_8));
}
exchange.sendResponseHeaders(ok ? 200 : 401, -1); // 返回任意 2xx 即视为通知成功
exchange.close();
});
server.start();
}
}
Node.js
// 人脸实名认证(基础版)H5 采集页示例(Node.js 18 及以上,使用内置 fetch、crypto 与 http,无需安装依赖)
// 一、创建认证会话,把 h5_url 交给用户打开;二、在 notify_url 对应的接口中验签异步通知
import { createHash, createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
import { createServer } from 'node:http';
const APP_ID = '你的 AppId';
const APP_SECRET = '你的 AppSecret'; // 只保存在服务端,切勿写进网页、小程序或 App
const API_ORIGIN = 'https://am.siyun223.com';
const API_PREFIX = '/api/open/v1';
// 待签名串:方法、路径、规范查询串(无查询参数时为空)、时间戳、随机串、请求体 SHA256
function sign(method, path, timestamp, nonce, body) {
const bodyHash = createHash('sha256').update(body).digest('hex');
const stringToSign = [method, path, '', timestamp, nonce, bodyHash].join('\n');
return createHmac('sha256', APP_SECRET).update(stringToSign, 'utf8').digest('hex');
}
// ==================== 一、创建认证会话 ====================
const path = API_PREFIX + '/face/basic/sessions';
const body = JSON.stringify({
name: '张三',
id_card_number: '11010519491231002X',
biz_no: 'kyc-20261008-0101',
return_url: 'https://www.example.com/kyc/return', // 域名必须是应用登记的业务域名或其子域名
notify_url: 'https://api.example.com/kyc/notify',
max_attempts: 3,
});
const timestamp = String(Math.floor(Date.now() / 1000));
const nonce = randomBytes(16).toString('hex');
const response = await fetch(API_ORIGIN + path, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-App-Id': APP_ID,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': sign('POST', path, timestamp, nonce, body),
},
body,
signal: AbortSignal.timeout(15000),
});
const created = await response.json();
console.log(response.status, response.status === 201 ? created.data.h5_url : created); // 把 h5_url 交给用户打开
// ==================== 二、验签异步通知 ====================
// 路径取 notify_url 的 path;notify_url 带查询参数时按规范查询串参与签名,这里假设不带
function verifyNotify(notifyPath, headers, rawBody) {
const ts = String(headers['x-timestamp'] || '');
if (headers['x-app-id'] !== APP_ID || Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
return null;
}
const expected = Buffer.from(sign('POST', notifyPath, ts, String(headers['x-nonce'] || ''), rawBody));
const actual = Buffer.from(String(headers['x-signature'] || ''));
if (expected.length !== actual.length || !timingSafeEqual(expected, actual)) {
return null;
}
return JSON.parse(rawBody.toString('utf8')); // 建议再记录 nonce 防止重放
}
createServer((req, res) => {
const chunks = [];
req.on('data', (chunk) => chunks.push(chunk));
req.on('end', () => {
const notice = req.method === 'POST' && req.url === '/kyc/notify'
? verifyNotify('/kyc/notify', req.headers, Buffer.concat(chunks))
: null;
if (notice === null) {
res.writeHead(401).end();
return;
}
// 按 session_no 去重;notice.data.passed 为 true 表示认证通过
console.log(notice.event, notice.data.session_no, notice.data.status);
res.writeHead(200).end('ok'); // 返回任意 2xx 即视为通知成功
});
}).listen(8080);
Shell
#!/usr/bin/env bash
# 人脸实名认证(基础版)H5 采集页示例(bash、curl 与 openssl)
# 一、创建认证会话,把 h5_url 交给用户打开;二、verify_notify 演示通知接口如何验签
# 保存为 face-h5-demo.sh,执行:bash face-h5-demo.sh
set -euo pipefail
APP_ID='你的 AppId'
APP_SECRET='你的 AppSecret' # 只保存在服务端,切勿写进网页、小程序或 App
API_ORIGIN='https://am.siyun223.com'
API_PREFIX='/api/open/v1'
# 用法:sign 方法 路径 时间戳 随机串 请求体文件
# 待签名串:方法、路径、规范查询串(无查询参数时为空行)、时间戳、随机串、请求体 SHA256
sign() {
local body_hash
body_hash=$(openssl dgst -sha256 -hex < "$5" | awk '{print $NF}')
printf '%s\n%s\n\n%s\n%s\n%s' "$1" "$2" "$3" "$4" "$body_hash" \
| openssl dgst -sha256 -hmac "$APP_SECRET" -hex | awk '{print $NF}'
}
# ==================== 一、创建认证会话 ====================
# return_url、notify_url 的域名必须是应用登记的业务域名或其子域名;biz_no 每个会话使用新的业务单号
body_file=$(mktemp)
trap 'rm -f "$body_file"' EXIT
printf '{"name":"%s","id_card_number":"%s","biz_no":"%s","return_url":"%s","notify_url":"%s","max_attempts":3}' \
'张三' '11010519491231002X' 'kyc-20261008-0101' \
'https://www.example.com/kyc/return' 'https://api.example.com/kyc/notify' > "$body_file"
path="${API_PREFIX}/face/basic/sessions"
timestamp=$(date +%s)
nonce=$(openssl rand -hex 16)
# HTTP 201 时 data.h5_url 为采集页链接,交给用户打开;保存 data.session_no 用于查询
curl -sS -X POST "${API_ORIGIN}${path}" --max-time 15 \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-H "X-App-Id: ${APP_ID}" \
-H "X-Timestamp: ${timestamp}" \
-H "X-Nonce: ${nonce}" \
-H "X-Signature: $(sign POST "$path" "$timestamp" "$nonce" "$body_file")" \
--data-binary "@${body_file}" \
-w '\nHTTP %{http_code}\n'
# ==================== 二、验签异步通知 ====================
# 用法:verify_notify 通知路径 X-App-Id X-Timestamp X-Nonce X-Signature 原始请求体文件
# 通过返回 0,失败返回 1;路径取 notify_url 的 path,这里假设不带查询参数
verify_notify() {
local now expected
now=$(date +%s)
[ "$2" = "$APP_ID" ] || return 1
[[ "$3" =~ ^[0-9]{1,12}$ ]] || return 1
[ $(( now > $3 ? now - $3 : $3 - now )) -le 300 ] || return 1
expected=$(sign POST "$1" "$3" "$4" "$6")
[ "$expected" = "$5" ] # 建议再记录 nonce,5 分钟内重复的直接拒绝,防止重放
}
# 在通知接口中(请求头与原始请求体由你的 Web 服务提供),通过后返回任意 2xx 即视为通知成功:
# if verify_notify /kyc/notify "$HDR_APP_ID" "$HDR_TIMESTAMP" "$HDR_NONCE" "$HDR_SIGNATURE" notify_body.json; then
# echo '验签通过:按 session_no 去重,data.passed 为 true 表示认证通过'
# fi
人脸实名认证(基础版)
人脸实名认证
/api/open/v1/face/basic/verify
提交姓名、证件号码与一张人脸照片,同步返回人证比对、质量检测、活体检测与合成图检测的结果。除公共请求头外,需要携带 Content-Type: application/json;本接口不使用 Idempotency-Key,幂等通过请求体中的 biz_no 实现。
请求参数(JSON 请求体)
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
name
|
string | 是 | 姓名,2–30 个字符,支持汉字、字母与「·」(如少数民族姓名中的间隔号),首尾空格会被去掉。 |
id_card_number
|
string | 是 | 18 位居民身份证号码,会校验出生日期与校验位;末位 X 不区分大小写。 |
image
|
string | 是 | 人脸照片的 Base64 编码,不需要 data:image/...;base64, 前缀。只支持 JPG、PNG、BMP,文件小于 10M,分辨率不超过 1920×1080(横竖均可)。 |
biz_no
|
string | 否 | 业务单号,1–64 位,只能包含字母、数字、_ 和 -;同一应用内唯一,用于幂等,详见「计费说明」。 |
quality_control
|
string | 否 | 质量检测级别:LOW(低)、NORMAL(中,推荐)、HIGH(高),不区分大小写,不支持 NONE;不传时使用平台默认级别。 |
liveness_control
|
string | 否 | 活体检测级别,取值同上;不传时使用平台默认级别。 |
spoofing_control
|
string | 否 | 合成图检测级别,取值同上;不传时使用平台默认级别。 |
检测级别
quality_control、liveness_control、spoofing_control 的取值说明。级别越高越严格,攻击越难通过,但真人照片被误拒的比例也越高;一般场景建议使用 NORMAL。
| 取值 | 名称 | 质量检测 | 活体检测 | 合成图检测 |
|---|---|---|---|---|
LOW
|
低 | 要求最宽松 | 阈值 0.05,误拒率约万分之一 | 阈值 0.00023,误拒率约 5% |
NORMAL
|
中(推荐) | 适中 | 阈值 0.3,误拒率约千分之一 | 阈值 0.00048,误拒率约 1% |
HIGH
|
高 | 对遮挡、模糊、光照的要求最严格 | 阈值 0.9,误拒率约百分之一 | 阈值 0.00109,误拒率约 0.1% |
人证比对的通过阈值默认为 80,对应误识率约万分之一。
请求示例
{
"name": "张三",
"id_card_number": "11010519491231002X",
"image": "/9j/4AAQSkZJRgABAQAAAQABAAD...(Base64 内容省略)",
"biz_no": "kyc-20261008-0001",
"liveness_control": "NORMAL",
"spoofing_control": "NORMAL"
}
成功响应(HTTP 200)
认证通过或未通过都返回 HTTP 200 与 ok 为 true,请以 data.passed 判断是否通过:verify_status 为 0 且 score 不低于 threshold 时才算通过。
| 字段 | 类型 | 说明 |
|---|---|---|
ok
|
boolean | 请求成功时为 true。认证未通过也返回 true,是否通过以 data.passed 为准。 |
data.verify_no
|
string | 平台流水号,FV 开头,共 24 位,用于查询认证结果与反馈问题。 |
data.biz_no
|
string | null | 请求中传入的业务单号,未传时为 null。 |
data.source
|
string | 认证来源:api 开放接口直接认证,h5 H5 采集页。 |
data.product
|
string | 产品:face_basic 基础版,face_enhanced 增强版。 |
data.status
|
string | 认证状态,见下方「认证状态」。 |
data.status_label
|
string | 认证状态的中文名称。 |
data.passed
|
boolean | 是否认证通过,等同于 status 为 passed。 |
data.result_code
|
string | 结果码,见「结果码与错误码」。出于隐私保护,姓名与证件号不符、查无证件、人脸与证件照不一致统一返回 identity_failed。 |
data.result_msg
|
string | 结果的中文说明,可直接展示给用户。 |
data.score
|
number | null | 人脸与证件照的比对分数,0–100;只在认证通过时返回,未通过时为 null。 |
data.threshold
|
number | null | 本次使用的通过阈值,默认 80(对应误识率约万分之一)。 |
data.verify_status
|
integer | null | 权威库核验状态,认证通过时为 0;未通过时为 null(不返回具体原因)。 |
data.liveness_score
|
number | null | 活体检测分数,未完成检测时为 null。 |
data.spoofing_score
|
number | null | 合成图检测分数,未完成检测时为 null。 |
data.charged
|
boolean | 本次是否计费。 |
data.charge_source
|
string | null | 扣费来源:package 次数包,balance 余额;未计费时为 null。 |
data.charge_amount
|
string | 余额扣费金额(元,4 位小数);使用次数包或未计费时为 0.0000。 |
data.idempotent
|
boolean | true 表示相同 biz_no 的重复提交,返回的是第一次的结果;查询接口固定为 false。 |
data.created_at
|
string | null | 受理时间,ISO 8601 格式(UTC)。 |
data.completed_at
|
string | null | 完成时间,ISO 8601 格式(UTC)。 |
认证状态(data.status)
| 取值 | 名称 | 说明 |
|---|---|---|
passed
|
认证通过 | verify_status 为 0,且 score 不低于 threshold。 |
failed
|
认证未通过 | 比对或检测未通过,或参数被判定为不合法;不计费,原因见 result_code。 |
error
|
调用异常 | 认证服务异常或超时,本次不计费,HTTP 状态码为 502。 |
认证通过
{
"ok": true,
"data": {
"verify_no": "FV2026100816000112345678",
"biz_no": "kyc-20261008-0001",
"source": "api",
"product": "face_basic",
"status": "passed",
"status_label": "认证通过",
"passed": true,
"result_code": "passed",
"result_msg": "认证通过",
"score": 92.381,
"threshold": 80.0,
"verify_status": 0,
"liveness_score": 0.9861,
"spoofing_score": 2.0e-5,
"charged": true,
"charge_source": "package",
"charge_amount": "0.0000",
"idempotent": false,
"created_at": "2026-10-08T08:00:01+00:00",
"completed_at": "2026-10-08T08:00:02+00:00"
}
}
认证未通过(活体检测未通过,不计费)
{
"ok": true,
"data": {
"verify_no": "FV2026100816010287654321",
"biz_no": "kyc-20261008-0002",
"source": "api",
"product": "face_basic",
"status": "failed",
"status_label": "认证未通过",
"passed": false,
"result_code": "liveness_failed",
"result_msg": "活体检测未通过",
"score": null,
"threshold": 80.0,
"verify_status": null,
"liveness_score": null,
"spoofing_score": null,
"charged": false,
"charge_source": null,
"charge_amount": "0.0000",
"idempotent": false,
"created_at": "2026-10-08T08:01:02+00:00",
"completed_at": "2026-10-08T08:01:03+00:00"
}
}
失败响应
HTTP 502 · 认证服务异常或超时
error_code 为 upstream_error 或 upstream_timeout,本次不计费,预扣的费用已退还。data 中带有平台流水号,status 为 error。请使用同一个 biz_no 稍后重试。
{
"ok": false,
"error_code": "upstream_timeout",
"message": "认证服务响应超时,本次不计费,请稍后重试",
"data": {
"verify_no": "FV2026100816020311223344",
"biz_no": "kyc-20261008-0003",
"source": "api",
"product": "face_basic",
"status": "error",
"status_label": "调用异常",
"passed": false,
"result_code": "upstream_timeout",
"result_msg": "认证服务响应超时,本次不计费,请稍后重试",
"score": null,
"threshold": 80.0,
"verify_status": null,
"liveness_score": null,
"spoofing_score": null,
"charged": false,
"charge_source": null,
"charge_amount": "0.0000",
"idempotent": false,
"created_at": "2026-10-08T08:02:03+00:00",
"completed_at": "2026-10-08T08:02:13+00:00"
}
}
HTTP 422 · 参数校验失败
error_code 为 validation_failed,errors 按字段列出原因,message 为第一条原因。本次未发起认证,不计费。
{
"ok": false,
"error_code": "validation_failed",
"message": "证件号码不正确,请传入 18 位居民身份证号码。",
"errors": {
"id_card_number": [
"证件号码不正确,请传入 18 位居民身份证号码。"
],
"image": [
"图片分辨率过大,需不超过 1920×1080。"
]
}
}
HTTP 402 · 次数包与余额不足
次数包已用完且余额不足以支付本次认证(insufficient_funds),或次数包已用完且平台未设置单价(price_not_set)。本次未发起认证,不计费。
{
"ok": false,
"error_code": "insufficient_funds",
"message": "人脸实名认证次数包已用完,账户余额 ¥0.2000 不足以支付本次认证(¥0.5000/次),请充值或购买次数包。"
}
HTTP 409 · 相同业务单号正在处理
相同 biz_no 的第一次请求还没有完成。稍后用同一个 biz_no 重新提交即可拿到第一次的结果,不会重复扣费。
{
"ok": false,
"error_code": "processing",
"message": "业务单号对应的认证(FV2026100816000112345678)正在处理中,请稍后查询结果。"
}
其他错误(503 服务暂未开放、500 平台内部异常,以及 401 / 403 / 429 鉴权与限流错误)见 结果码与错误码。
人脸实名认证(基础版)
查询认证结果
/api/open/v1/face/basic/verifications/{verify_no}
按平台流水号查询认证记录,只能查询本应用的记录。data 的字段与认证接口相同,idempotent 固定为 false。认证接口是同步返回的,正常情况下不需要轮询,本接口用于对账或补查。
路径参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
verify_no
|
string | 是 | 平台流水号,取认证接口返回的 data.verify_no(FV 开头,共 24 位)。 |
响应示例
{
"ok": true,
"data": {
"verify_no": "FV2026100816000112345678",
"biz_no": "kyc-20261008-0001",
"source": "api",
"product": "face_basic",
"status": "passed",
"status_label": "认证通过",
"passed": true,
"result_code": "passed",
"result_msg": "认证通过",
"score": 92.381,
"threshold": 80.0,
"verify_status": 0,
"liveness_score": 0.9861,
"spoofing_score": 2.0e-5,
"charged": true,
"charge_source": "package",
"charge_amount": "0.0000",
"idempotent": false,
"created_at": "2026-10-08T08:00:01+00:00",
"completed_at": "2026-10-08T08:00:02+00:00"
}
}
HTTP 404 · 记录不存在
流水号不存在、格式不正确,或不是本应用的记录。
{
"ok": false,
"error_code": "not_found",
"message": "认证记录不存在。"
}
人脸实名认证(基础版)
查询余额与次数
/api/open/v1/face/basic/account/balance
查询应用所属账户的余额、基础版次数包的可用次数与按次单价。认证时先扣次数包,没有可用次数时按 unit_price 从余额扣除。
请求参数
无。
响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
ok
|
boolean | 查询成功时为 true。 |
data.product
|
string | 接口所属产品:基础版接口为 face_basic,增强版接口为 face_enhanced。 |
data.balance
|
string | 账户余额(元,4 位小数),与短信服务共用同一个账户余额。 |
data.currency
|
string | 币种,固定为 CNY。 |
data.available_times
|
integer | 本产品生效中的次数包剩余可用次数(基础版与增强版的次数包分开计算),不含余额可支付的次数。 |
data.unit_price
|
string | null | 本产品次数包用完后按次扣余额的单价(元/次);平台未设置单价或服务未开放时为 null。 |
响应示例
{
"ok": true,
"data": {
"product": "face_basic",
"balance": "149.0000",
"currency": "CNY",
"available_times": 500,
"unit_price": "0.5000"
}
}
人脸实名认证(基础版)
H5 采集页接入
不想自己开发采集界面时,可以使用平台托管的 H5 采集页:你的服务端创建认证会话,把返回的采集页链接交给用户,用户在手机浏览器中完成刷脸,平台完成人证比对后跳回你的页面并发送异步通知。签名鉴权与计费口径与 人脸实名认证 接口相同,使用同一个人脸实名认证应用。
动作活体
- 采集页每次随机下发 2 个动作(从眨眼、张嘴、向左转头、向右转头中抽取),用户按提示现场完成,用于防范照片、录屏视频等冒用。
- 页面实时识别动作并提示调整位置与光线;动作完成后,关键画面会提交平台服务端再次复核,复核通过后才使用正脸照片进行质量检测、活体检测、合成图检测与人证比对。
- 动作未完成或复核未通过时不进行人证比对、不计费、不消耗认证次数,用户可直接重试;同一会话动作复核失败次数过多时会话结束(状态为认证未通过)。
- 采集页需要浏览器支持摄像头与 WebAssembly,主流手机浏览器与微信内置浏览器均可使用;首次打开需加载约 8MB 的识别组件,之后由浏览器缓存。
接入流程
-
服务端创建会话
你的服务端调用 POST /face/basic/sessions,传入姓名、身份证号码与业务单号,拿到 h5_url。
-
引导用户打开采集页
把 h5_url 发给用户(跳转、短信或二维码均可),链接 15 分钟内有效,只能由本人使用。
-
用户在采集页刷脸
用户同意授权后按提示完成刷脸;未通过时可在剩余次数内重试。
-
跳回与通知
认证结束后跳回 return_url,同时向 notify_url 发送异步通知。
-
服务端确认结果
以异步通知或 GET /face/basic/sessions/{session_no} 的查询结果为准,不要信任跳回页面的参数。
注意事项
- 会话自创建起 15 分钟有效,过期后状态变为 expired,需要使用新的 biz_no 重新创建。
- 一个会话内可多次尝试,未通过时消耗 1 次认证次数;认证服务异常不消耗认证次数,也不计费。
- 一个会话最多计费 1 次:只有认证通过的那一次计费,未通过与异常都不计费。
- 姓名、证件号码只在会话有效期内加密暂存,会话结束即删除,平台不落库;采集页只显示脱敏后的姓名,不显示证件号码。
- h5_url 即访问凭证,请只发给认证本人,不要写入日志或公开传播。
跳回 return_url 时只追加 session_no 与 biz_no,不带认证结果。请务必以异步通知或查询接口的结果为准,不要根据用户是否跳回来判断认证通过。
人脸实名认证(基础版)
创建认证会话
/api/open/v1/face/basic/sessions
创建一个 H5 认证会话并返回采集页链接。除公共请求头外,需要携带 Content-Type: application/json。会话自创建起 15 分钟内有效;创建会话本身不计费。
请求参数(JSON 请求体)
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
name
|
string | 是 | 姓名,规则同「人脸实名认证」接口。只加密暂存到会话结束,平台不落库。 |
id_card_number
|
string | 是 | 18 位居民身份证号码,规则同「人脸实名认证」接口。只加密暂存到会话结束,平台不落库。 |
biz_no
|
string | 是 | 业务单号,1–64 位字母、数字、_ 和 -;同一应用内唯一,每个 biz_no 只能创建一个会话。 |
return_url
|
string | 否 | 认证结束后的跳回地址,完整的 http(s) 地址,不超过 1000 个字符;域名必须是应用登记的业务域名或其子域名。不传时采集页只显示结果。 |
notify_url
|
string | 否 | 异步通知地址,要求同 return_url;另外只能使用默认端口(http 80 / https 443,地址中不写端口),且域名必须解析到公网地址(不能是内网、本机或保留地址)。建议传入,见「异步通知与跳回」。 |
max_attempts
|
integer | 否 | 本会话最多认证次数,1–5,默认 3。认证服务异常不消耗次数。 |
quality_control
|
string | 否 | 质量检测级别:LOW(低)、NORMAL(中,推荐)、HIGH(高);不传时使用平台默认级别。 |
liveness_control
|
string | 否 | 活体检测级别,取值同上;不传时使用平台默认级别。 |
spoofing_control
|
string | 否 | 合成图检测级别,取值同上;不传时使用平台默认级别。 |
product
|
string | 否 | 产品,可不传:会话产品由应用类型决定。传入时只能为 basic(基础版接口)或 enhanced(增强版接口),且必须与应用类型一致,否则返回 HTTP 422。 |
业务域名在「人脸实名认证服务 › 我的应用」的应用详情中登记;return_url、notify_url 的域名必须是该域名本身或其子域名。
请求示例
{
"name": "张三",
"id_card_number": "11010519491231002X",
"biz_no": "kyc-20261008-0101",
"return_url": "https://www.example.com/kyc/return",
"notify_url": "https://api.example.com/kyc/notify",
"max_attempts": 3,
"liveness_control": "NORMAL"
}
成功响应(HTTP 201)
| 字段 | 类型 | 说明 |
|---|---|---|
data.session_no
|
string | 平台会话号,FS 开头,共 24 位。 |
data.biz_no
|
string | 创建时传入的业务单号。 |
data.product
|
string | 会话所属产品,与应用类型一致:face_basic 基础版,face_enhanced 增强版。 |
data.status
|
string | 会话状态,见下方「会话状态」。 |
data.status_label
|
string | 会话状态的中文名称。 |
data.passed
|
boolean | 是否认证通过,等同于 status 为 passed。 |
data.attempts
|
integer | 已认证次数(认证服务异常不计入)。 |
data.max_attempts
|
integer | 最多认证次数。 |
data.verify_no
|
string | null | 最近一次认证的平台流水号,可用于查询认证结果接口(基础版 GET /face/basic/verifications/{verify_no},增强版 GET /face/enhanced/verifications/{verify_no});尚未认证时为 null。 |
data.result_code
|
string | null | 最近一次认证的结果码,见「结果码与错误码」。 |
data.result_msg
|
string | null | 最近一次认证结果的中文说明。 |
data.score
|
number | null | 最近一次认证的比对分数。 |
data.threshold
|
number | null | 最近一次认证使用的通过阈值。 |
data.charged
|
boolean | 是否计费,只有认证通过时为 true;一个会话最多计费 1 次。 |
data.charge_source
|
string | null | 扣费来源:package 次数包,balance 余额;未计费时为 null。 |
data.charge_amount
|
string | 余额扣费金额(元,4 位小数);使用次数包或未计费时为 0.0000。 |
data.expires_at
|
string | 会话过期时间,ISO 8601 格式(UTC)。 |
data.completed_at
|
string | null | 会话结束时间,ISO 8601 格式(UTC);未结束时为 null。 |
data.created_at
|
string | 会话创建时间,ISO 8601 格式(UTC)。 |
data.h5_url
|
string | 采集页链接,仅创建接口返回;请勿记录到日志或公开传播,链接即凭证。 |
{
"ok": true,
"data": {
"session_no": "FS2026100816100012345678",
"biz_no": "kyc-20261008-0101",
"product": "face_basic",
"status": "pending",
"status_label": "待认证",
"passed": false,
"attempts": 0,
"max_attempts": 3,
"verify_no": null,
"result_code": null,
"result_msg": null,
"score": null,
"threshold": null,
"charged": false,
"charge_source": null,
"charge_amount": "0.0000",
"expires_at": "2026-10-08T08:25:00+00:00",
"completed_at": null,
"created_at": "2026-10-08T08:10:00+00:00",
"h5_url": "https://am.siyun223.com/h5/face/3f9a1c7e3f9a1c7e3f9a1c7e3f9a1c7e3f9a1c7e3f9a1c7e3f9a1c7e3f9a1c7e"
}
}
失败响应
HTTP 409 · 业务单号已创建过会话
error_code 为 biz_no_exists。每个 biz_no 只能创建一个会话;需要重新认证时请使用新的 biz_no,原会话结果可继续查询。
{
"ok": false,
"error_code": "biz_no_exists",
"message": "业务单号已创建过认证会话(FS2026100816100012345678),请使用新的 biz_no。"
}
HTTP 422 · 参数校验失败
error_code 为 validation_failed,errors 按字段列出原因,例如缺少 biz_no、return_url 不是应用业务域名、max_attempts 超出范围。
{
"ok": false,
"error_code": "validation_failed",
"message": "return_url(认证结束后的跳回地址)的域名必须是应用登记的业务域名 example.com 或其子域名。",
"errors": {
"return_url": [
"return_url(认证结束后的跳回地址)的域名必须是应用登记的业务域名 example.com 或其子域名。"
]
}
}
HTTP 503 · 服务暂不可用
error_code 为 service_unavailable:人脸实名认证服务暂未开放,或会话服务暂时不可用。请稍后用同一个 biz_no 重试。
{
"ok": false,
"error_code": "service_unavailable",
"message": "认证会话服务暂时不可用,请稍后重试。"
}
401 / 403 / 429 鉴权与限流错误见 状态码与错误码。
人脸实名认证(基础版)
查询认证会话
/api/open/v1/face/basic/sessions/{session_no}
按平台会话号查询会话状态与最近一次认证结果,只能查询本应用的会话。data 的字段与创建接口相同,但不含 h5_url。用户跳回你的页面后、或没有收到异步通知时,用本接口获取最终结果。
路径参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
session_no
|
string | 是 | 平台会话号,取创建会话返回的 data.session_no(FS 开头,共 24 位)。 |
会话状态(data.status)
| 取值 | 名称 | 说明 |
|---|---|---|
pending
|
待认证 | 会话已创建,用户尚未完成认证。 |
passed
|
认证通过 | 某一次认证通过,会话结束;本会话计费 1 次。 |
failed
|
认证未通过 | 认证次数已用完仍未通过,会话结束;不计费。 |
expired
|
已过期 | 超过有效期仍未完成,会话结束;不计费。 |
cancelled
|
用户取消 | 用户在采集页放弃认证,会话结束;不计费。 |
响应示例
{
"ok": true,
"data": {
"session_no": "FS2026100816100012345678",
"biz_no": "kyc-20261008-0101",
"product": "face_basic",
"status": "passed",
"status_label": "认证通过",
"passed": true,
"attempts": 2,
"max_attempts": 3,
"verify_no": "FV2026100816120387651234",
"result_code": "passed",
"result_msg": "认证通过",
"score": 91.276,
"threshold": 80.0,
"charged": true,
"charge_source": "package",
"charge_amount": "0.0000",
"expires_at": "2026-10-08T08:25:00+00:00",
"completed_at": "2026-10-08T08:12:04+00:00",
"created_at": "2026-10-08T08:10:00+00:00"
}
}
HTTP 404 · 会话不存在
会话号不存在、格式不正确,或不是本应用的会话。
{
"ok": false,
"error_code": "not_found",
"message": "认证会话不存在。"
}
人脸实名认证(基础版)
异步通知与跳回
异步通知
- 会话结束(认证通过、次数用完、过期或用户取消)后,平台向 notify_url 发送 POST 请求,请求体为 JSON:{"event":"face.session.completed","data":{…}},data 与查询会话的返回相同。
- 验签:待签名串 = POST、notify_url 的路径(path)、notify_url 中查询参数的规范查询串(没有时为空)、X-Timestamp、X-Nonce、原始请求体的 SHA256,用 \n 连接;与调用开放接口的签名算法完全相同,可直接复用签名代码。
- 你的服务返回任意 HTTP 2xx 即视为通知成功,响应内容不限;不会跟随重定向。
- 通知失败(非 2xx、超时或连接失败)时,按 1、5、30、120、360 分钟的间隔依次重试,全部失败后不再通知,请通过查询接口补查。
- 同一会话可能收到重复通知,请以 session_no 去重,按幂等方式处理。
通知请求头
| 请求头 | 类型 | 必填 | 说明 |
|---|---|---|---|
Content-Type
|
string | 是 | 固定为 application/json。 |
X-App-Id
|
string | 是 | 创建会话所用应用的 AppId。 |
X-Timestamp
|
string | 是 | 发送时的 Unix 时间戳(秒),建议校验与当前时间相差不超过 5 分钟。 |
X-Nonce
|
string | 是 | 32 位随机串,每次通知(含重试)都不同,可用于防重放。 |
X-Signature
|
string | 是 | 签名,算法与调用开放接口完全相同,使用同一应用的 AppSecret。 |
通知请求体示例
{
"event": "face.session.completed",
"data": {
"session_no": "FS2026100816100012345678",
"biz_no": "kyc-20261008-0101",
"product": "face_basic",
"status": "passed",
"status_label": "认证通过",
"passed": true,
"attempts": 2,
"max_attempts": 3,
"verify_no": "FV2026100816120387651234",
"result_code": "passed",
"result_msg": "认证通过",
"score": 91.276,
"threshold": 80.0,
"charged": true,
"charge_source": "package",
"charge_amount": "0.0000",
"expires_at": "2026-10-08T08:25:00+00:00",
"completed_at": "2026-10-08T08:12:04+00:00",
"created_at": "2026-10-08T08:10:00+00:00"
}
}
验签示例代码见 示例代码。
跳回参数
会话结束后,采集页在 return_url 后追加 session_no 与 biz_no 两个查询参数(原有查询参数与 # 片段保留),例如:
https://www.example.com/kyc/return?session_no=FS2026100816100012345678&biz_no=kyc-20261008-0101
跳回地址不带认证结果,参数也可能被用户篡改。收到跳回后,请用 session_no 调用 查询认证会话,或以异步通知的结果为准。
人脸实名认证(基础版)
结果码与错误码
结果码(data.result_code)
认证完成后返回的结果码。是否计费以响应中的 data.charged 为准:只有 passed 计费,其余结果码均不计费。
| 结果码 | 含义 | 认证状态 | 计费 | 说明 |
|---|---|---|---|---|
passed
|
认证通过 | 认证通过(passed) | 计费 | verify_status 为 0 且 score 不低于 threshold。 |
identity_failed
|
身份核验未通过 | 认证未通过(failed) | 不计费 | 身份核验未通过:证件号码与姓名不匹配、权威库查无该证件,或人脸与证件照不是同一人(比对分数低于 threshold)。出于隐私保护不区分具体原因,也不返回比对分数。 |
no_face
|
未检测到人脸 | 认证未通过(failed) | 不计费 | 图片中未检测到人脸,或无法解析图片中的人脸。 |
quality_failed
|
质量检测未通过 | 认证未通过(failed) | 不计费 | 人脸有遮挡、模糊、光照不佳、不完整、未面向正前方、尺寸过小或不是真人照片,具体原因见 result_msg。 |
liveness_failed
|
活体检测未通过 | 认证未通过(failed) | 不计费 | 活体检测未通过,照片可能是翻拍的屏幕或纸质照片。 |
spoofing_failed
|
合成图检测未通过 | 认证未通过(failed) | 不计费 | 合成图检测未通过,照片可能经过合成或编辑。 |
invalid_id_card
|
证件号码格式错误 | 认证未通过(failed) | 不计费 | 证件号码被认证服务判定为格式不正确。 |
invalid_name
|
姓名格式错误 | 认证未通过(failed) | 不计费 | 姓名被认证服务判定为格式不正确。 |
invalid_image
|
图片格式错误 | 认证未通过(failed) | 不计费 | 图片格式解析失败、Base64 格式不正确,或尺寸过小、过大。 |
risk_intercepted
|
数据源风险拦截 | 认证未通过(failed) | 不计费 | 同一身份信息短时间内重复认证,被数据源风险拦截,请稍后再试。 |
upstream_error
|
认证服务异常 | 调用异常(error) | 不计费 | 认证服务暂时异常或繁忙,HTTP 状态码为 502,可用同一个 biz_no 重试。 |
upstream_timeout
|
认证服务超时 | 调用异常(error) | 不计费 | 认证服务响应超时,HTTP 状态码为 502,可用同一个 biz_no 重试。 |
错误码(error_code)
ok 为 false 时返回。除 502 外均未发起认证、不计费。缺少签名、签名错误、应用停用、IP 白名单、限流等鉴权错误码(401 / 403 / 429)与短信服务相同,见 状态码与错误码。
| error_code | HTTP 状态码 | 说明 |
|---|---|---|
validation_failed
|
422 | 请求参数校验失败,errors 按字段列出原因;不计费。 |
insufficient_funds
|
402 | 次数包已用完,且账户余额不足以支付本次认证;不计费。 |
price_not_set
|
402 | 次数包已用完,且平台未设置按次单价;不计费。 |
processing
|
409 | 相同 biz_no 的请求仍在处理中,稍后用同一个 biz_no 重新提交即可。 |
biz_no_exists
|
409 | 创建 H5 认证会话时,该 biz_no 已创建过会话,请使用新的 biz_no;不计费。 |
daily_limit_exceeded
|
429 | 本应用今日(北京时间)的认证次数已达上限(每个应用每天 3,000 次,直接认证与创建 H5 会话分别计算),请明天再试或联系平台调整;不计费。 |
identity_restricted
|
403 | 该身份信息暂无法在平台内认证(直接认证、创建 H5 会话时返回;会话创建后才受限的,打开采集页或提交时会话直接结束为认证失败)。请引导用户联系平台运营查询;不计费。 |
not_found
|
404 | 认证记录或认证会话不存在,或不是本应用的;也用于接口路径错误。 |
upstream_error
|
502 | 认证服务暂时异常,不计费,可用同一个 biz_no 重试。 |
upstream_timeout
|
502 | 认证服务响应超时,不计费,可用同一个 biz_no 重试。 |
service_unavailable
|
503 | 人脸实名认证服务暂未开放,或认证会话服务暂时不可用;不计费。 |
internal_error
|
500 | 平台内部异常,请稍后重试;传入 biz_no 重试可避免重复扣费。 |
app_service_mismatch
|
403 | 使用了短信服务的应用调用人脸接口,请使用「人脸实名认证」下创建的应用。 |
app_product_mismatch
|
403 | 应用类型与接口不符:基础版应用只能调用 /face/basic/*,增强版应用只能调用 /face/enhanced/*;不计费。 |
product_not_supported
|
403 | 增强版应用调用了不带产品的旧路径 POST /face/verify。增强版不支持直接上传照片认证,请调用 POST /face/enhanced/sessions;不计费。 |
人脸实名认证(增强版)
产品说明
人脸实名认证(增强版)由用户在平台 H5 采集页按随机动作现场录制一段短视频,平台先校验动作并对视频做活体与合成图检测,再用视频中的最佳人脸与公安权威库进行人证比对。相比基础版的单张照片,更能防范照片翻拍、录屏回放与 AI 换脸等冒用。
增强版的接口路径均以 /api/open/v1/face/enhanced/ 开头,只接受类型为「增强版」的人脸实名认证应用;参数、返回与通知和基础版的 H5 采集页接口相同。
- 认证方式
- 姓名 + 证件号码 + 用户在 H5 采集页按随机动作现场录制的短视频
- 活体检测
- 视频动作活体:随机下发动作(默认 2 个,从眨眨眼、抬抬头、低低头、左右转头中抽取),平台服务端校验动作是否完成,并对视频多帧画面做活体与合成图检测
- 换脸鉴别
- 平台启用时,对视频中的最佳人脸再做一次 AI 换脸鉴别
- 人证比对
- 使用视频中的最佳人脸与公安权威库比对,通过规则同基础版(verify_status 为 0 且 score ≥ threshold,默认 80)
- 接入方式
- 只支持平台托管的 H5 采集页,不提供直接上传照片的认证接口
- 数据保存
- 视频只在处理过程中临时存在,处理完立即删除;平台不保存视频、图片、姓名与证件号码
与基础版的区别
| 对比项 | 基础版 | 增强版 |
|---|---|---|
| 接口路径 | /face/basic/* | /face/enhanced/* |
| 应用类型 | 创建应用时选择「基础版」 | 创建应用时选择「增强版」(类型创建后不能修改) |
| 接入方式 | 开放接口直接上传照片,或 H5 采集页 | 只支持 H5 采集页 |
| 活体检测 | 照片静默活体(识别翻拍);H5 采集页另有随机动作活体 | 录制视频,平台服务端校验随机动作,并做视频多帧活体与合成图检测 |
| 换脸鉴别 | 无 | 平台启用时进行 |
| 计费 | 基础版单价与基础版次数包 | 增强版单价与增强版次数包,与基础版分开计算、不能互用 |
| 适用场景 | 一般的实名核验 | 开户、大额交易、账号找回等对防冒用要求更高的场景 |
安全边界
- 只能由你的服务端调用,AppSecret 不能出现在网页、小程序或 App 中。
- 采集页现场录制视频并征得用户授权;平台不保存视频、图片、姓名和证件号码,认证记录只保存结果、分数、计费信息与调用来源。
- 使用「人脸实名认证服务 › 我的应用」中类型为「增强版」的应用调用;基础版应用调用会返回 HTTP 403 app_product_mismatch,短信应用调用会返回 HTTP 403 app_service_mismatch。
人脸实名认证(增强版)
计费说明
- 仅认证通过计费,未通过与异常不计费。每次认证通过计 1 次:先扣生效中的增强版次数包(先到期的先用),没有可用次数时按增强版单价从账户余额扣除。
- 增强版与基础版的次数包分开计算、不能互用;增强版单价在「人脸实名认证服务 › 人脸实名认证(增强版)」页面查看,也可调用「查询余额与次数」获取。
- 创建认证会话不计费。一个会话内可多次尝试,最多计费 1 次(认证通过的那一次)。
- 用户提交认证前会先预扣 1 次,认证未通过或异常时自动退还到原来的次数包或余额;会话结果中的 charged、charge_source、charge_amount 为最终的计费结果。
- 用户提交认证时增强版次数包已用完且余额不足(或平台未设置增强版单价),采集页会提示无法认证,不会发起认证,也不扣费。
计费的情况
仅认证通过(result_code 为 passed)计费,一个会话最多计费 1 次。
不计费的情况
- 动作未按提示完成、视频活体或合成图检测未通过、换脸鉴别未通过、视频中没有人脸或有多张人脸:不计费,消耗会话的 1 次认证次数;
- 人证比对未通过(分数低于阈值、证件号码与姓名不匹配、权威库查无等):不计费,消耗 1 次认证次数;
- 动作校验过期、视频解析失败、认证服务异常或超时:不计费,也不消耗认证次数,用户可直接重试;
- 会话过期或用户取消:不计费。
每个结果码是否计费,见 结果码与错误码。
人脸实名认证(增强版)
接入流程
-
创建增强版应用
在「人脸实名认证服务 › 我的应用」创建应用,类型选择「增强版」,并登记业务域名。应用类型创建后不能修改。
-
服务端创建会话
你的服务端调用 POST /face/enhanced/sessions,传入姓名、身份证号码与业务单号,拿到 h5_url。
-
用户在采集页完成动作
把 h5_url 发给用户(15 分钟内有效)。用户同意授权后按提示依次完成随机动作,采集页同时录制几秒钟的视频并提交。
-
平台检测与比对
平台依次进行动作校验与视频活体检测、换脸鉴别(启用时)、人证比对;未通过时用户可在剩余次数内重试。
-
跳回与通知
认证结束后跳回 return_url,同时向 notify_url 发送异步通知,规则与基础版相同。
-
服务端确认结果
以异步通知或 GET /face/enhanced/sessions/{session_no} 的查询结果为准,不要信任跳回页面的参数。
注意事项
- 会话自创建起 15 分钟有效,默认最多认证 3 次(可通过 max_attempts 设为 1–5),规则与基础版相同。
- 增强版的检测阈值由平台统一设置,不支持 quality_control、liveness_control、spoofing_control 参数(传入也不生效)。
- 采集页需要浏览器允许使用摄像头,并支持 WebAssembly 与视频录制;主流手机浏览器均可使用,建议在光线充足的环境下认证。
- 姓名、证件号码只在会话有效期内加密暂存,会话结束即删除;采集页只显示脱敏后的姓名,不显示证件号码。
- h5_url 即访问凭证,请只发给认证本人,不要写入日志或公开传播。
跳回 return_url 时只追加 session_no 与 biz_no,不带认证结果。请务必以异步通知或查询接口的结果为准,不要根据用户是否跳回来判断认证通过。
人脸实名认证(增强版)
示例代码
演示创建增强版认证会话并取得 h5_url,以及在 notify_url 对应的接口中校验通知签名。签名实现与短信服务、基础版的示例代码相同,只是接口路径为 /face/enhanced/*。把「你的 AppId」「你的 AppSecret」替换为增强版应用的真实值即可运行。
PHP
<?php
// 人脸实名认证(增强版)H5 采集页示例(PHP 7.4 及以上,需要 curl 扩展)
// 一、创建认证会话,把 h5_url 交给用户打开;二、在 notify_url 对应的接口中验签异步通知
const APP_ID = '你的 AppId';
const APP_SECRET = '你的 AppSecret'; // 只保存在服务端,切勿写进网页、小程序或 App
const API_ORIGIN = 'https://am.siyun223.com';
const API_PREFIX = '/api/open/v1';
/** 签名并发送请求,返回 [HTTP 状态码, 响应数组](与人脸实名认证示例相同) */
function openApiRequest(string $method, string $path, ?array $payload = null): array
{
$fullPath = API_PREFIX . $path;
$body = $payload === null ? '' : json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$timestamp = (string) time();
$nonce = bin2hex(random_bytes(16));
$stringToSign = implode("\n", [$method, $fullPath, '', $timestamp, $nonce, hash('sha256', $body)]);
$headers = [
'Accept: application/json',
'Content-Type: application/json',
'X-App-Id: ' . APP_ID,
'X-Timestamp: ' . $timestamp,
'X-Nonce: ' . $nonce,
'X-Signature: ' . hash_hmac('sha256', $stringToSign, APP_SECRET),
];
$ch = curl_init(API_ORIGIN . $fullPath);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => $headers,
]);
if ($payload !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
}
$response = curl_exec($ch);
if ($response === false) {
throw new RuntimeException('请求失败:' . curl_error($ch));
}
return [(int) curl_getinfo($ch, CURLINFO_HTTP_CODE), json_decode($response, true)];
}
// ==================== 一、创建认证会话 ====================
// return_url、notify_url 的域名必须是应用登记的业务域名或其子域名
[$status, $result] = openApiRequest('POST', '/face/enhanced/sessions', [
'name' => '张三',
'id_card_number' => '11010519491231002X',
'biz_no' => 'kyc-20261008-0101', // 每个会话使用新的业务单号
'return_url' => 'https://www.example.com/kyc/return',
'notify_url' => 'https://api.example.com/kyc/notify',
'max_attempts' => 3,
]);
if ($status === 201) {
// 把 h5_url 交给用户打开(跳转、短信或二维码),保存 session_no 用于查询
echo $result['data']['session_no'], ' ', $result['data']['h5_url'], PHP_EOL;
} else {
echo $status, ' ', $result['error_code'] ?? '', ' ', $result['message'] ?? '', PHP_EOL;
}
// ==================== 二、验签异步通知(notify_url 对应的接口) ====================
/**
* 校验平台发来的通知,通过返回解析后的数组,失败返回 null。
* 路径取 notify_url 的 path;notify_url 带查询参数时,按规范查询串参与签名(见「签名方法」),这里假设不带。
*/
function verifyFaceNotify(string $path, array $headers, string $rawBody): ?array
{
$timestamp = $headers['x-timestamp'] ?? '';
$nonce = $headers['x-nonce'] ?? '';
$signature = $headers['x-signature'] ?? '';
if (($headers['x-app-id'] ?? '') !== APP_ID || abs(time() - (int) $timestamp) > 300) {
return null;
}
$stringToSign = implode("\n", ['POST', $path, '', $timestamp, $nonce, hash('sha256', $rawBody)]);
if (!hash_equals(hash_hmac('sha256', $stringToSign, APP_SECRET), $signature)) {
return null;
}
// 建议再用 Redis 等记录 nonce,5 分钟内重复的直接拒绝,防止重放
return json_decode($rawBody, true);
}
// 在通知接口中:
// $headers = array_change_key_case(getallheaders(), CASE_LOWER);
// $notice = verifyFaceNotify('/kyc/notify', $headers, file_get_contents('php://input'));
// if ($notice === null) { http_response_code(401); exit; }
// $data = $notice['data']; // 按 session_no 去重;data.passed 为 true 表示认证通过
// http_response_code(200); // 返回任意 2xx 即视为通知成功
Python
# 人脸实名认证(增强版)H5 采集页示例(Python 3.6 及以上,只使用标准库)
# 一、创建认证会话,把 h5_url 交给用户打开;二、在 notify_url 对应的接口中验签异步通知
import hashlib
import hmac
import json
import secrets
import time
import urllib.error
import urllib.request
APP_ID = "你的 AppId"
APP_SECRET = "你的 AppSecret" # 只保存在服务端,切勿写进网页、小程序或 App
API_ORIGIN = "https://am.siyun223.com"
API_PREFIX = "/api/open/v1"
def sign(method, path, timestamp, nonce, body):
"""待签名串:方法、路径、规范查询串(无查询参数时为空)、时间戳、随机串、请求体 SHA256"""
string_to_sign = "\n".join([method, path, "", timestamp, nonce, hashlib.sha256(body).hexdigest()])
return hmac.new(APP_SECRET.encode("utf-8"), string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
def open_api_request(method, path, payload=None):
full_path = API_PREFIX + path
body = b"" if payload is None else json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
timestamp = str(int(time.time()))
nonce = secrets.token_hex(16)
request = urllib.request.Request(API_ORIGIN + full_path, data=body if payload is not None else None, method=method, headers={
"Accept": "application/json",
"Content-Type": "application/json",
"X-App-Id": APP_ID,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature": sign(method, full_path, timestamp, nonce, body),
})
try:
with urllib.request.urlopen(request, timeout=15) as response:
return response.status, json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as error:
return error.code, json.loads(error.read().decode("utf-8"))
def verify_notify(path, headers, raw_body):
"""校验平台发来的通知(headers 的键为小写),通过返回解析后的 dict,失败返回 None。
路径取 notify_url 的 path;notify_url 带查询参数时按规范查询串参与签名,这里假设不带。"""
timestamp = headers.get("x-timestamp", "")
if headers.get("x-app-id") != APP_ID or not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
return None
expected = sign("POST", path, timestamp, headers.get("x-nonce", ""), raw_body)
if not hmac.compare_digest(expected, headers.get("x-signature", "")):
return None
# 建议再记录 nonce,5 分钟内重复的直接拒绝,防止重放
return json.loads(raw_body.decode("utf-8"))
if __name__ == "__main__":
# 一、创建认证会话:return_url、notify_url 的域名必须是应用登记的业务域名或其子域名
status, result = open_api_request("POST", "/face/enhanced/sessions", {
"name": "张三",
"id_card_number": "11010519491231002X",
"biz_no": "kyc-20261008-0101",
"return_url": "https://www.example.com/kyc/return",
"notify_url": "https://api.example.com/kyc/notify",
"max_attempts": 3,
})
if status == 201:
print(result["data"]["session_no"], result["data"]["h5_url"]) # 把 h5_url 交给用户打开
else:
print(status, result.get("error_code"), result.get("message"))
# 二、在通知接口中(以 Flask 为例):
# notice = verify_notify("/kyc/notify", {k.lower(): v for k, v in request.headers.items()}, request.get_data())
# if notice is None: return "", 401
# data = notice["data"] # 按 session_no 去重;data["passed"] 为 True 表示认证通过
# return "", 200 # 返回任意 2xx 即视为通知成功
Java
// 人脸实名认证(增强版)H5 采集页示例(Java 11 及以上,只使用 JDK 标准库)
// 一、创建认证会话,把 h5_url 交给用户打开;二、在 8080 端口接收异步通知并验签
// 保存为 FaceH5Demo.java,执行:javac -encoding UTF-8 FaceH5Demo.java && java FaceH5Demo
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpServer;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.time.Duration;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class FaceH5Demo {
static final String APP_ID = "你的 AppId";
static final String APP_SECRET = "你的 AppSecret"; // 只保存在服务端,切勿写进网页、小程序或 App
static final String API_ORIGIN = "https://am.siyun223.com";
static final String API_PREFIX = "/api/open/v1";
static String hex(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
sb.append(Character.forDigit((b >> 4) & 0xF, 16)).append(Character.forDigit(b & 0xF, 16));
}
return sb.toString();
}
// 待签名串:方法、路径、规范查询串(无查询参数时为空)、时间戳、随机串、请求体 SHA256
static String sign(String method, String path, String timestamp, String nonce, byte[] body) throws Exception {
String bodyHash = hex(MessageDigest.getInstance("SHA-256").digest(body));
String stringToSign = String.join("\n", method, path, "", timestamp, nonce, bodyHash);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(APP_SECRET.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
return hex(mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8)));
}
// 校验平台发来的通知;路径取 notify_url 的 path,notify_url 带查询参数时按规范查询串参与签名,这里假设不带
static boolean verifyNotify(String path, Headers headers, byte[] rawBody) throws Exception {
String timestamp = headers.getFirst("X-Timestamp");
String nonce = headers.getFirst("X-Nonce");
String signature = headers.getFirst("X-Signature");
if (!APP_ID.equals(headers.getFirst("X-App-Id")) || timestamp == null || !timestamp.matches("\\d{1,12}")
|| Math.abs(System.currentTimeMillis() / 1000 - Long.parseLong(timestamp)) > 300
|| nonce == null || signature == null) {
return false;
}
byte[] expected = sign("POST", path, timestamp, nonce, rawBody).getBytes(StandardCharsets.UTF_8);
// 常量时间比较;建议再记录 nonce,5 分钟内重复的直接拒绝,防止重放
return MessageDigest.isEqual(expected, signature.getBytes(StandardCharsets.UTF_8));
}
public static void main(String[] args) throws Exception {
// ==================== 一、创建认证会话 ====================
// return_url、notify_url 的域名必须是应用登记的业务域名或其子域名;biz_no 每个会话使用新的业务单号
String path = API_PREFIX + "/face/enhanced/sessions";
String json = "{\"name\":\"张三\",\"id_card_number\":\"11010519491231002X\",\"biz_no\":\"kyc-20261008-0101\","
+ "\"return_url\":\"https://www.example.com/kyc/return\","
+ "\"notify_url\":\"https://api.example.com/kyc/notify\",\"max_attempts\":3}";
byte[] body = json.getBytes(StandardCharsets.UTF_8);
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
byte[] random = new byte[16];
new SecureRandom().nextBytes(random);
String nonce = hex(random);
HttpRequest request = HttpRequest.newBuilder(URI.create(API_ORIGIN + path))
.timeout(Duration.ofSeconds(15))
.header("Accept", "application/json")
.header("Content-Type", "application/json")
.header("X-App-Id", APP_ID)
.header("X-Timestamp", timestamp)
.header("X-Nonce", nonce)
.header("X-Signature", sign("POST", path, timestamp, nonce, body))
.POST(HttpRequest.BodyPublishers.ofByteArray(body))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
// HTTP 201 时 data.h5_url 为采集页链接,交给用户打开;保存 data.session_no 用于查询
System.out.println(response.statusCode() + " " + response.body());
// ==================== 二、接收异步通知并验签 ====================
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
server.createContext("/kyc/notify", exchange -> {
byte[] raw = exchange.getRequestBody().readAllBytes();
boolean ok;
try {
ok = "POST".equals(exchange.getRequestMethod())
&& verifyNotify("/kyc/notify", exchange.getRequestHeaders(), raw);
} catch (Exception e) {
ok = false;
}
if (ok) {
// 按 session_no 去重;解析 JSON 后 data.passed 为 true 表示认证通过
System.out.println(new String(raw, StandardCharsets.UTF_8));
}
exchange.sendResponseHeaders(ok ? 200 : 401, -1); // 返回任意 2xx 即视为通知成功
exchange.close();
});
server.start();
}
}
Node.js
// 人脸实名认证(增强版)H5 采集页示例(Node.js 18 及以上,使用内置 fetch、crypto 与 http,无需安装依赖)
// 一、创建认证会话,把 h5_url 交给用户打开;二、在 notify_url 对应的接口中验签异步通知
import { createHash, createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
import { createServer } from 'node:http';
const APP_ID = '你的 AppId';
const APP_SECRET = '你的 AppSecret'; // 只保存在服务端,切勿写进网页、小程序或 App
const API_ORIGIN = 'https://am.siyun223.com';
const API_PREFIX = '/api/open/v1';
// 待签名串:方法、路径、规范查询串(无查询参数时为空)、时间戳、随机串、请求体 SHA256
function sign(method, path, timestamp, nonce, body) {
const bodyHash = createHash('sha256').update(body).digest('hex');
const stringToSign = [method, path, '', timestamp, nonce, bodyHash].join('\n');
return createHmac('sha256', APP_SECRET).update(stringToSign, 'utf8').digest('hex');
}
// ==================== 一、创建认证会话 ====================
const path = API_PREFIX + '/face/enhanced/sessions';
const body = JSON.stringify({
name: '张三',
id_card_number: '11010519491231002X',
biz_no: 'kyc-20261008-0101',
return_url: 'https://www.example.com/kyc/return', // 域名必须是应用登记的业务域名或其子域名
notify_url: 'https://api.example.com/kyc/notify',
max_attempts: 3,
});
const timestamp = String(Math.floor(Date.now() / 1000));
const nonce = randomBytes(16).toString('hex');
const response = await fetch(API_ORIGIN + path, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-App-Id': APP_ID,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': sign('POST', path, timestamp, nonce, body),
},
body,
signal: AbortSignal.timeout(15000),
});
const created = await response.json();
console.log(response.status, response.status === 201 ? created.data.h5_url : created); // 把 h5_url 交给用户打开
// ==================== 二、验签异步通知 ====================
// 路径取 notify_url 的 path;notify_url 带查询参数时按规范查询串参与签名,这里假设不带
function verifyNotify(notifyPath, headers, rawBody) {
const ts = String(headers['x-timestamp'] || '');
if (headers['x-app-id'] !== APP_ID || Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
return null;
}
const expected = Buffer.from(sign('POST', notifyPath, ts, String(headers['x-nonce'] || ''), rawBody));
const actual = Buffer.from(String(headers['x-signature'] || ''));
if (expected.length !== actual.length || !timingSafeEqual(expected, actual)) {
return null;
}
return JSON.parse(rawBody.toString('utf8')); // 建议再记录 nonce 防止重放
}
createServer((req, res) => {
const chunks = [];
req.on('data', (chunk) => chunks.push(chunk));
req.on('end', () => {
const notice = req.method === 'POST' && req.url === '/kyc/notify'
? verifyNotify('/kyc/notify', req.headers, Buffer.concat(chunks))
: null;
if (notice === null) {
res.writeHead(401).end();
return;
}
// 按 session_no 去重;notice.data.passed 为 true 表示认证通过
console.log(notice.event, notice.data.session_no, notice.data.status);
res.writeHead(200).end('ok'); // 返回任意 2xx 即视为通知成功
});
}).listen(8080);
Shell
#!/usr/bin/env bash
# 人脸实名认证(增强版)H5 采集页示例(bash、curl 与 openssl)
# 一、创建认证会话,把 h5_url 交给用户打开;二、verify_notify 演示通知接口如何验签
# 保存为 face-h5-demo.sh,执行:bash face-h5-demo.sh
set -euo pipefail
APP_ID='你的 AppId'
APP_SECRET='你的 AppSecret' # 只保存在服务端,切勿写进网页、小程序或 App
API_ORIGIN='https://am.siyun223.com'
API_PREFIX='/api/open/v1'
# 用法:sign 方法 路径 时间戳 随机串 请求体文件
# 待签名串:方法、路径、规范查询串(无查询参数时为空行)、时间戳、随机串、请求体 SHA256
sign() {
local body_hash
body_hash=$(openssl dgst -sha256 -hex < "$5" | awk '{print $NF}')
printf '%s\n%s\n\n%s\n%s\n%s' "$1" "$2" "$3" "$4" "$body_hash" \
| openssl dgst -sha256 -hmac "$APP_SECRET" -hex | awk '{print $NF}'
}
# ==================== 一、创建认证会话 ====================
# return_url、notify_url 的域名必须是应用登记的业务域名或其子域名;biz_no 每个会话使用新的业务单号
body_file=$(mktemp)
trap 'rm -f "$body_file"' EXIT
printf '{"name":"%s","id_card_number":"%s","biz_no":"%s","return_url":"%s","notify_url":"%s","max_attempts":3}' \
'张三' '11010519491231002X' 'kyc-20261008-0101' \
'https://www.example.com/kyc/return' 'https://api.example.com/kyc/notify' > "$body_file"
path="${API_PREFIX}/face/enhanced/sessions"
timestamp=$(date +%s)
nonce=$(openssl rand -hex 16)
# HTTP 201 时 data.h5_url 为采集页链接,交给用户打开;保存 data.session_no 用于查询
curl -sS -X POST "${API_ORIGIN}${path}" --max-time 15 \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-H "X-App-Id: ${APP_ID}" \
-H "X-Timestamp: ${timestamp}" \
-H "X-Nonce: ${nonce}" \
-H "X-Signature: $(sign POST "$path" "$timestamp" "$nonce" "$body_file")" \
--data-binary "@${body_file}" \
-w '\nHTTP %{http_code}\n'
# ==================== 二、验签异步通知 ====================
# 用法:verify_notify 通知路径 X-App-Id X-Timestamp X-Nonce X-Signature 原始请求体文件
# 通过返回 0,失败返回 1;路径取 notify_url 的 path,这里假设不带查询参数
verify_notify() {
local now expected
now=$(date +%s)
[ "$2" = "$APP_ID" ] || return 1
[[ "$3" =~ ^[0-9]{1,12}$ ]] || return 1
[ $(( now > $3 ? now - $3 : $3 - now )) -le 300 ] || return 1
expected=$(sign POST "$1" "$3" "$4" "$6")
[ "$expected" = "$5" ] # 建议再记录 nonce,5 分钟内重复的直接拒绝,防止重放
}
# 在通知接口中(请求头与原始请求体由你的 Web 服务提供),通过后返回任意 2xx 即视为通知成功:
# if verify_notify /kyc/notify "$HDR_APP_ID" "$HDR_TIMESTAMP" "$HDR_NONCE" "$HDR_SIGNATURE" notify_body.json; then
# echo '验签通过:按 session_no 去重,data.passed 为 true 表示认证通过'
# fi
人脸实名认证(增强版)
创建认证会话
/api/open/v1/face/enhanced/sessions
创建一个增强版 H5 认证会话并返回采集页链接。除公共请求头外,需要携带 Content-Type: application/json。会话自创建起 15 分钟内有效;创建会话本身不计费。
请求参数(JSON 请求体)
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
name
|
string | 是 | 姓名,规则同「人脸实名认证」接口。只加密暂存到会话结束,平台不落库。 |
id_card_number
|
string | 是 | 18 位居民身份证号码,规则同「人脸实名认证」接口。只加密暂存到会话结束,平台不落库。 |
biz_no
|
string | 是 | 业务单号,1–64 位字母、数字、_ 和 -;同一应用内唯一,每个 biz_no 只能创建一个会话。 |
return_url
|
string | 否 | 认证结束后的跳回地址,完整的 http(s) 地址,不超过 1000 个字符;域名必须是应用登记的业务域名或其子域名。不传时采集页只显示结果。 |
notify_url
|
string | 否 | 异步通知地址,要求同 return_url;另外只能使用默认端口(http 80 / https 443,地址中不写端口),且域名必须解析到公网地址(不能是内网、本机或保留地址)。建议传入,见「异步通知与跳回」。 |
max_attempts
|
integer | 否 | 本会话最多认证次数,1–5,默认 3。认证服务异常不消耗次数。 |
product
|
string | 否 | 产品,可不传:会话产品由应用类型决定。传入时只能为 enhanced,且应用必须为增强版,否则返回 HTTP 422。 |
增强版的检测阈值由平台统一设置,不支持 quality_control、liveness_control、spoofing_control 参数。业务域名在「人脸实名认证服务 › 我的应用」的应用详情中登记。
请求示例
{
"name": "张三",
"id_card_number": "11010519491231002X",
"biz_no": "kyc-20261008-0201",
"return_url": "https://www.example.com/kyc/return",
"notify_url": "https://api.example.com/kyc/notify",
"max_attempts": 3
}
成功响应(HTTP 201)
字段与基础版的 创建认证会话 相同,data.product 为 face_enhanced。
| 字段 | 类型 | 说明 |
|---|---|---|
data.session_no
|
string | 平台会话号,FS 开头,共 24 位。 |
data.biz_no
|
string | 创建时传入的业务单号。 |
data.product
|
string | 会话所属产品,与应用类型一致:face_basic 基础版,face_enhanced 增强版。 |
data.status
|
string | 会话状态,见下方「会话状态」。 |
data.status_label
|
string | 会话状态的中文名称。 |
data.passed
|
boolean | 是否认证通过,等同于 status 为 passed。 |
data.attempts
|
integer | 已认证次数(认证服务异常不计入)。 |
data.max_attempts
|
integer | 最多认证次数。 |
data.verify_no
|
string | null | 最近一次认证的平台流水号,可用于查询认证结果接口(基础版 GET /face/basic/verifications/{verify_no},增强版 GET /face/enhanced/verifications/{verify_no});尚未认证时为 null。 |
data.result_code
|
string | null | 最近一次认证的结果码,见「结果码与错误码」。 |
data.result_msg
|
string | null | 最近一次认证结果的中文说明。 |
data.score
|
number | null | 最近一次认证的比对分数。 |
data.threshold
|
number | null | 最近一次认证使用的通过阈值。 |
data.charged
|
boolean | 是否计费,只有认证通过时为 true;一个会话最多计费 1 次。 |
data.charge_source
|
string | null | 扣费来源:package 次数包,balance 余额;未计费时为 null。 |
data.charge_amount
|
string | 余额扣费金额(元,4 位小数);使用次数包或未计费时为 0.0000。 |
data.expires_at
|
string | 会话过期时间,ISO 8601 格式(UTC)。 |
data.completed_at
|
string | null | 会话结束时间,ISO 8601 格式(UTC);未结束时为 null。 |
data.created_at
|
string | 会话创建时间,ISO 8601 格式(UTC)。 |
data.h5_url
|
string | 采集页链接,仅创建接口返回;请勿记录到日志或公开传播,链接即凭证。 |
{
"ok": true,
"data": {
"session_no": "FS2026100816100012345678",
"biz_no": "kyc-20261008-0101",
"product": "face_enhanced",
"status": "pending",
"status_label": "待认证",
"passed": false,
"attempts": 0,
"max_attempts": 3,
"verify_no": null,
"result_code": null,
"result_msg": null,
"score": null,
"threshold": null,
"charged": false,
"charge_source": null,
"charge_amount": "0.0000",
"expires_at": "2026-10-08T08:25:00+00:00",
"completed_at": null,
"created_at": "2026-10-08T08:10:00+00:00",
"h5_url": "https://am.siyun223.com/h5/face/7d2e9b417d2e9b417d2e9b417d2e9b417d2e9b417d2e9b417d2e9b417d2e9b41"
}
}
失败响应
HTTP 409 · 业务单号已创建过会话
error_code 为 biz_no_exists。每个 biz_no 只能创建一个会话;需要重新认证时请使用新的 biz_no,原会话结果可继续查询。
{
"ok": false,
"error_code": "biz_no_exists",
"message": "业务单号已创建过认证会话(FS2026100816100012345678),请使用新的 biz_no。"
}
HTTP 422 · 参数校验失败
error_code 为 validation_failed,errors 按字段列出原因,例如缺少 biz_no、return_url 不是应用业务域名、max_attempts 超出范围。
{
"ok": false,
"error_code": "validation_failed",
"message": "return_url(认证结束后的跳回地址)的域名必须是应用登记的业务域名 example.com 或其子域名。",
"errors": {
"return_url": [
"return_url(认证结束后的跳回地址)的域名必须是应用登记的业务域名 example.com 或其子域名。"
]
}
}
HTTP 503 · 服务暂不可用
error_code 为 service_unavailable:人脸实名认证服务暂未开放,或会话服务暂时不可用。请稍后用同一个 biz_no 重试。
{
"ok": false,
"error_code": "service_unavailable",
"message": "认证会话服务暂时不可用,请稍后重试。"
}
HTTP 403 · 应用类型与接口不符
error_code 为 app_product_mismatch:基础版应用调用了增强版接口。请使用类型为「增强版」的应用,或改为调用 /face/basic/*。
401 / 403 / 429 鉴权与限流错误见 状态码与错误码。
人脸实名认证(增强版)
查询认证会话
/api/open/v1/face/enhanced/sessions/{session_no}
按平台会话号查询会话状态与最近一次认证结果,只能查询本应用的会话。data 的字段与创建接口相同,但不含 h5_url。用户跳回你的页面后、或没有收到异步通知时,用本接口获取最终结果。
路径参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
session_no
|
string | 是 | 平台会话号,取创建会话返回的 data.session_no(FS 开头,共 24 位)。 |
会话状态(data.status)
| 取值 | 名称 | 说明 |
|---|---|---|
pending
|
待认证 | 会话已创建,用户尚未完成认证。 |
passed
|
认证通过 | 某一次认证通过,会话结束;本会话计费 1 次。 |
failed
|
认证未通过 | 认证次数已用完仍未通过,会话结束;不计费。 |
expired
|
已过期 | 超过有效期仍未完成,会话结束;不计费。 |
cancelled
|
用户取消 | 用户在采集页放弃认证,会话结束;不计费。 |
响应示例
{
"ok": true,
"data": {
"session_no": "FS2026100816100012345678",
"biz_no": "kyc-20261008-0101",
"product": "face_enhanced",
"status": "passed",
"status_label": "认证通过",
"passed": true,
"attempts": 2,
"max_attempts": 3,
"verify_no": "FV2026100816120387651234",
"result_code": "passed",
"result_msg": "认证通过",
"score": 91.276,
"threshold": 80.0,
"charged": true,
"charge_source": "package",
"charge_amount": "0.0000",
"expires_at": "2026-10-08T08:25:00+00:00",
"completed_at": "2026-10-08T08:12:04+00:00",
"created_at": "2026-10-08T08:10:00+00:00"
}
}
HTTP 404 · 会话不存在
会话号不存在、格式不正确,或不是本应用的会话。
{
"ok": false,
"error_code": "not_found",
"message": "认证会话不存在。"
}
人脸实名认证(增强版)
查询认证结果
/api/open/v1/face/enhanced/verifications/{verify_no}
按平台流水号查询会话内某一次认证的详细结果(含视频活体分数),只能查询本应用的记录。流水号取会话结果中的 data.verify_no。一般以会话结果为准,本接口用于对账或排查。
路径参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
verify_no
|
string | 是 | 平台流水号,取认证接口返回的 data.verify_no(FV 开头,共 24 位)。 |
响应字段
字段与基础版「人脸实名认证」接口的响应相同。增强版中 liveness_score 为视频活体分数(0–1),spoofing_score 为视频合成图检测分数,source 固定为 h5,product 为 face_enhanced。
| 字段 | 类型 | 说明 |
|---|---|---|
ok
|
boolean | 请求成功时为 true。认证未通过也返回 true,是否通过以 data.passed 为准。 |
data.verify_no
|
string | 平台流水号,FV 开头,共 24 位,用于查询认证结果与反馈问题。 |
data.biz_no
|
string | null | 请求中传入的业务单号,未传时为 null。 |
data.source
|
string | 认证来源:api 开放接口直接认证,h5 H5 采集页。 |
data.product
|
string | 产品:face_basic 基础版,face_enhanced 增强版。 |
data.status
|
string | 认证状态,见下方「认证状态」。 |
data.status_label
|
string | 认证状态的中文名称。 |
data.passed
|
boolean | 是否认证通过,等同于 status 为 passed。 |
data.result_code
|
string | 结果码,见「结果码与错误码」。出于隐私保护,姓名与证件号不符、查无证件、人脸与证件照不一致统一返回 identity_failed。 |
data.result_msg
|
string | 结果的中文说明,可直接展示给用户。 |
data.score
|
number | null | 人脸与证件照的比对分数,0–100;只在认证通过时返回,未通过时为 null。 |
data.threshold
|
number | null | 本次使用的通过阈值,默认 80(对应误识率约万分之一)。 |
data.verify_status
|
integer | null | 权威库核验状态,认证通过时为 0;未通过时为 null(不返回具体原因)。 |
data.liveness_score
|
number | null | 活体检测分数,未完成检测时为 null。 |
data.spoofing_score
|
number | null | 合成图检测分数,未完成检测时为 null。 |
data.charged
|
boolean | 本次是否计费。 |
data.charge_source
|
string | null | 扣费来源:package 次数包,balance 余额;未计费时为 null。 |
data.charge_amount
|
string | 余额扣费金额(元,4 位小数);使用次数包或未计费时为 0.0000。 |
data.idempotent
|
boolean | true 表示相同 biz_no 的重复提交,返回的是第一次的结果;查询接口固定为 false。 |
data.created_at
|
string | null | 受理时间,ISO 8601 格式(UTC)。 |
data.completed_at
|
string | null | 完成时间,ISO 8601 格式(UTC)。 |
响应示例
{
"ok": true,
"data": {
"verify_no": "FV2026100816120387651234",
"biz_no": "kyc-20261008-0201",
"source": "h5",
"product": "face_enhanced",
"status": "passed",
"status_label": "认证通过",
"passed": true,
"result_code": "passed",
"result_msg": "认证通过",
"score": 91.276,
"threshold": 80.0,
"verify_status": 0,
"liveness_score": 0.993215,
"spoofing_score": 0.00012,
"charged": true,
"charge_source": "package",
"charge_amount": "0.0000",
"idempotent": false,
"created_at": "2026-10-08T08:00:01+00:00",
"completed_at": "2026-10-08T08:00:02+00:00"
}
}
HTTP 404 · 记录不存在
流水号不存在、格式不正确,或不是本应用的记录。
{
"ok": false,
"error_code": "not_found",
"message": "认证记录不存在。"
}
人脸实名认证(增强版)
查询余额与次数
/api/open/v1/face/enhanced/account/balance
查询应用所属账户的余额、增强版次数包的可用次数与增强版按次单价。认证通过时先扣增强版次数包,没有可用次数时按 unit_price 从余额扣除。
请求参数
无。
响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
ok
|
boolean | 查询成功时为 true。 |
data.product
|
string | 接口所属产品:基础版接口为 face_basic,增强版接口为 face_enhanced。 |
data.balance
|
string | 账户余额(元,4 位小数),与短信服务共用同一个账户余额。 |
data.currency
|
string | 币种,固定为 CNY。 |
data.available_times
|
integer | 本产品生效中的次数包剩余可用次数(基础版与增强版的次数包分开计算),不含余额可支付的次数。 |
data.unit_price
|
string | null | 本产品次数包用完后按次扣余额的单价(元/次);平台未设置单价或服务未开放时为 null。 |
响应示例
{
"ok": true,
"data": {
"product": "face_enhanced",
"balance": "149.0000",
"currency": "CNY",
"available_times": 200,
"unit_price": "0.8000"
}
}
人脸实名认证(增强版)
异步通知与跳回
通知与跳回的规则、签名算法与基础版完全相同,通知请求体中的 data.product 为 face_enhanced。
异步通知
- 会话结束(认证通过、次数用完、过期或用户取消)后,平台向 notify_url 发送 POST 请求,请求体为 JSON:{"event":"face.session.completed","data":{…}},data 与查询会话的返回相同。
- 验签:待签名串 = POST、notify_url 的路径(path)、notify_url 中查询参数的规范查询串(没有时为空)、X-Timestamp、X-Nonce、原始请求体的 SHA256,用 \n 连接;与调用开放接口的签名算法完全相同,可直接复用签名代码。
- 你的服务返回任意 HTTP 2xx 即视为通知成功,响应内容不限;不会跟随重定向。
- 通知失败(非 2xx、超时或连接失败)时,按 1、5、30、120、360 分钟的间隔依次重试,全部失败后不再通知,请通过查询接口补查。
- 同一会话可能收到重复通知,请以 session_no 去重,按幂等方式处理。
通知请求头
| 请求头 | 类型 | 必填 | 说明 |
|---|---|---|---|
Content-Type
|
string | 是 | 固定为 application/json。 |
X-App-Id
|
string | 是 | 创建会话所用应用的 AppId。 |
X-Timestamp
|
string | 是 | 发送时的 Unix 时间戳(秒),建议校验与当前时间相差不超过 5 分钟。 |
X-Nonce
|
string | 是 | 32 位随机串,每次通知(含重试)都不同,可用于防重放。 |
X-Signature
|
string | 是 | 签名,算法与调用开放接口完全相同,使用同一应用的 AppSecret。 |
通知请求体示例
{
"event": "face.session.completed",
"data": {
"session_no": "FS2026100816100012345678",
"biz_no": "kyc-20261008-0101",
"product": "face_enhanced",
"status": "passed",
"status_label": "认证通过",
"passed": true,
"attempts": 2,
"max_attempts": 3,
"verify_no": "FV2026100816120387651234",
"result_code": "passed",
"result_msg": "认证通过",
"score": 91.276,
"threshold": 80.0,
"charged": true,
"charge_source": "package",
"charge_amount": "0.0000",
"expires_at": "2026-10-08T08:25:00+00:00",
"completed_at": "2026-10-08T08:12:04+00:00",
"created_at": "2026-10-08T08:10:00+00:00"
}
}
验签示例代码见 示例代码。
跳回参数
会话结束后,采集页在 return_url 后追加 session_no 与 biz_no 两个查询参数(原有查询参数与 # 片段保留),例如:
https://www.example.com/kyc/return?session_no=FS2026100816100012345678&biz_no=kyc-20261008-0101
跳回地址不带认证结果,参数也可能被用户篡改。收到跳回后,请用 session_no 调用 查询认证会话,或以异步通知的结果为准。
人脸实名认证(增强版)
结果码与错误码
结果码(data.result_code)
会话内每次认证完成后的结果码,可在查询会话、异步通知与查询认证结果中获取。是否计费以 data.charged 为准:只有 passed 计费;状态为调用异常(error)的结果码不消耗会话认证次数。
| 结果码 | 含义 | 认证状态 | 计费 | 说明 |
|---|---|---|---|---|
passed
|
认证通过 | 认证通过(passed) | 计费 | 动作与活体检测通过,且人证比对 verify_status 为 0、score 不低于 threshold。 |
action_failed
|
动作校验未通过 | 认证未通过(failed) | 不计费 | 未按提示完成随机动作,消耗 1 次认证次数。 |
liveness_failed
|
活体检测未通过 | 认证未通过(failed) | 不计费 | 视频活体检测未通过,可能是翻拍、录屏或视频回放。 |
spoofing_failed
|
合成图检测未通过 | 认证未通过(failed) | 不计费 | 视频合成图检测未通过,疑似 AI 合成或换脸。 |
deepfake_failed
|
换脸鉴别未通过 | 认证未通过(failed) | 不计费 | 换脸鉴别未通过,疑似 AI 换脸(平台启用换脸鉴别时)。 |
no_face
|
未检测到人脸 | 认证未通过(failed) | 不计费 | 视频中未检测到清晰的人脸。 |
multiple_faces
|
画面中有多张人脸 | 认证未通过(failed) | 不计费 | 视频中出现多张人脸,需确保只有本人入镜。 |
quality_failed
|
质量检测未通过 | 认证未通过(failed) | 不计费 | 视频中人脸质量较差(光线、角度、模糊或遮挡)。 |
identity_failed
|
身份核验未通过 | 认证未通过(failed) | 不计费 | 身份核验未通过:证件号码与姓名不匹配、权威库查无该证件,或人脸与证件照不是同一人。出于隐私保护不区分具体原因,也不返回比对分数。 |
risk_intercepted
|
数据源风险拦截 | 认证未通过(failed) | 不计费 | 同一身份信息短时间内重复认证,被数据源风险拦截,请稍后再试。 |
challenge_expired
|
动作校验已过期 | 调用异常(error) | 不计费 | 动作校验已过期(动作下发后太久才提交),不消耗认证次数,用户重新开始即可。 |
invalid_video
|
视频解析失败 | 调用异常(error) | 不计费 | 视频解析失败,不消耗认证次数,用户重新录制即可。 |
upstream_error
|
认证服务异常 | 调用异常(error) | 不计费 | 认证服务暂时异常(含换脸鉴别暂时不可用),不消耗认证次数。 |
upstream_timeout
|
认证服务超时 | 调用异常(error) | 不计费 | 认证服务响应超时,不消耗认证次数。 |
错误码(error_code)
ok 为 false 时返回,均不计费。认证服务异常与余额不足发生在用户提交认证时,体现在会话的认证结果与采集页提示中,不作为接口错误码返回。鉴权错误码(401 / 403 / 429)见 状态码与错误码。
| error_code | HTTP 状态码 | 说明 |
|---|---|---|
validation_failed
|
422 | 请求参数校验失败,errors 按字段列出原因;不计费。 |
biz_no_exists
|
409 | 创建 H5 认证会话时,该 biz_no 已创建过会话,请使用新的 biz_no;不计费。 |
daily_limit_exceeded
|
429 | 本应用今日(北京时间)的认证次数已达上限(每个应用每天 3,000 次,直接认证与创建 H5 会话分别计算),请明天再试或联系平台调整;不计费。 |
identity_restricted
|
403 | 该身份信息暂无法在平台内认证(直接认证、创建 H5 会话时返回;会话创建后才受限的,打开采集页或提交时会话直接结束为认证失败)。请引导用户联系平台运营查询;不计费。 |
not_found
|
404 | 认证记录或认证会话不存在,或不是本应用的;也用于接口路径错误。 |
service_unavailable
|
503 | 人脸实名认证服务暂未开放,或认证会话服务暂时不可用;不计费。 |
internal_error
|
500 | 平台内部异常,请稍后重试;传入 biz_no 重试可避免重复扣费。 |
app_service_mismatch
|
403 | 使用了短信服务的应用调用人脸接口,请使用「人脸实名认证」下创建的应用。 |
app_product_mismatch
|
403 | 应用类型与接口不符:基础版应用只能调用 /face/basic/*,增强版应用只能调用 /face/enhanced/*;不计费。 |
常见问题
返回 invalid_signature,怎么排查?
- 确认 X-App-Id 与 AppSecret 属于同一个应用,且密钥没有被重置过。
- 待签名串固定 6 行,行与行之间用 \n(LF)连接,不要使用 \r\n,最后一行末尾不加换行。
- 请求路径要包含 /api/open/v1 前缀,并与实际请求的路径完全一致(区分大小写,末尾不要多出 /)。
- 请求体 SHA256 要基于实际发送的原始字节计算;签名之后不要再修改、重新序列化或格式化 JSON。
- GET 请求没有请求体,第 6 行是空字符串的 SHA256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855。
- 签名结果使用十六进制小写,可以用「鉴权与签名」中的签名示例校验你的实现。
返回 timestamp_expired 怎么办?
调用方服务器的时间与平台相差超过 300 秒。请开启 NTP 时间同步;响应中的 server_time 为平台当前时间,可用于排查偏差。
重试时返回 nonce_reused?
X-Nonce 只能使用一次。重试时要重新生成 X-Timestamp 与 X-Nonce 并重新签名,发送接口的 Idempotency-Key 保持不变。
服务器 IP 已经在白名单里,为什么还返回 ip_not_allowed?
平台校验的是请求到达平台时的公网出口 IP,经过 NAT 网关或代理后,出口 IP 可能与服务器网卡上的 IP 不同。请以响应中的 client_ip 为准添加到白名单。
发送接口超时或返回 5xx,应该怎么重试?
使用相同的 Idempotency-Key 重试。如果首次请求其实已经成功,平台会直接返回首次的结果(replayed 为 true),不会重复发送或扣费。
发送接口返回 409,短信到底发出去了没有?
响应带有 batch_uuid 时,请调用「查询发送记录」确认:billing.status 为 refunded 表示未发出且已退费,为 pending_review 表示结果待核实。不要换新的 Idempotency-Key 重发,以免用户收到重复短信。
怎么知道短信是否送达?
调用「查询发送记录」,messages[].status 为 delivered 表示已送达、failed 表示发送失败(原因见 error_summary)。运营商状态回执是异步返回的,建议发送后间隔 10 秒以上再查询;超过 6 小时仍未收到回执的号码记为 timeout。全部号码都有结果后,批次 status 变为 completed,可以停止查询。
一条短信按多少条计费?
计费字数包含【签名】和替换变量后的正文,每 60 字计 1 条,不足 60 字按 1 条计算,详见「计费说明」。
没有变量的模板可以发送吗?
当前版本的 template_params 至少需要 1 项,暂不支持没有变量的模板,请使用带变量的模板发送。
可以在网页或 App 里直接调用吗?
不可以。开放 API 不支持浏览器跨域调用,而且客户端代码会泄露 AppSecret。请由你的服务端调用,再把结果返回给前端。
版本与更新
- 当前版本
- V1
- 接口路径前缀
/api/open/v1- 文档更新时间
- 2026-10-12(北京时间,按文档内容最后修改日期)
当前版本包含的接口
| 业务 | 接口数量 |
|---|---|
| 短信服务 | 4 |
| 短信服务 · 资质 / 签名 / 模板申请 | 18 |
| 人脸实名认证(基础版) | 5 |
| 人脸实名认证(增强版) | 4 |
各接口的方法与路径见 接口列表。
人脸实名认证接口路径调整
- 人脸实名认证按产品分为两组接口:基础版
/api/open/v1/face/basic/*,增强版/api/open/v1/face/enhanced/*,两组接口的参数、返回与签名方式相同,只接受对应类型的应用。 - 原有不带产品的路径(如
/api/open/v1/face/verify、/api/open/v1/face/sessions)继续可用,按应用类型自动处理;新接入请使用按产品区分的路径。
兼容性建议
- 版本号体现在接口路径前缀中,同一版本内请以本文档为准。
- 解析响应时请忽略不认识的字段,按 status、error_code 等取值判断,不要依赖 message 等文字说明。
- 遇到文档中没有列出的 status 或 error_code 取值时,请按失败或待核实处理并记录日志,不要直接视为成功。