- {{ ln }}
{{ apiSecret }}
网关地址:{{ apiInfo.gateway }} 所有接口 POST + application/json(UTF-8)。
一、通用约定
- 金额一律用「分」(整数):1 元 = 100,0.5 元 = 50,禁止传小数(如 1.00)。
- 每个请求都要带 4 个公共参数并参与签名:
app_id应用ID、timestampUnix 秒(与服务器偏差 ≤300 秒)、nonce随机串(300 秒内不可重复)、sign签名。 - 响应统一为
{ "code": 0, "msg": "ok", "data": {...} }:code=0表示接口成功,业务结果看data.status。
二、签名算法(HMAC-SHA256)
- 取除
sign外、值非空的所有参数; - 按参数名 ASCII 升序排序;
- 拼成
k1=v1&k2=v2&...(值原样,不做 URL 编码); - 以
app_secret为密钥做 HMAC-SHA256,输出十六进制小写即sign。
待签名串示例(参数 amount=100 / app_id=qcabc / bind_id=12 / nonce=x9f / out_trade_no=T1 / timestamp=1720000000):
amount=100&app_id=qcabc&bind_id=12&nonce=x9f&out_trade_no=T1×tamp=1720000000
const crypto = require('crypto');
function sign(params, secret) {
const str = Object.keys(params)
.filter(k => k !== 'sign' && params[k] != null && params[k] !== '')
.sort()
.map(k => `${k}=${params[k]}`)
.join('&');
return crypto.createHmac('sha256', secret).update(str, 'utf8').digest('hex');
}
function sign(array $params, string $secret): string {
unset($params['sign']);
$params = array_filter($params, fn($v) => $v !== null && $v !== '');
ksort($params);
$pairs = [];
foreach ($params as $k => $v) { $pairs[] = "$k=$v"; }
return hash_hmac('sha256', implode('&', $pairs), $secret);
}
import hmac, hashlib
def sign(params: dict, secret: str) -> str:
items = sorted((k, v) for k, v in params.items()
if k != 'sign' and v not in (None, ''))
base = '&'.join(f'{k}={v}' for k, v in items)
return hmac.new(secret.encode(), base.encode(), hashlib.sha256).hexdigest()
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.*;
static String sign(Map<String,String> params, String secret) throws Exception {
TreeMap<String,String> m = new TreeMap<>();
for (Map.Entry<String,String> e : params.entrySet())
if (!"sign".equals(e.getKey()) && e.getValue() != null && !e.getValue().isEmpty())
m.put(e.getKey(), e.getValue());
StringBuilder sb = new StringBuilder();
for (Map.Entry<String,String> e : m.entrySet()) {
if (sb.length() > 0) sb.append('&');
sb.append(e.getKey()).append('=').append(e.getValue());
}
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256"));
byte[] raw = mac.doFinal(sb.toString().getBytes("UTF-8"));
StringBuilder hex = new StringBuilder();
for (byte b : raw) hex.append(String.format("%02x", b));
return hex.toString();
}
import time, uuid, requests
params = {
"app_id": "qcabc...",
"timestamp": str(int(time.time())),
"nonce": uuid.uuid4().hex,
"out_trade_no": "T20260714001", # 商户订单号,幂等键
"bind_id": "12", # 已绑卡ID
"amount": "100", # 单位:分(1元=100)
"goods_name": "会员服务费",
"notify_url": "https://your.com/api/notify",
}
params["sign"] = sign(params, "YOUR_APP_SECRET")
r = requests.post("https://qiancipay.com/api/openapi/pay", json=params, timeout=30)
print(r.json())
# -> {"code":0,"data":{"out_trade_no":"T20260714001","trade_no":"Q1784180262695873","pay_order_no":"59ce7e561a6948838f5dd85260a9c43c","amount":100,"fee":1,"status":"success"}}
三、接口明细
路径均相对网关地址;金额字段单位为分。以下只列业务参数(公共参数 app_id/timestamp/nonce/sign 每个请求都要带)。
快捷绑卡:返回银行授权地址,由持卡人跳转银行完成授权与选卡,无需上送卡号。
| 参数 | 必填 | 说明 |
|---|---|---|
| out_user_id | 是 | 商户侧用户唯一标识(同一用户复用) |
| name | 是 | 持卡人姓名 |
| id_card_num | 是 | 持卡人身份证号 |
| bank_code | 是 | 开户银行代码,取自下方「银行代码表」(也可 GET /openapi/banks 拉取) |
| phone | 是 | 持卡人手机号(11位);仅平台留存,便于后续联系持卡人 |
| notify_url | 否 | 绑卡结果异步通知地址 |
| return_url | 否 | 银行授权完成后用户浏览器返回页 |
// 请求
{
"app_id": "qcabc...",
"timestamp": "1720000000",
"nonce": "x9f...",
"out_user_id": "U10001",
"name": "张三",
"id_card_num": "110101199001011234",
"bank_code": "CCB",
"phone": "13800001234",
"notify_url": "https://your.com/api/notify",
"sign": "9a1c..."
}
// 响应
{
"code": 0,
"msg": "ok",
"data": {
"bind_id": 12,
"out_user_id": "U10001",
"status": "submitting",
"redirect_url": "https://<银行授权页地址>"
}
}
引导用户打开 redirect_url 完成授权;最终结果以「绑卡查询」或异步通知为准(bound 已绑定 / failed 失败)。
银行代码表(bank_code 取值,快捷绑卡支持的银行;也可 GET /openapi/banks 免签拉取。⚠️ 暂不支持工商银行 ICBC / 招商银行 CMB,仅借记卡):
| bank_code | 银行 | bank_code | 银行 |
|---|---|---|---|
| PINGANBANK | 平安银行 | HXB | 华夏银行 |
| BOC | 中国银行 | BCCB | 北京银行 |
| ECITIC | 中信银行 | CCB | 建设银行 |
| QDTH | 青岛银行 | POST | 中国邮政储蓄 |
| GDB | 广发银行 | CMBC | 民生银行 |
参数:bind_id(推荐)或 out_user_id。
// 响应
{
"code": 0,
"msg": "ok",
"data": {
"bind_id": 12,
"out_user_id": "U10001",
"bank_code": "CCB",
"card_type": "DEBIT_CARD",
"card_mask": "**** 4230",
"name_mask": "张*",
"status": "bound",
"created_at": "2026-07-14 10:00:00"
}
}
参数:bind_id。响应 data:{ "bind_id": 12, "status": "unbound" }
| 参数 | 必填 | 说明 |
|---|---|---|
| out_trade_no | 是 | 商户订单号(幂等键,同商户唯一) |
| bind_id | 是 | 已绑定成功的绑卡ID |
| amount | 是 | 扣款金额(分,正整数) |
| goods_name | 否 | 商品/服务名称 |
| notify_url | 否 | 扣款结果异步通知地址 |
// 响应
{
"code": 0,
"msg": "ok",
"data": {
"out_trade_no": "T20260714001",
"trade_no": "Q1784180262695873",
"pay_order_no": "59ce7e561a6948838f5dd85260a9c43c",
"amount": 100,
"fee": 1,
"status": "success"
}
}
三种订单号:out_trade_no 商户订单号(贵司自己的单号)/trade_no 平台订单号(我方唯一单号,Q 开头,建议贵司存此号用于查单对账)/pay_order_no 支付公司订单号(下单成功才有;失败为 null)。
status:success 成功 / failed 失败 / paying 银行处理中(以通知为准)。fee 为本笔技术服务费(分),从预付费余额扣。重复 out_trade_no 返回首次结果并带 "idempotent": true。
参数:out_trade_no。
// 响应 data
{
"out_trade_no": "T20260714001",
"trade_no": "Q1784180262695873",
"pay_order_no": "59ce7e561a6948838f5dd85260a9c43c",
"amount": 100,
"fee": 1,
"settle_fee": 2,
"refunded_amount": 0,
"status": "success",
"paid_at": "2026-07-14 10:05:00"
}
| 参数 | 必填 | 说明 |
|---|---|---|
| out_refund_no | 是 | 商户退款单号(幂等键,同商户唯一) |
| out_trade_no | 是 | 原扣款订单号 |
| amount | 否 | 退款金额(分);不传则退全部剩余可退 |
| notify_url | 否 | 退款结果异步通知地址 |
// 响应 data
{
"out_refund_no": "R20260714001",
"refund_no": "Q1784180377261987",
"pay_refund_no": "41e74e67c0044cccb08fa79fde2685ca",
"amount": 100,
"status": "success"
}
refund_no 平台退款单号(Q 开头字符串)/pay_refund_no 支付公司退款流水(受理成功才有)。status:success / failed / processing(以通知为准)。支持部分退款、多次退款(累计不超过原金额)。
参数:out_refund_no。响应 data:{ out_refund_no, refund_no, pay_refund_no, amount, status, done_at }
无业务参数。balance/fee_min/fee_max 单位分,fee_rate 为小数(如 0.003 = 0.3%)。
// 响应 data
{ "balance": 94800, "fee_rate": 0.003, "fee_min": 100, "fee_max": 5000 }
四、异步通知(notify)
扣款/退款/绑卡到终态时,若该请求带了 notify_url,我方会向该地址 POST 一个 JSON 通知,通知体带我方签名(同第二节算法,密钥为你的 app_secret)。你需验签确认来源,处理成功后返回纯文本 SUCCESS;否则我方按退避策略重试(30s、1m、5m、15m、30m、1h、2h、6h,最多 8 次)。
{
"app_id": "qcabc...",
"timestamp": "1720000100",
"nonce": "7c2...",
"event": "payment",
"out_trade_no": "T20260714001",
"trade_no": "Q1784180262695873",
"pay_order_no": "59ce7e561a6948838f5dd85260a9c43c",
"amount": 100,
"fee": 1,
"status": "success",
"sign": "..."
}
// 绑卡通知 event=bindcard:含 bind_id, out_user_id, bank_code, card_mask, status
// 退款通知 event=refund:含 out_refund_no, out_trade_no, refund_no, pay_refund_no, amount, status
@app.route("/api/notify", methods=["POST"])
def notify():
body = request.get_json()
if sign(body, "YOUR_APP_SECRET") != body.get("sign"):
return "FAIL", 400 # 验签不通过
# TODO: 按 body["event"] 与 status 更新你的订单...
return "SUCCESS" # 收妥,务必返回纯文本 SUCCESS
五、错误码
| code | 说明 |
|---|---|
| 0 | 成功 |
| 4001 / 4003 / 4004 | 缺 app_id / app_id 无效 / 签名校验失败 |
| 4002 | timestamp 偏差过大 或 nonce 重复(防重放) |
| 4005 / 4006 / 4007 / 4008 | 未开通API / 商户停用 / 未签约 / 未进件开通 |
| 4201 / 4204 | 金额须为正整数(分) / 预付费余额不足 |
| 4202 / 4203 | 绑卡不存在 / 该卡未完成绑卡 |
| 4205 | 下单失败(含银行返回原因) |
| 4305 | 退款金额超出可退范围 |
| 5000 / 5001 | 系统错误 / 支付通道未配置 |
status 语义:success 成功(终态)· failed 失败(终态)· paying/processing 处理中(以异步通知或查询接口为准)。