思云网络服务代理商平台 开发文档
文档目录

开放 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 位小数

接入指南

快速开始

  1. 按业务创建应用

    短信服务在「短信服务 › 我的应用」中创建,人脸实名认证在「人脸实名认证服务 › 我的应用」中创建,获取 AppId 与 AppSecret。完整的 AppSecret 只在创建成功和重置后显示一次,请立即保存到服务端配置。

  2. 配置 IP 白名单(建议)

    在应用中填写服务器的公网出口 IP,只允许你的服务器调用。

  3. 实现签名

    按「鉴权与签名」计算 X-Signature。两种业务的签名方式完全相同,可以共用同一份签名代码。

  4. 调用短信接口

    先调用「查询可用短信模板」记下 variant.id 与 signature.id,再调用「发送短信」,用返回的 batch_uuid 调用「查询发送记录」获取送达状态。

  5. 申请自己的资质、签名与模板(可选)

    不使用平台共享签名时,依次提交企业实名资质、短信签名与短信模板,平台审核通过后提交短信通道审核,审核结果通过各自的查询接口获取。

  6. 调用人脸实名认证接口

    基础版应用调用 /face/basic/*:自行采集照片时调用「人脸实名认证」同步返回结果,或使用平台托管的 H5 采集页。增强版应用调用 /face/enhanced/*,只支持 H5 采集页(录制动作视频)。H5 采集页都是先创建认证会话,用户刷脸完成后平台向 notify_url 发送异步通知,也可主动查询认证会话。

注册并登录后,在用户中心对应业务的「我的应用」中创建应用,即可获得 AppId 与 AppSecret,无需审核。

应用按业务线区分

每个应用只属于一条业务线,只能调用该业务线的接口。两种应用的签名方式完全相同。

应用的业务线
业务线 在用户中心创建 可调用的接口
短信服务 短信服务 › 我的应用 /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,请求体为图片原始字节。

签名步骤

  1. 计算请求体 SHA256:对实际发送的原始请求体字节计算 SHA256,结果为 64 位十六进制小写;GET 请求没有请求体,按空字符串计算。
  2. 拼接待签名串:按下表顺序拼接 6 行,行与行之间用换行符 \n(LF)连接,最后一行末尾不加换行。
  3. 计算签名:以 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 行为空行)

TEXT
POST
/api/open/v1/sms/send

1760000000
5f2b8c4e9a1d4e7f8b3c6a9d0e1f2a3b
905ebaf8fc3cdab4a244e57d71daa5866b2849b5622af86c26b4e12b34a0e294

完整请求

HTTP
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 状态码

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 为应用所属账号申请自己的资质、签名和模板。申请记录与网页端「资质管理 / 签名管理 / 模板管理」是同一份数据,提交后进入相同的审核流程:待平台审核 → 平台审核通过后提交短信通道审核 → 同步审核结果。审核结果没有回调,请按需调用查询接口获取。

  1. 提交资质

    上传营业证件、身份证件与授权委托书照片,获取经办人手机验证码后提交资质。资质归属应用所属账号,与网页端「资质管理」是同一份数据。

  2. 等待资质审核

    平台初审通过后提交短信通道审核,按资质详情的 status 查询进度;驳回时 rejection_reason 给出原因,可修改后重新提交。

  3. 申请签名

    资质 usable_for_signature 为 true 后,用它的 id 提交签名申请;平台审核通过后提交短信通道审核。

  4. 申请模板

    选择本人已通过的签名或平台共享签名提交模板;审核通过后详情的 send_options 给出发送短信时使用的 template_variant_id 与 signature_id。

  5. 发送短信

    模板通过后也会出现在「查询可用短信模板」中,按「发送短信」接口发送即可。

调用限制

申请类接口的调用限制
项目 限制
调用频率 每个应用每分钟最多 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 7.4 及以上,需要 curl 扩展;保存为 open_api_demo.php,执行 php open_api_demo.php

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

运行环境:Python 3.6 及以上,只使用标准库;保存为 open_api_demo.py,执行 python3 open_api_demo.py

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

运行环境:Java 11 及以上,只使用 JDK 标准库;保存为 OpenApiDemo.java,执行 javac -encoding UTF-8 OpenApiDemo.java && java OpenApiDemo

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

运行环境:Node.js 18 及以上,无需安装依赖;保存为 open-api-demo.mjs,执行 node open-api-demo.mjs

JavaScript
// 短信开放 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

运行环境:bash、curl 与 openssl;保存为 open-api-demo.sh,执行 bash open-api-demo.sh

Bash
#!/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 7.4 及以上,需要 curl 扩展;保存为 sms_apply_demo.php,把 proof.jpg 放在同一目录,执行 php sms_apply_demo.php

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 及以上,只使用标准库;保存为 sms_apply_demo.py,把 proof.jpg 放在同一目录,执行 python3 sms_apply_demo.py

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 标准库;保存为 SmsApplyDemo.java,把 proof.jpg 放在同一目录,执行 javac -encoding UTF-8 SmsApplyDemo.java && java SmsApplyDemo

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 及以上,无需安装依赖;保存为 sms-apply-demo.mjs,把 proof.jpg 放在同一目录,执行 node sms-apply-demo.mjs

JavaScript
// 短信签名 / 模板申请示例(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

运行环境:bash、curl、openssl;保存为 sms-apply-demo.sh,把 proof.jpg 放在同一目录,执行 bash sms-apply-demo.sh

Bash
#!/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

申请企业实名资质

企业实名资质是 申请短信签名 的前提。提交后为「待平台审核」,平台初审通过后提交短信通道审核,结果通过 查询企业实名资质 获取。经办人(以及手机号与经办人不同的责任人)需要完成手机验证码验证,验证码只发到本人手机,请向本人获取后填写。

第一步:上传材料图片

POST /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。
JSON
{
    "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"
    }
}

第二步:获取手机验证码

POST /api/open/v1/sms/qualifications/verify-codes
获取手机验证码的请求参数
参数 类型 必填 说明
role string 是 operator(经办人)或 responsible(责任人)。
mobile string 是 接收验证码的 11 位手机号,须与提交资质时填写的经办人 / 责任人手机号一致。

验证码 5 分钟内有效,最多可输错 5 次;同一手机号 60 秒内只能获取一次,每个应用每天最多 20 次。响应中的 resend_after 为可再次获取的等待秒数,expires_in 为验证码有效秒数。

JSON
{
    "role": "operator",
    "mobile": "13800000000"
}
JSON
{
    "ok": true,
    "data": {
        "role": "operator",
        "role_label": "经办人",
        "mobile": "138****0000",
        "resend_after": 60,
        "expires_in": 300
    }
}

第三步:提交资质

POST /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 港澳台居民居住证

请求示例

示例中的证件号码与信用代码仅作格式演示,平台会校验校验位,不能直接提交。

JSON
{
    "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)

JSON
{
    "ok": false,
    "error_code": "invalid_parameter",
    "message": "验证码不正确,还可以再试 4 次。",
    "errors": {
        "operator_mobile_code": [
            "验证码不正确,还可以再试 4 次。"
        ]
    }
}

短信资质 API

查询企业实名资质

GET /api/open/v1/sms/qualifications?status={status}&page={page}&per_page={per_page}
GET /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 已失效 资质在短信通道已不可用(关闭或过期)。

详情响应示例

JSON
{
    "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

修改企业实名资质

PUT /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)

JSON
{
    "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 · 当前状态不能修改

JSON
{
    "ok": false,
    "error_code": "invalid_state",
    "message": "当前状态的资质不能修改。"
}

短信资质 API

撤回企业实名资质申请

POST /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)

JSON
{
    "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 · 当前状态不能撤回

JSON
{
    "ok": false,
    "error_code": "invalid_state",
    "message": "平台已开始处理该资质,不能撤回。"
}

短信签名 API

申请短信签名

使用本账号名下已通过的企业实名资质申请自己的短信签名。提交后为「待平台审核」,平台审核通过后提交短信通道审核,结果通过 查询短信签名 获取。同一签名内容在平台内只能申请一次;已是平台共享签名的内容无需申请,可直接使用。还没有资质时,请先 申请企业实名资质。

第一步:上传材料图片

POST /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。
JSON
{
    "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"
    }
}

第二步:提交签名申请

POST /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 商标全称

请求示例

JSON
{
    "qualification_id": 8,
    "content": "示例科技",
    "sign_source": 1,
    "scene": "用于向注册用户发送登录验证码与订单通知",
    "files": {
        "other": [
            "file_token"
        ]
    }
}

成功响应(HTTP 201)

data 为签名详情,字段见 查询短信签名。

JSON
{
    "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

查询短信签名

GET /api/open/v1/sms/signatures?status={status}&page={page}&per_page={per_page}
GET /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 已过期 签名已过期。

详情响应示例

JSON
{
    "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

修改短信签名

PUT /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)

JSON
{
    "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 · 当前状态不能修改

JSON
{
    "ok": false,
    "error_code": "invalid_state",
    "message": "当前状态的签名不能修改。"
}

短信签名 API

撤回短信签名申请

POST /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)

JSON
{
    "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 · 当前状态不能撤回

JSON
{
    "ok": false,
    "error_code": "invalid_state",
    "message": "平台已开始处理该签名,不能撤回。"
}

短信模板 API

申请短信模板

POST /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 营销

请求示例

JSON
{
    "name": "登录验证码",
    "channel_type": "CN_OTP",
    "signature_id": 23,
    "content": "您的验证码为${code},5分钟内有效,请勿泄露给他人。"
}

成功响应(HTTP 201)

data 为模板申请详情,字段见 查询短信模板申请。刚提交时 status 为 pending_review,send_options 为空数组。

短信模板 API

查询短信模板申请

GET /api/open/v1/sms/templates/applications?status={status}&page={page}&per_page={per_page}
GET /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 已失效 模板所用签名或模板在短信通道已不可用。

列表响应示例

JSON
{
    "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
        }
    }
}

详情响应示例(审核通过后)

JSON
{
    "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

修改短信模板

PUT /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 · 当前状态不能修改

JSON
{
    "ok": false,
    "error_code": "request_rejected",
    "message": "当前状态的模板不能修改。"
}

短信模板 API

撤回短信模板申请

POST /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)

JSON
{
    "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 · 当前状态不能撤回

JSON
{
    "ok": false,
    "error_code": "request_rejected",
    "message": "平台已开始处理该模板,不能撤回。"
}

短信发送 API

查询可用短信模板

GET /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 短信通道名称。

响应示例

JSON
{
    "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

发送短信

POST /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 项,当前暂不支持没有变量的模板。

请求示例

JSON
{
    "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 重发。

JSON
{
    "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 提交。

JSON
{
    "ok": false,
    "error_code": "insufficient_funds",
    "message": "余额和流量包额度不足:本次需 2 条,流量包可用 0 条,余额可支付 0 条(单价 ¥0.045/条)。"
}

HTTP 409 · 发送失败

通道明确未受理,本次扣费已自动退还,billing.status 为 refunded。修正问题后,请使用新的 Idempotency-Key 重新发送。

JSON
{
    "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 重发,以免重复发送。

JSON
{
    "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 查询结果即可,不会重复发送。

JSON
{
    "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。

JSON
{
    "message": "手机号格式无效。 (另有 1 项错误)",
    "errors": {
        "phone_numbers.0": [
            "手机号格式无效。"
        ],
        "template_params": [
            "模板参数不能为空。"
        ]
    }
}

HTTP 500 · 服务内部异常

平台内部异常,短信未提交到通道。请保持 Idempotency-Key 不变稍后重试:如果首次请求已生成批次,重试会直接返回该批次的结果,不会重复发送。

JSON
{
    "ok": false,
    "error_code": "internal_error",
    "message": "服务暂时异常,请保持 Idempotency-Key 不变,稍后重试。"
}

短信发送 API

查询发送记录

GET /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。

响应示例

JSON
{
    "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

查询余额

GET /api/open/v1/account/balance

查询应用所属账户的余额与流量包可用条数。发送时先扣流量包,不足部分按通道单价从余额扣除。

请求参数

无。

响应字段

查询余额的响应字段
字段 类型 说明
ok boolean 查询成功时为 true。
data object 账户信息。
data.balance string 账户余额(元,4 位小数)。
data.currency string 币种,固定为 CNY。
data.available_segments integer 生效中流量包的剩余可用条数,不含余额可支付的条数。

响应示例

JSON
{
    "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 7.4 及以上,需要 curl 扩展;保存为 face_verify_demo.php,把 face.jpg 放在同一目录,执行 php face_verify_demo.php

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 及以上,只使用标准库;保存为 face_verify_demo.py,把 face.jpg 放在同一目录,执行 python3 face_verify_demo.py

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,把 face.jpg 放在同一目录,执行 javac -encoding UTF-8 FaceVerifyDemo.java && java FaceVerifyDemo

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 及以上,无需安装依赖;保存为 face-verify-demo.mjs,把 face.jpg 放在同一目录,执行 node face-verify-demo.mjs

JavaScript
// 人脸实名认证(基础版)调用示例(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

运行环境:bash、curl、openssl 与 base64;保存为 face-verify-demo.sh,把 face.jpg 放在同一目录,执行 bash face-verify-demo.sh

Bash
#!/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 7.4 及以上,需要 curl 扩展;创建会话部分可直接执行 php face_h5_session.php;验签部分放到 notify_url 对应的接口中

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

运行环境:Python 3.6 及以上,只使用标准库;创建会话部分可直接执行 python3 face_h5_session.py;verify_notify() 在 notify_url 对应的接口中调用

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

运行环境:Java 11 及以上,只使用 JDK 标准库;保存为 FaceH5Demo.java,执行 javac -encoding UTF-8 FaceH5Demo.java && java FaceH5Demo 会创建会话并在 8080 端口接收通知

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

运行环境:Node.js 18 及以上,无需安装依赖;保存为 face-h5-session.mjs,执行 node face-h5-session.mjs 会创建会话并在 8080 端口接收通知

JavaScript
// 人脸实名认证(基础版)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

运行环境:bash、curl 与 openssl;保存为 face-h5-demo.sh,执行 bash face-h5-demo.sh 创建会话;verify_notify 供通知接口验签时调用

Bash
#!/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

人脸实名认证(基础版)

人脸实名认证

POST /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,对应误识率约万分之一。

请求示例

JSON
{
    "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。

认证通过

JSON
{
    "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"
    }
}

认证未通过(活体检测未通过,不计费)

JSON
{
    "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 稍后重试。

JSON
{
    "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 为第一条原因。本次未发起认证,不计费。

JSON
{
    "ok": false,
    "error_code": "validation_failed",
    "message": "证件号码不正确,请传入 18 位居民身份证号码。",
    "errors": {
        "id_card_number": [
            "证件号码不正确,请传入 18 位居民身份证号码。"
        ],
        "image": [
            "图片分辨率过大,需不超过 1920×1080。"
        ]
    }
}

HTTP 402 · 次数包与余额不足

次数包已用完且余额不足以支付本次认证(insufficient_funds),或次数包已用完且平台未设置单价(price_not_set)。本次未发起认证,不计费。

JSON
{
    "ok": false,
    "error_code": "insufficient_funds",
    "message": "人脸实名认证次数包已用完,账户余额 ¥0.2000 不足以支付本次认证(¥0.5000/次),请充值或购买次数包。"
}

HTTP 409 · 相同业务单号正在处理

相同 biz_no 的第一次请求还没有完成。稍后用同一个 biz_no 重新提交即可拿到第一次的结果,不会重复扣费。

JSON
{
    "ok": false,
    "error_code": "processing",
    "message": "业务单号对应的认证(FV2026100816000112345678)正在处理中,请稍后查询结果。"
}

其他错误(503 服务暂未开放、500 平台内部异常,以及 401 / 403 / 429 鉴权与限流错误)见 结果码与错误码。

人脸实名认证(基础版)

查询认证结果

GET /api/open/v1/face/basic/verifications/{verify_no}

按平台流水号查询认证记录,只能查询本应用的记录。data 的字段与认证接口相同,idempotent 固定为 false。认证接口是同步返回的,正常情况下不需要轮询,本接口用于对账或补查。

路径参数

查询认证结果的路径参数
参数 类型 必填 说明
verify_no string 是 平台流水号,取认证接口返回的 data.verify_no(FV 开头,共 24 位)。

响应示例

JSON
{
    "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 · 记录不存在

流水号不存在、格式不正确,或不是本应用的记录。

JSON
{
    "ok": false,
    "error_code": "not_found",
    "message": "认证记录不存在。"
}

人脸实名认证(基础版)

查询余额与次数

GET /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。

响应示例

JSON
{
    "ok": true,
    "data": {
        "product": "face_basic",
        "balance": "149.0000",
        "currency": "CNY",
        "available_times": 500,
        "unit_price": "0.5000"
    }
}

人脸实名认证(基础版)

H5 采集页接入

不想自己开发采集界面时,可以使用平台托管的 H5 采集页:你的服务端创建认证会话,把返回的采集页链接交给用户,用户在手机浏览器中完成刷脸,平台完成人证比对后跳回你的页面并发送异步通知。签名鉴权与计费口径与 人脸实名认证 接口相同,使用同一个人脸实名认证应用。

动作活体

  • 采集页每次随机下发 2 个动作(从眨眼、张嘴、向左转头、向右转头中抽取),用户按提示现场完成,用于防范照片、录屏视频等冒用。
  • 页面实时识别动作并提示调整位置与光线;动作完成后,关键画面会提交平台服务端再次复核,复核通过后才使用正脸照片进行质量检测、活体检测、合成图检测与人证比对。
  • 动作未完成或复核未通过时不进行人证比对、不计费、不消耗认证次数,用户可直接重试;同一会话动作复核失败次数过多时会话结束(状态为认证未通过)。
  • 采集页需要浏览器支持摄像头与 WebAssembly,主流手机浏览器与微信内置浏览器均可使用;首次打开需加载约 8MB 的识别组件,之后由浏览器缓存。

接入流程

  1. 服务端创建会话

    你的服务端调用 POST /face/basic/sessions,传入姓名、身份证号码与业务单号,拿到 h5_url。

  2. 引导用户打开采集页

    把 h5_url 发给用户(跳转、短信或二维码均可),链接 15 分钟内有效,只能由本人使用。

  3. 用户在采集页刷脸

    用户同意授权后按提示完成刷脸;未通过时可在剩余次数内重试。

  4. 跳回与通知

    认证结束后跳回 return_url,同时向 notify_url 发送异步通知。

  5. 服务端确认结果

    以异步通知或 GET /face/basic/sessions/{session_no} 的查询结果为准,不要信任跳回页面的参数。

注意事项

  • 会话自创建起 15 分钟有效,过期后状态变为 expired,需要使用新的 biz_no 重新创建。
  • 一个会话内可多次尝试,未通过时消耗 1 次认证次数;认证服务异常不消耗认证次数,也不计费。
  • 一个会话最多计费 1 次:只有认证通过的那一次计费,未通过与异常都不计费。
  • 姓名、证件号码只在会话有效期内加密暂存,会话结束即删除,平台不落库;采集页只显示脱敏后的姓名,不显示证件号码。
  • h5_url 即访问凭证,请只发给认证本人,不要写入日志或公开传播。

跳回 return_url 时只追加 session_no 与 biz_no,不带认证结果。请务必以异步通知或查询接口的结果为准,不要根据用户是否跳回来判断认证通过。

人脸实名认证(基础版)

创建认证会话

POST /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 的域名必须是该域名本身或其子域名。

请求示例

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,
    "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 采集页链接,仅创建接口返回;请勿记录到日志或公开传播,链接即凭证。
JSON
{
    "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,原会话结果可继续查询。

JSON
{
    "ok": false,
    "error_code": "biz_no_exists",
    "message": "业务单号已创建过认证会话(FS2026100816100012345678),请使用新的 biz_no。"
}

HTTP 422 · 参数校验失败

error_code 为 validation_failed,errors 按字段列出原因,例如缺少 biz_no、return_url 不是应用业务域名、max_attempts 超出范围。

JSON
{
    "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 重试。

JSON
{
    "ok": false,
    "error_code": "service_unavailable",
    "message": "认证会话服务暂时不可用,请稍后重试。"
}

401 / 403 / 429 鉴权与限流错误见 状态码与错误码。

人脸实名认证(基础版)

查询认证会话

GET /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 用户取消 用户在采集页放弃认证,会话结束;不计费。

响应示例

JSON
{
    "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 · 会话不存在

会话号不存在、格式不正确,或不是本应用的会话。

JSON
{
    "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。

通知请求体示例

JSON
{
    "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 两个查询参数(原有查询参数与 # 片段保留),例如:

URL
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 次认证次数;
  • 动作校验过期、视频解析失败、认证服务异常或超时:不计费,也不消耗认证次数,用户可直接重试;
  • 会话过期或用户取消:不计费。

每个结果码是否计费,见 结果码与错误码。

人脸实名认证(增强版)

接入流程

  1. 创建增强版应用

    在「人脸实名认证服务 › 我的应用」创建应用,类型选择「增强版」,并登记业务域名。应用类型创建后不能修改。

  2. 服务端创建会话

    你的服务端调用 POST /face/enhanced/sessions,传入姓名、身份证号码与业务单号,拿到 h5_url。

  3. 用户在采集页完成动作

    把 h5_url 发给用户(15 分钟内有效)。用户同意授权后按提示依次完成随机动作,采集页同时录制几秒钟的视频并提交。

  4. 平台检测与比对

    平台依次进行动作校验与视频活体检测、换脸鉴别(启用时)、人证比对;未通过时用户可在剩余次数内重试。

  5. 跳回与通知

    认证结束后跳回 return_url,同时向 notify_url 发送异步通知,规则与基础版相同。

  6. 服务端确认结果

    以异步通知或 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 7.4 及以上,需要 curl 扩展;创建会话部分可直接执行 php face_h5_session.php;验签部分放到 notify_url 对应的接口中

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

运行环境:Python 3.6 及以上,只使用标准库;创建会话部分可直接执行 python3 face_h5_session.py;verify_notify() 在 notify_url 对应的接口中调用

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

运行环境:Java 11 及以上,只使用 JDK 标准库;保存为 FaceH5Demo.java,执行 javac -encoding UTF-8 FaceH5Demo.java && java FaceH5Demo 会创建会话并在 8080 端口接收通知

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

运行环境:Node.js 18 及以上,无需安装依赖;保存为 face-h5-session.mjs,执行 node face-h5-session.mjs 会创建会话并在 8080 端口接收通知

JavaScript
// 人脸实名认证(增强版)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

运行环境:bash、curl 与 openssl;保存为 face-h5-demo.sh,执行 bash face-h5-demo.sh 创建会话;verify_notify 供通知接口验签时调用

Bash
#!/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

人脸实名认证(增强版)

创建认证会话

POST /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 参数。业务域名在「人脸实名认证服务 › 我的应用」的应用详情中登记。

请求示例

JSON
{
    "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 采集页链接,仅创建接口返回;请勿记录到日志或公开传播,链接即凭证。
JSON
{
    "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,原会话结果可继续查询。

JSON
{
    "ok": false,
    "error_code": "biz_no_exists",
    "message": "业务单号已创建过认证会话(FS2026100816100012345678),请使用新的 biz_no。"
}

HTTP 422 · 参数校验失败

error_code 为 validation_failed,errors 按字段列出原因,例如缺少 biz_no、return_url 不是应用业务域名、max_attempts 超出范围。

JSON
{
    "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 重试。

JSON
{
    "ok": false,
    "error_code": "service_unavailable",
    "message": "认证会话服务暂时不可用,请稍后重试。"
}

HTTP 403 · 应用类型与接口不符

error_code 为 app_product_mismatch:基础版应用调用了增强版接口。请使用类型为「增强版」的应用,或改为调用 /face/basic/*。

401 / 403 / 429 鉴权与限流错误见 状态码与错误码。

人脸实名认证(增强版)

查询认证会话

GET /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 用户取消 用户在采集页放弃认证,会话结束;不计费。

响应示例

JSON
{
    "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 · 会话不存在

会话号不存在、格式不正确,或不是本应用的会话。

JSON
{
    "ok": false,
    "error_code": "not_found",
    "message": "认证会话不存在。"
}

人脸实名认证(增强版)

查询认证结果

GET /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)。

响应示例

JSON
{
    "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 · 记录不存在

流水号不存在、格式不正确,或不是本应用的记录。

JSON
{
    "ok": false,
    "error_code": "not_found",
    "message": "认证记录不存在。"
}

人脸实名认证(增强版)

查询余额与次数

GET /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。

响应示例

JSON
{
    "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。

通知请求体示例

JSON
{
    "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 两个查询参数(原有查询参数与 # 片段保留),例如:

URL
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 取值时,请按失败或待核实处理并记录日志,不要直接视为成功。