| 方法 | 路径 | 说明 | 鉴权 |
|---|---|---|---|
| POST | /api/open/v1/verify | 提交核验 | HMAC 签名 |
| GET | /api/open/v1/query | 查询核验结果 | HMAC 签名 |
| GET | /api/open/v1/balance | 查询剩余次数 | HMAC 签名 |
本平台为无法自行申请支付宝实名认证资质的网站提供核验底座:支付宝侧的全部配置(AppID、密钥、回跳地址)由平台持有,接入方无需向支付宝登记任何域名。
| # | 步骤 | 说明 |
|---|---|---|
| 1 | 注册并实名 | 在用户中心注册账号,完成经营者本人实名认证 |
| 2 | 购买套餐 | 按次计费;调用核验前需有可用套餐次数(不足返回 1004) |
| 3 | 创建应用 | 获得 AppKey / AppSecret(Secret 仅创建时明文显示一次);可填写回调地址,留空则用查询接口取结果,随时可改 |
| 4 | 提交核验 | 调用 POST /api/open/v1/verify,响应含 certify_url |
| 5 | 用户完成人脸核验 | 引导用户打开 certify_url 完成认证 |
| 6 | 获取结果 | 用户完成后平台主动 POST 回调(约 5 秒内,见「结果获取时效」);也可轮询 GET /api/open/v1/query,查询会即时向支付宝核实 |
所有请求必须携带以下四个请求头。签名串按固定顺序拼接,空值参数不参与。
待签串 = METHOD + "\n"
+ PATH + "\n"
+ "k1=v1&k2=v2&..." + "\n" # 参数按 key 字典序,每项后都带 &
+ TIMESTAMP + "\n"
+ NONCE
sign = hex( HMAC-SHA256(app_secret, 待签串) ) # 小写十六进制
| 请求头 | 说明 |
|---|---|
X-App-Key | 应用 AppKey |
X-Timestamp | 秒级时间戳,与服务器相差不超过 5 分钟 |
X-Nonce | 随机串,5 分钟内不可重复(防重放) |
X-Sign | 上述算法算出的签名 |
提交核验的请求体为表单编码(application/x-www-form-urlencoded,不是 JSON)。参与签名的业务参数:out_order_no、type、name、idcard、phone(phone 仅三要素需要),空值不参与签名,签名使用解码后的参数值。
POST /api/open/v1/verify Content-Type: application/x-www-form-urlencoded X-App-Key: ak_xxxxxxxxxxxx X-Timestamp: 1789801118 X-Nonce: 8f3a9c2e5b1d X-Sign: <按签名规则计算> out_order_no=ORDER_001&type=face&name=张三&idcard=110101199003074258
type 取值:face 人脸核验 / two 二要素 / three 三要素(必须携带 phone)。成功响应 data.certify_url 即人脸核验跳转地址,引导用户打开完成认证。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
out_order_no | string | 是 | 商户订单号,同一应用内唯一,重复返回 1008 |
type | string | 否 | 核验类型:face 人脸(默认)/ two 二要素 / three 三要素 |
name | string | 是 | 真实姓名,至少 2 个字符 |
idcard | string | 是 | 身份证号,需通过校验位校验 |
phone | string | 三要素必填 | 手机号,仅 three 类型需要 |
以下三种语言可直接运行的最小实现,签名算法完全一致,任选其一接入。要点:签名用解码后的参数值按 key 字典序拼接(空值不参与);提交核验的请求体是表单编码(不是 JSON);签名结果为小写十六进制。
import hmac, hashlib, time, uuid, requests
APP_KEY = "ak_xxxxxxxx"
APP_SECRET = "你的应用Secret"
BASE = "https://平台域名"
def signed_headers(method, path, params):
ts = str(int(time.time()))
nonce = uuid.uuid4().hex # 每次请求唯一
kv = "".join(f"{k}={params[k]}&" for k in sorted(params) if params[k])
raw = method + "\n" + path + "\n" + kv + "\n" + ts + "\n" + nonce
sign = hmac.new(APP_SECRET.encode(), raw.encode(),
hashlib.sha256).hexdigest()
return {"X-App-Key": APP_KEY, "X-Timestamp": ts,
"X-Nonce": nonce, "X-Sign": sign}
# 1. 提交核验(表单编码)
params = {"out_order_no": "ORDER_001", "type": "face",
"name": "张三", "idcard": "110101199003074258"}
r = requests.post(BASE + "/api/open/v1/verify",
data=params,
headers=signed_headers("POST", "/api/open/v1/verify", params))
certify_url = r.json()["data"]["certify_url"] # 引导用户打开完成人脸核验
# 2. 查询结果(轮询)
q = {"out_order_no": "ORDER_001"}
r = requests.get(BASE + "/api/open/v1/query", params=q,
headers=signed_headers("GET", "/api/open/v1/query", q))
print(r.json()["data"]["passed"])
<?php
// 签名算法与 Python / Go 完全一致
$appKey = "ak_xxxxxxxx";
$appSecret = "你的应用Secret";
$base = "https://平台域名";
// 生成四个鉴权请求头
function signed_headers($method, $path, $params) {
global $appKey, $appSecret;
$ts = (string)time();
$nonce = bin2hex(random_bytes(8)); // 每次请求唯一
ksort($params); // 按 key 字典序
$kv = "";
foreach ($params as $k => $v) {
if (trim((string)$v) === "") continue; // 空值不参与
$kv .= $k . "=" . $v . "&"; // 每项后都带 &
}
$raw = strtoupper($method) . "\n" . $path . "\n" . $kv . "\n" . $ts . "\n" . $nonce;
$sign = hash_hmac("sha256", $raw, $appSecret); // 小写 hex
return [
"X-App-Key: $appKey",
"X-Timestamp: $ts",
"X-Nonce: $nonce",
"X-Sign: $sign",
];
}
// 统一调用:GET 拼 query,POST 用表单编码
function api_call($method, $path, $params) {
global $base;
$ch = curl_init();
$url = $base . $path;
if (strtoupper($method) === "GET") {
$url .= "?" . http_build_query($params);
} else {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
}
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, signed_headers($method, $path, $params));
$resp = curl_exec($ch);
curl_close($ch);
return json_decode($resp, true);
}
// 1. 提交核验(表单编码)
$r = api_call("POST", "/api/open/v1/verify", [
"out_order_no" => "ORDER_001",
"type" => "face",
"name" => "张三",
"idcard" => "110101199003074258",
]);
$certifyUrl = $r["data"]["certify_url"]; // 引导用户打开完成人脸核验
// 2. 查询结果(轮询)
$q = api_call("GET", "/api/open/v1/query", ["out_order_no" => "ORDER_001"]);
echo $q["data"]["passed"] ? "通过" : "未通过或处理中";
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"time"
)
const (
appKey = "ak_xxxxxxxx"
appSecret = "你的应用Secret"
base = "https://平台域名"
)
// buildSignString 构造待签串:METHOD\nPATH\nk1=v1&k2=v2&\nTIMESTAMP\nNONCE
func buildSignString(method, path string, params map[string]string, ts, nonce string) string {
keys := make([]string, 0, len(params))
for k, v := range params {
if strings.TrimSpace(v) == "" { // 空值不参与签名
continue
}
keys = append(keys, k)
}
sort.Strings(keys) // 按 key 字典序
var sb strings.Builder
sb.WriteString(strings.ToUpper(method))
sb.WriteString("\n")
sb.WriteString(path)
sb.WriteString("\n")
for _, k := range keys {
sb.WriteString(k)
sb.WriteString("=")
sb.WriteString(params[k])
sb.WriteString("&") // 每项后都带 &
}
sb.WriteString("\n")
sb.WriteString(ts)
sb.WriteString("\n")
sb.WriteString(nonce)
return sb.String()
}
// calcSign 计算 HMAC-SHA256 签名(小写十六进制)
func calcSign(secret, signStr string) string {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(signStr))
return hex.EncodeToString(mac.Sum(nil))
}
// signedHeaders 生成四个鉴权请求头
func signedHeaders(method, path string, params map[string]string) map[string]string {
ts := strconv.FormatInt(time.Now().Unix(), 10)
nonce := strconv.FormatInt(time.Now().UnixNano(), 36) // 每次请求唯一
return map[string]string{
"X-App-Key": appKey,
"X-Timestamp": ts,
"X-Nonce": nonce,
"X-Sign": calcSign(appSecret, buildSignString(method, path, params, ts, nonce)),
}
}
// do 发起请求:GET 拼 query,POST 用表单编码
func do(method, path string, params map[string]string) (map[string]interface{}, error) {
full := base + path
var body io.Reader
if strings.ToUpper(method) == "GET" {
q := url.Values{}
for k, v := range params {
q.Set(k, v)
}
full += "?" + q.Encode()
} else {
form := url.Values{}
for k, v := range params {
form.Set(k, v)
}
body = strings.NewReader(form.Encode())
}
req, err := http.NewRequest(strings.ToUpper(method), full, body)
if err != nil {
return nil, err
}
if strings.ToUpper(method) == "POST" {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
}
for k, v := range signedHeaders(method, path, params) {
req.Header.Set(k, v)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
raw, _ := io.ReadAll(resp.Body)
var out map[string]interface{}
if err := json.Unmarshal(raw, &out); err != nil {
return nil, err
}
return out, nil
}
func main() {
// 1. 提交核验
r, err := do("POST", "/api/open/v1/verify", map[string]string{
"out_order_no": "ORDER_001",
"type": "face",
"name": "张三",
"idcard": "110101199003074258",
})
if err != nil {
panic(err)
}
data := r["data"].(map[string]interface{})
fmt.Println("核验跳转地址:", data["certify_url"]) // 引导用户打开完成人脸核验
// 2. 查询结果(轮询)
q, _ := do("GET", "/api/open/v1/query", map[string]string{"out_order_no": "ORDER_001"})
fmt.Printf("核验结果: %v\n", q["data"].(map[string]interface{})["passed"])
}
GET /api/open/v1/balance 无业务参数(仅需签名头),返回当前应用所属账户的可用人脸核验次数与余额。
# Python
r = requests.get(BASE + "/api/open/v1/balance",
headers=signed_headers("GET", "/api/open/v1/balance", {}))
print(r.json()["data"])
# {"app_key":"ak_xxx","face_remain":100,"balance_free":0,"balance_yuan":"0.00"}
{
"code": 200,
"msg": "success",
"data": {
"order_no": "V20260919145429z83zLrrS",
"out_order_no": "ORDER_001",
"status": 0,
"certify_url": "https://...",
"qr_image": "data:image/png;base64,iVBORw0KGgo..."
}
}
支付宝扫码认证(推荐):data.qr_image 是 certify_url 的二维码(PNG 的 data URI),可直接 <img src="data.qr_image"> 渲染。用户在 PC 端用手机支付宝「扫一扫」扫描即可进入人脸核验,无需跳转。若场景更适合跳转,仍可用 certify_url;qr_image 生成失败时该字段为空串,退回使用 certify_url。
GET /api/open/v1/query?out_order_no=ORDER_001
{
"code": 200,
"msg": "success",
"data": {
"order_no": "V2026...",
"out_order_no": "ORDER_001",
"type": "face",
"status": 1,
"passed": true,
"message": "核验通过",
"create_time": "2026-10-06 11:56:40",
"finish_time": "2026-10-06 12:01:12"
}
}
查询参数:out_order_no(你的商户订单号)或 order_no(平台核验单号)二选一;两者都传时以 order_no 为准。
status 取值:0 处理中 / 1 通过 / 2 未通过。结果获取时效:查询接口每次被调用都会即时向支付宝核实并回写,用户完成后你下一次查询立即拿到终态;回调由平台结果同步任务驱动(见下节),用户完成后约 5 秒内自动推送,无需你先发起查询。两点注意:①「未通过」终态判定有 15 分钟宽限期——用户刚发起时上游返回的「未通过」只代表「尚未完成」,宽限期内 status 保持 0,防止误判;②核验单有效期有限,超期未完成上游将返回「认证已失效」,平台会把该单置为未通过并回调通知你。
创建应用时可填写回调地址,也可在「用户中心 → 应用管理 → 设置回调」随时修改或清空。核验出结果后平台主动 POST 到该地址(Content-Type: application/json;用户完成核验后约 5 秒内推送,由平台同步任务驱动,无需你先轮询),失败按退避策略重试(最多 6 次)。你的服务应返回 2xx(建议响应正文为 success)。
未配置回调地址时,请通过 GET /api/open/v1/query 轮询获取结果(支付宝官方流程即为查询制:认证结果以服务端查询为准,无需向支付宝登记任何域名)。
POST {你的回调地址}
X-Sign: <签名>
X-Timestamp: 1789801118
{"order_no":"V2026...","out_order_no":"ORDER_001",
"status":1,"passed":true,"message":"核验通过"}
| 字段 | 类型 | 说明 |
|---|---|---|
order_no | string | 平台核验单号 |
out_order_no | string | 你提交的商户订单号 |
type | string | 核验类型 face / two / three |
status | int | 0 处理中 / 1 通过 / 2 未通过 |
passed | bool | 是否通过(status==1) |
message | string | 结果说明(未通过时为原因) |
finish_time | string | 核验完成时间 |
timestamp | int | 秒级时间戳(与 X-Timestamp 一致,用于验签) |
回调验签(注意:签名输入是原始 body 字节,不是重新序列化的 JSON;时间戳与服务器相差超过 5 分钟应拒绝,防重放):
sign = HMAC-SHA256(app_secret, timestamp + "\n" + 原始body字节)
| 错误码 | 说明 |
|---|---|
| 200 | 成功 |
| 1001 | 签名错误 |
| 1002 | 时间戳过期或 nonce 重复(防重放) |
| 1004 | 余额不足 |
| 1005 | 参数错误 |
| 1006 | 频率超限 |
| 1007 | IP 不允许 |
| 1008 | 重复订单号 |
| 403 | 应用不存在或已停用(AppKey 无效) |