本文档面向接入商户,说明如何通过签名接口完成下单、查单、接收支付结果通知。
http://127.0.0.1:3456(生产环境替换为你的公网域名)HTTP POST + JSON | 编码:UTF-8 | 鉴权:merchant_id + secret_key + HMAC-SHA256 签名
接入前由平台方分配两个参数:
| 参数 | 说明 |
|---|---|
merchant_id | 商户号,全局唯一 |
secret_key | 商户密钥,仅商户与平台知晓,严禁泄露 |
凭证在后台「商户管理」中创建:POST /api/merchants,返回 merchant_id 和 secret_key。
所有接口的请求和响应都带 sign 签名,用于身份校验和防篡改。
sign 外的全部字段,含 merchant_id、timestamp、nonce、业务字段)。key1=value1&key2=value2&...(value 取原始值,不转义;空字符串不参与签名)。secret_key 计算 HMAC-SHA256,输出 16 进制小写字符串,即 sign。| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
merchant_id | string | ✓ | 商户号 |
timestamp | int | ✓ | 当前时间戳(秒)。与服务器时间差超过 ±300 秒会被拒绝(防重放) |
nonce | string | ✓ | 随机字符串,建议 16 位,防重放 |
sign | string | ✓ | 签名结果(不参与签名计算) |
假设商户号 M1001,密钥 abc123,下单参数:
{
"merchant_id": "M1001",
"timestamp": 1793018723,
"nonce": "8fb066158f90fe71",
"amount": 1000
}
排序后拼接串(空值 notify_url 不参与):
amount=1000&merchant_id=M1001&nonce=8fb066158f90fe71×tamp=1793018723
sign = HMAC_SHA256("abc123", 上面字符串) 的 16 进制小写
POST /api/v1/order/create
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
merchant_id | string | ✓ | 商户号 |
timestamp | int | ✓ | 时间戳(秒) |
nonce | string | ✓ | 随机串 |
sign | string | ✓ | 签名 |
amount | int | ✓ | 下单金额(单位:分),必须为商品单价的整数倍。如商品单价 100 分(¥1),传 1000 即购买 10 件 |
notify_url | string | ✗ | 支付结果回调地址;不传则用商品默认配置 |
{
"code": 0,
"msg": "ok",
"data": {
"order_no": "E20261026204529052006191",
"price": 100,
"pay_url": "https://cashier.youzan.com/pay/buyer?...",
"alipay_h5_url": "https://cashier.youzan.com/assets/c-cashier/alipay-h5?...",
"alipay_open_url": "https://render.alipay.com/p/s/i?scheme=..."
},
"sign": "4c90fd7a32538d6ac17a1b6de15c2abf4f7e30f620ff015c2243488571e6bea0"
}
| 字段 | 说明 |
|---|---|
order_no | 订单号,查单主键,务必保存 |
price | 金额(单位:分) |
pay_url | 有赞收银台(需登录有赞,自用/调试) |
alipay_h5_url | 支付宝 H5 页(copy-link) |
alipay_open_url | 给买家付款用:浏览器点开 → 唤起支付宝 App → 免登录直接付款 |
sign | 服务端对 data 字段的签名(用商户密钥验签) |
alipay_open_url:任意浏览器/微信点开,自动唤起支付宝 App 收银台,全程无需登录有赞。
POST /api/v1/order/query
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
merchant_id | string | ✓ | 商户号 |
timestamp | int | ✓ | 时间戳(秒) |
nonce | string | ✓ | 随机串 |
sign | string | ✓ | 签名 |
order_no | string | ✓ | 下单返回的订单号 |
{
"code": 0,
"msg": "ok",
"data": {
"order_no": "E20261026204529052006191",
"paid": false,
"status": 0,
"status_text": "初始/未支付",
"price": 100,
"real_pay": 100,
"pay_state": "ALL_UNPAID"
},
"sign": "b8cfb9e831082711209babaeb5b4c85f5a2dce9d595dce5d933a70afe1e55f04"
}
status| 值 | 含义 |
|---|---|
| 0 | 初始/未支付 |
| 1 | 申请失败 |
| 2 | 处理中 |
| 3 | 已付款 ✅ |
| 4 | 成功 ✅ |
| 5 | 失败 |
| 6 | 已关闭 |
| 7 | 已退票 |
paid = true 即视为支付成功。status >= 5 为终态(失败/关闭)。
商户在下单时传 notify_url,平台 worker 轮询到订单支付成功(或进入终态)后,主动 POST 到该地址。
{
"merchant_id": "M1001",
"order_no": "E20261026204529052006191",
"kdt_id": "213478012",
"price": 100,
"paid": true,
"status": 3,
"statusText": "已付款",
"payState": "ALL_PAID",
"orderStatus": 20,
"orderStatusText": "待发货",
"realPay": 100,
"paidAt": 1793018723000,
"timestamp": 1793018723,
"nonce": "a1b2c3d4e5f60718",
"sign": "计算出的签名"
}
{
"merchant_id": "M1001",
"order_no": "E20261026204529052006191",
"kdt_id": "213478012",
"price": 100,
"paid": false,
"status": 6,
"statusText": "已关闭",
"timestamp": 1793018723,
"nonce": "a1b2c3d4e5f60718",
"sign": "计算出的签名"
}
secret_key 重算 sign 并比对,防止伪造回调。timestamp 在合理时间窗口内。order_no 是已下单的订单号。paidAt 相同或已处理过则直接忽略。{"code":0,"msg":"success"} 或 HTTP 200,否则平台会按轮询间隔重试。通知里的 sign 是对除 sign 外的全部字段(含 merchant_id、timestamp、nonce、业务字段)按 key 字典序排序后计算 HMAC-SHA256。空值字段不参与签名。
| code | 含义 |
|---|---|
| 0 | 成功 |
| 400 | 参数错误(商品不存在 / 无可用商品 / 订单不存在) |
| 401 | 鉴权失败(签名错误 / 商户不存在 / 商户停用 / 时间戳超时) |
| 403 | 无权查询该订单 |
| 500 | 服务端异常 |
const crypto = require("crypto");
function sign(params, secretKey) {
const keys = Object.keys(params)
.filter((k) => params[k] !== undefined && params[k] !== null && params[k] !== "")
.sort();
const str = keys.map((k) => `${k}=${params[k]}`).join("&");
return crypto.createHmac("sha256", secretKey).update(str, "utf8").digest("hex");
}
// 下单
const body = {
merchant_id: "M1001",
timestamp: Math.floor(Date.now() / 1000),
nonce: crypto.randomBytes(8).toString("hex"),
amount: 1000, // 下单金额(分), 如 ¥10 = 1000, 必须是商品单价的整数倍
notify_url: "https://你的回调地址/paid",
};
body.sign = sign(body, "你的secret_key");
const res = await fetch("http://127.0.0.1:3456/api/v1/order/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
}).then((r) => r.json());
console.log(res.data.alipay_open_url); // 给买家付款的链接
import hmac, hashlib, time, secrets, json, requests
def sign(params, secret_key):
items = {k: v for k, v in params.items() if v not in (None, "")}
s = "&".join(f"{k}={items[k]}" for k in sorted(items))
return hmac.new(secret_key.encode(), s.encode(), hashlib.sha256).hexdigest()
body = {
"merchant_id": "M1001",
"timestamp": int(time.time()),
"nonce": secrets.token_hex(8),
"amount": 1000, # 下单金额(分)
"notify_url": "https://你的回调地址/paid",
}
body["sign"] = sign(body, "你的secret_key")
r = requests.post("http://127.0.0.1:3456/api/v1/order/create", json=body).json()
print(r["data"]["alipay_open_url"])
function sign(array $params, string $secretKey): string {
$params = array_filter($params, fn($v) => $v !== null && $v !== '');
ksort($params);
$s = '';
foreach ($params as $k => $v) $s .= "$k=$v&";
$s = rtrim($s, '&');
return hash_hmac('sha256', $s, $secretKey);
}
$body = [
'merchant_id' => 'M1001',
'timestamp' => time(),
'nonce' => bin2hex(random_bytes(8)),
'amount' => 1000, // 下单金额(分)
'notify_url' => 'https://你的回调地址/paid',
];
$body['sign'] = sign($body, '你的secret_key');
$ch = curl_init('http://127.0.0.1:3456/api/v1/order/create');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$res = json_decode(curl_exec($ch), true);
echo $res['data']['alipay_open_url'];
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"sort"
"strings"
)
func Sign(params map[string]string, secretKey string) string {
keys := make([]string, 0, len(params))
for k, v := range params {
if v != "" {
keys = append(keys, k)
}
}
sort.Strings(keys)
parts := make([]string, 0, len(keys))
for _, k := range keys {
parts = append(parts, k+"="+params[k])
}
str := strings.Join(parts, "&")
h := hmac.New(sha256.New, []byte(secretKey))
h.Write([]byte(str))
return hex.EncodeToString(h.Sum(nil))
}
商户接收支付通知后,用同样算法验证 sign:
const crypto = require("crypto");
function verify(notifyBody, secretKey) {
const { sign, ...rest } = notifyBody;
const keys = Object.keys(rest)
.filter((k) => rest[k] !== undefined && rest[k] !== null && rest[k] !== "")
.sort();
const str = keys.map((k) => `${k}=${rest[k]}`).join("&");
const expected = crypto.createHmac("sha256", secretKey).update(str, "utf8").digest("hex");
return expected === sign;
}
app.post("/paid", (req, res) => {
if (!verify(req.body, "你的secret_key")) {
return res.status(401).json({ code: 401, msg: "签名错误" });
}
// 校验时间戳
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - req.body.timestamp) > 300) {
return res.json({ code: 400, msg: "时间戳超时" });
}
// 处理订单发货逻辑...
console.log(`订单 ${req.body.order_no} 已支付, 实付 ${req.body.realPay} 分`);
res.json({ code: 0, msg: "success" }); // 必须返回成功, 否则会重试
});
merchant_id 和 secret_keymerchant_id / timestamp / nonce / signtimestamp 用秒,与服务器时间偏差 < 300 秒order_noalipay_open_urlnotify_url 可公网访问,收到通知后验签 + 幂等 + 返回 {"code":0,"msg":"success"}