易支付 pro
适用于 易支付 pro。下载后请参照包内说明安装配置。
从获取密钥到下单、查单、支付通知和退款,按完整流程完成接入。支持原生 JSON 网关与易支付兼容协议,地址自动使用当前站点域名。
商户后台 → 接入中心,获取 AppKey 与 AppSecret。
使用 V2 签名后 POST /api/gateway/orders,跳转收银台。
验签并核对订单金额,持久化支付结果与发货任务后应答 SUCCESS。
| 用途 | 地址 / 配置 |
|---|---|
| 平台基础地址 | {BASE} |
| 商户登录 | {BASE}/merchant/login |
| 在线接口文档 | {BASE}/docs |
| 创建订单 | POST {BASE}/api/gateway/orders |
| 查询订单 | POST {BASE}/api/gateway/orders/query |
| 申请退款 | POST {BASE}/api/gateway/refunds |
| 支付收银台 | 使用下单响应中的 cashierUrl,不要自行拼接 |
| 异步回调地址 | 商户自行部署,例如 https://shop.example.com/payment/notify |
| 支付完成跳转地址 | 商户自行部署,例如 https://shop.example.com/payment/return |
回调地址由接入方提供。 下单时把自己网站的通知接口填入 notifyUrl,平台支付成功后向该地址通知。它不是平台网关地址,也不是上游渠道通知平台的地址。
orderNo,引导用户进入返回的 cashierUrl。notifyUrl 发送异步通知。SUCCESS;重复通知不能重复发货。returnUrl 后,由商户服务端查单,再展示真实支付结果。接口调用成功不等于付款成功。 code=0 只说明本次原生接口调用成功,订单付款结果需要检查 data.status。
原生下单、查询和退款统一使用 POST、UTF-8 JSON,请求头如下:
Content-Type: application/json X-App-Key: 你的AppKey
原生接口金额均为整数「分」:1.00 元传 100,10.00 元传 1000。请求金额不要传小数、浮点元或数字字符串。时间字段通常为 RFC3339,时间戳使用 Unix 秒。
{"code": 0, "message": "ok", "data": {}}
非 0 时读取 message 并保留业务订单编号;不要只检查 HTTP 状态码。
新接入推荐统一使用 V2,退款必须使用 V2。以下字段要添加到每个接口的业务参数中:
| 字段 | 类型 | 要求 |
|---|---|---|
| signType | string | 固定 HMAC-SHA256-V2 |
| timestamp | string | 当前 Unix 秒级时间戳;允许过去 300 秒、未来 30 秒 |
| nonce | string | 每次请求新生成的 16~64 位字母、数字、下划线或连字符 |
| sign | string | 按下面规则计算的签名 |
同一商户跨接口也不能复用 nonce。请求重试时重新生成 timestamp、nonce 和 sign,业务订单号或退款编号按原业务保留。
sign,其余字段都参与签名,包括空字符串、signType、timestamp、nonce。不要传 null、数组或嵌套对象;extra 先序列化为 JSON 字符串。frame(s) 为「s 的 UTF-8 字节长度的十进制文本 + 冒号 + s」。中文按字节计算,不按字符数。fourpay-gateway-v2\n 开头,其中 \n 为一个真实换行字符。frame("POST")、frame(接口路径)、frame(AppKey)。接口路径不含域名、查询串或末尾多余斜杠。frame(字段名) + frame(字段值),整数转为无小数点的十进制文本。不再添加其他分隔符。原文 = "fourpay-gateway-v2\n"
+ frame("POST") + frame("/api/gateway/orders") + frame(AppKey)
+ 按字段名排序追加 frame(字段名) + frame(字段值)
sign = Base64URL无填充(HMAC-SHA256(AppSecret, 原文))
原生下单、查单仍兼容旧 MD5:排除 sign 和空值,按字段名排序拼接 k=v&k=v,追加 &key=AppSecret,取 MD5 大写。参数名不能包含 & 或 =,参数值不能包含 &。复杂回调 URL 或 extra 请用 V2。旧 MD5 不能申请退款。平台原生回调仍用 MD5 验签,与请求使用 V2 无关。
POST {BASE}/api/gateway/orders
| 业务字段 | 必填 | 类型 / 说明 |
|---|---|---|
| merchantOrderNo | 是 | string,商户系统内唯一订单号 |
| amount | 是 | int,正整数分,10.00 元传 1000 |
| subject | 是 | string,商品标题 |
| notifyUrl | 是 | string,商户接收通知的公网 HTTPS 地址 |
| returnUrl | 否 | string,商户支付结果页面,仅作展示 |
| payMethod | 否 | string,alipay / wxpay / qqpay 等,以实际开通为准;留空由收银台选择 |
| channelCode | 否 | string,通常留空;wxpay / alipay / qqpay 按对应品牌轮询本商户可用子商户并覆盖 payMethod;留空或 0 按 payMethod 路由;其他编码指定通道 |
| expireMinutes | 否 | int,1~1440 分钟,未传或超出范围按 30 分钟处理 |
| extra | 否 | string,JSON 字符串,可传 openid / buyerId / payer 等支付账号信息,按通道要求提供 |
| clientIp | 否 | string,真实付款用户 IP;未传时使用请求 IP,服务端代下单时可传真实客户端 IP |
以上字段还需加上 V2 公共字段。下方 timestamp、nonce、sign 是说明占位值,实际调用请使用示例函数生成:
{
"merchantOrderNo": "SHOP202609140001",
"amount": 1000,
"subject": "测试商品",
"notifyUrl": "https://shop.example.com/payment/notify",
"returnUrl": "https://shop.example.com/payment/return",
"payMethod": "alipay",
"expireMinutes": 30,
"signType": "HMAC-SHA256-V2",
"timestamp": "发送时的Unix秒级时间戳",
"nonce": "每次请求新生成的随机字符串",
"sign": "计算得到的签名"
}
成功响应示例(部分通道还会返回 payUrl、payType):
{
"code": 0,
"message": "ok",
"data": {
"orderNo": "Pxxxxxxxx",
"merchantOrderNo": "SHOP202609140001",
"cashierUrl": "{BASE}/pay/Pxxxxxxxx"
}
}
保存 orderNo 与本地订单的对应关系,然后跳转返回的 cashierUrl。同一 merchantOrderNo 原单待支付时可返回原单;已终态会报订单号已存在。下单超时先用原订单号查单,不要直接换号重复创建。
POST {BASE}/api/gateway/orders/query
| 业务字段 | 要求 |
|---|---|
| orderNo | 平台订单号,与 merchantOrderNo 二选一 |
| merchantOrderNo | 商户订单号,与 orderNo 二选一 |
请求体除下面的业务字段外,还需带 V2 公共字段,并携带 X-App-Key:
{"merchantOrderNo": "SHOP202609140001"}
{
"code": 0,
"message": "ok",
"data": {
"orderNo": "Pxxxxxxxx",
"merchantOrderNo": "SHOP202609140001",
"amount": 1000,
"fee": 0,
"actualAmount": 1000,
"refundedAmount": 0,
"status": "SUCCESS",
"channelCode": "实际通道编码",
"payMethod": "alipay",
"createdAt": "2026-09-14T02:00:00Z",
"paidAt": "2026-09-14T02:01:00Z"
}
}
amount 是原订单金额,actualAmount 是商户实收金额,refundedAmount 是累计已退款金额,均为分。手续费不改变回调中用于核验的原订单金额。未付款时 paidAt 可能为 null。
原生查询对待支付订单会尝试向上游核实;是否更新取决于通道查询能力及响应。不要把待支付或查询失败解释为支付成功。
| 状态 | 含义 |
|---|---|
| PENDING | 待支付 |
| SUCCESS | 支付成功 |
| FAILED | 支付失败 |
| CLOSED | 已关闭 |
| REFUNDING | 退款中 |
| REFUNDED | 已全额退款 |
| PARTIAL_REFUND | 部分退款 |
支付、退款结果均以实际返回的订单状态和金额核对,不能仅凭接口 code 判断。
回调地址由商户自行部署,通过下单字段 notifyUrl 传入,例如 https://shop.example.com/payment/notify。使用可公网访问的最终 HTTPS 地址,避免 301/302 跳转、登录校验或页面验证码阻断通知。
平台向该地址发送 POST JSON:
{
"orderNo": "Pxxxxxxxx",
"merchantOrderNo": "SHOP202609140001",
"amount": "1000",
"status": "SUCCESS",
"channelCode": "实际通道编码",
"payTime": "2026-09-14T02:01:00Z",
"sign": "平台生成的MD5大写签名"
}
amount 为原订单金额(分的字符串);payTime 可能为空字符串。部分订单还带 payerOpenid、payerAppid。验签使用实际收到的全部字段,不能只选择示例里的字段。
k=v&k=v。&key=你的AppSecret,计算 MD5 并转成 32 位大写十六进制。下单用 V2,回调仍用 MD5。易支付兼容回调又是另一套格式,见对应章节。
HTTP/1.1 200 OK Content-Type: text/plain SUCCESS
正文直接返回 SUCCESS,不要返回 JSON、HTML、NOT SUCCESS 或带解释的句子。处理失败时不能先应答成功再丢弃业务。
启用持久化通知队列后,新支付成功订单每轮最多尝试 3 次,最多自动执行 5 轮,后续间隔依次为 1 分钟、5 分钟、30 分钟、2 小时。网络结果不确定、发送中断或结果保存失败时停止自动重试,需核实后人工补发;商户必须按订单号幂等处理。历史未加入队列的订单保留每次最多 3 次的原规则,不自动补发。未确认订单可主动查单核对;后台通知记录显示队列状态及下次执行时间。
例如 https://shop.example.com/payment/return,由商户自行部署并在下单时传入。页面应调用自己的服务端查单后展示真实结果。用户访问此页面、页面参数或浏览器跳转都不能作为发货依据。
这类地址供配置微信、支付宝或其他支付插件使用,普通接入商户不需要填到自己的 notifyUrl。具体使用插件配置页或平台下单时生成的地址,不要自行猜测替换。
| 用途 | 路由说明 |
|---|---|
| 插件支付通知入口 | {BASE}/api/plugin/notify/{pluginCode},pluginCode 使用实际插件编码 |
| 旧通道通知入口 | {BASE}/api/gateway/notify/{channelCode},仅按对应旧通道配置使用 |
| 商户收款通知 | 商户自行提供的 notifyUrl,不能填写上述平台内部入口 |
POST {BASE}/api/gateway/refunds
| 业务字段 | 要求 |
|---|---|
| orderNo / merchantOrderNo | 二选一,目标支付订单 |
| merchantRefundNo | 必填,本次退款唯一编号,1~64 位字母、数字、下划线或连字符 |
| refundAmount | 必填,正整数分;支持部分退款,累计不能超过原单金额 |
| reason | 可选,退款原因 |
退款必须加上 V2 公共字段并携带 X-App-Key,旧 MD5 退款会被拒绝。业务参数示例:
{
"merchantOrderNo": "SHOP202609140001",
"merchantRefundNo": "REFUND202609140001",
"refundAmount": 1000,
"reason": "用户申请退款"
}
保存响应中的 refundNo 和 merchantRefundNo,核对订单状态及 refundedAmount。通道是否支持退款、是否即时完成,以实际能力和响应为准。
同一次退款超时、失败或结果未知时,保留原 merchantRefundNo、原订单、金额和原因,只更新 timestamp、nonce、sign。不能更换退款编号重试未知结果,更换编号代表一笔新的退款。
重复成功请求会返回原退款编号;处理中或未知结果需要继续核对,不能仅因接口返回就认定资金已退。当前公开网关没有单独的退款单查询端点,可通过订单查询核对累计已退款金额,异常退款到平台后台进一步核查。
系统同时提供原生 JSON 网关和易支付兼容接口。两种协议的字段、金额单位、成功码和签名规则不能混用。
| 用途 | 地址 |
|---|---|
| 页面跳转下单 | GET/POST {BASE}/submit.php,成功后跳转收银台 |
| API 下单 | GET/POST {BASE}/mapi.php,返回 JSON |
| 查询订单 | GET/POST {BASE}/api.php,act=order |
| 平台 RSA 公钥 | {BASE}/api/epay/platform-public-key |
请求使用 Query 或 application/x-www-form-urlencoded 表单,不是 JSON。
| 配置项 / 下单字段 | 内容 |
|---|---|
| 网关地址 | {BASE},配置基础地址时不要附带 /docs 或接口路径 |
| pid | 本平台商户号(也支持商户 ID),不是原生 X-App-Key |
| 商户密钥 | 经典 MD5 模式使用本商户 AppSecret |
| type | alipay / wxpay / qqpay 等已开通方式 |
| out_trade_no | 商户唯一订单号 |
| name | 商品标题 |
| money | 金额单位为元,例如 10.00,不是 1000 分 |
| notify_url | 商户自己的公网 HTTPS 通知地址 |
| return_url | 可选,商户支付结果页 |
| sign_type / sign | 签名类型与签名 |
排除 sign、sign_type 和空值,按字段名 ASCII 升序拼接 k=v&k=v,直接追加 AppSecret,计算 MD5 小写。不要混用原生接口的 &key= 规则。参数值不能包含 &;需要复杂 URL 或附加参数时改用原生 V2。
sign = MD5小写(排序后的参数原文 + AppSecret) sign_type = MD5
RSA/RSA2 模式需先配置商户公钥,由商户私钥签请求;通知签名可能采用 RSA,应按实际 sign_type 使用平台公钥验签,不能把 RSA 通知当成 MD5。
mapi.php 成功返回 code=1、trade_no、payurl,使用 payurl 跳转;失败返回 code=-1 和 msg。
查单传 pid、act=order,以及 trade_no / out_trade_no 二选一,并计算签名。act=order 也要参与签名,不支持只传 pid + key 查单。 当前 api.php 只支持 order,不支持退款等其他 act。status=1 表示已有支付时间,status=0 表示尚无支付时间;它不是原生接口完整的支付/退款状态枚举,也不执行原生查询的上游补单流程。
查单响应附带的签名只覆盖 trade_no、out_trade_no、type、pid、name、money、status,按此字段集合验签,不把 code、msg、addtime、endtime 加入响应验签原文。
易支付异步通知采用 GET,包含 pid、type、name、money、trade_no、out_trade_no、trade_status、sign、sign_type。验签并核对订单、元金额及 trade_status=TRADE_SUCCESS 后持久化处理,返回 HTTP 200 和纯文本 success。同步回跳仅作展示,不是发货依据。
{BASE},原生 xypay 插件填写本商户 AppKey / AppSecret,通道编码通常留空。以下示例只负责请求签名和发送,不会在浏览器中运行。替换商户通知地址,并从服务端环境变量读取 FOURPAY_APP_KEY、FOURPAY_APP_SECRET。订单号由业务生成并持久化,重试时不要生成新订单号。退款示例仅展示调用方式,不会自动执行。
import base64, hashlib, hmac, json, os, secrets, time, urllib.request
BASE = '{BASE}'
APP_KEY = os.environ['FOURPAY_APP_KEY']
SECRET = os.environ['FOURPAY_APP_SECRET']
def frame(value):
data = str(value).encode('utf-8')
return str(len(data)).encode('ascii') + b':' + data
def sign_v2(params, path):
raw = b'fourpay-gateway-v2\n' + frame('POST') + frame(path) + frame(APP_KEY)
for key in sorted(k for k in params if k != 'sign'):
raw += frame(key) + frame(params[key])
digest = hmac.new(SECRET.encode('utf-8'), raw, hashlib.sha256).digest()
return base64.urlsafe_b64encode(digest).decode('ascii').rstrip('=')
def request(path, business):
params = dict(business)
params.update(signType='HMAC-SHA256-V2',
timestamp=str(int(time.time())), nonce=secrets.token_hex(16))
params['sign'] = sign_v2(params, path)
req = urllib.request.Request(BASE + path,
data=json.dumps(params, ensure_ascii=False).encode('utf-8'),
headers={'Content-Type': 'application/json', 'X-App-Key': APP_KEY},
method='POST')
with urllib.request.urlopen(req, timeout=30) as response:
result = json.load(response)
if result.get('code') != 0:
raise RuntimeError(result.get('message', '网关请求失败;请核对订单状态'))
return result['data']
# 以下调用放入你的服务端业务处理函数,不要反复执行固定示例订单。
# data = request('/api/gateway/orders', {
# 'merchantOrderNo': persisted_order_no, 'amount': 1000, 'subject': '测试商品',
# 'notifyUrl': 'https://shop.example.com/payment/notify', 'payMethod': 'alipay'})
# 保存 data['orderNo'],跳转 data['cashierUrl']。
# result = request('/api/gateway/orders/query', {'merchantOrderNo': persisted_order_no})
# refund = request('/api/gateway/refunds', {
# 'merchantOrderNo': persisted_order_no, 'merchantRefundNo': persisted_refund_no,
# 'refundAmount': 1000, 'reason': '用户申请退款'})
# 超时/异常时保留原业务编号,先核对状态,不能换退款编号重试。
<?php
function fourpay_request(string $path, array $business): array {
$base = '{BASE}';
$appKey = getenv('FOURPAY_APP_KEY');
$secret = getenv('FOURPAY_APP_SECRET');
if (!$appKey || !$secret) throw new RuntimeException('请配置商户凭据');
$p = $business;
unset($p['sign']);
$p['signType'] = 'HMAC-SHA256-V2';
$p['timestamp'] = (string) time();
$p['nonce'] = bin2hex(random_bytes(16));
$frame = static function ($v): string {
$s = (string) $v;
return strlen($s) . ':' . $s; // PHP 字符串使用 UTF-8 字节
};
ksort($p, SORT_STRING);
$raw = "fourpay-gateway-v2\n" . $frame('POST') . $frame($path) . $frame($appKey);
foreach ($p as $k => $v) $raw .= $frame($k) . $frame($v);
$p['sign'] = rtrim(strtr(base64_encode(hash_hmac('sha256', $raw, $secret, true)), '+/', '-_'), '=');
$ch = curl_init($base . $path);
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30, CURLOPT_HTTPHEADER => [
'Content-Type: application/json', 'X-App-Key: ' . $appKey],
CURLOPT_POSTFIELDS => json_encode($p, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR)]);
$body = curl_exec($ch);
$error = curl_error($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($body === false || $status !== 200) throw new RuntimeException('请求结果未知,请查单:' . $error);
$result = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
if (($result['code'] ?? -1) !== 0) throw new RuntimeException($result['message'] ?? '请求失败');
return $result['data'];
}
// fourpay_request('/api/gateway/orders/query', ['merchantOrderNo' => $persistedOrderNo]);
// 下单/退款替换 path 和业务参数;amount/refundAmount 使用 PHP 整数。
const crypto = require('node:crypto');
const BASE = '{BASE}';
async function fourpayRequest(path, business) {
const appKey = process.env.FOURPAY_APP_KEY, secret = process.env.FOURPAY_APP_SECRET;
if (!appKey || !secret) throw new Error('请配置商户凭据');
const p = { ...business, signType: 'HMAC-SHA256-V2',
timestamp: String(Math.floor(Date.now() / 1000)), nonce: crypto.randomBytes(16).toString('hex') };
delete p.sign;
const frame = v => { const s = String(v); return Buffer.byteLength(s, 'utf8') + ':' + s; };
let raw = 'fourpay-gateway-v2\n' + frame('POST') + frame(path) + frame(appKey);
for (const k of Object.keys(p).sort()) raw += frame(k) + frame(p[k]);
p.sign = crypto.createHmac('sha256', secret).update(raw, 'utf8').digest('base64url');
const response = await fetch(BASE + path, { method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-App-Key': appKey },
body: JSON.stringify(p), signal: AbortSignal.timeout(30000) });
if (!response.ok) throw new Error('请求结果未知,请查单:HTTP ' + response.status);
const result = await response.json();
if (result.code !== 0) throw new Error(result.message || '网关请求失败');
return result.data;
}
// await fourpayRequest('/api/gateway/orders/query', { merchantOrderNo: persistedOrderNo });
// 在 async 业务函数内调用。amount/refundAmount 使用安全范围内的整数,extra 使用 JSON.stringify。
import hashlib, hmac
def verify_notify(payload, secret):
if not isinstance(payload, dict):
return False
received = payload.get('sign')
if not isinstance(received, str) or len(received) != 32 or not received.isascii():
return False
# 当前平台通知字段都是字符串,拒绝异常类型。
if any(not isinstance(v, str) for v in payload.values()):
return False
items = sorted((k, v) for k, v in payload.items() if k != 'sign' and v != '')
raw = '&'.join(k + '=' + v for k, v in items) + '&key=' + secret
expected = hashlib.md5(raw.encode('utf-8')).hexdigest().upper()
return hmac.compare_digest(expected, received)
# payload = 框架解析的请求JSON
# 验签通过后,必须查本地订单并核对 orderNo、merchantOrderNo、amount、status。
# 事务内去重,保存支付结果和可靠发货任务,提交成功后返回 HTTP 200 / SUCCESS。
# 本函数仅验签,不能替代业务金额核对、持久化和防重复发货。
各语言按同一 V2 规则实现,重点核对 UTF-8 字节长度、HMAC 原始摘要和 Base64URL 无填充编码。发送 JSON 时金额必须保持整数类型;签名时才转为十进制文本。
| 语言 | UTF-8 字节长度 | HMAC-SHA256 / 编码 |
|---|---|---|
| Java | s.getBytes(StandardCharsets.UTF_8).length | Mac.getInstance("HmacSHA256");Base64.getUrlEncoder().withoutPadding() |
| Go | len(s),字符串内容须为 UTF-8 | hmac.New(sha256.New, key);base64.RawURLEncoding |
| C# | Encoding.UTF8.GetByteCount(s) | HMACSHA256;Base64 后替换 + 为 -、/ 为 _ 并移除末尾 = |
| Ruby | s.encode('UTF-8').bytesize | OpenSSL::HMAC.digest('SHA256', secret, raw);Base64.urlsafe_encode64(..., padding: false) |
先检查商户开通状态和请求签名,再用本人已开通的通道做小额联调。接入中心的自检不创建真实订单,也不证明支付、退款或回调已经可用。
| 现象 | 排查方式 |
|---|---|
| 商户鉴权失败 | 检查 X-App-Key、商户启用状态及账户验证;不要把商户号当作 AppKey |
| V2 签名失败 | 检查 POST/路径/AppKey 是否一致,是否按 UTF-8 字节计数,是否包含空值及公共字段,是否使用无填充 Base64URL |
| 请求过期 / nonce 已使用 | 校准服务器时间;每次请求和跨接口调用生成新的 nonce |
| MD5 参数存在歧义 | 参数中包含 &,改用原生 V2;不要自行替换字符后仍按旧值发送 |
| notifyUrl 无效 / 回调 301 | 使用可公网访问的最终 HTTPS 地址,检查证书、重定向、登录和站点防护规则 |
| HTTP 200 仍显示通知失败 | 正文直接返回 SUCCESS;不返回 JSON、HTML 或附加说明 |
| 已付款却未发货 | 查看通知记录、商户处理日志并主动查单;排查签名、订单金额和事务去重 |
| merchantOrderNo 已存在 | 按原订单号查询核对;仅新的业务订单使用新编号 |
| 无可用支付通道 | 检查通道启用、支付品牌、商户分组授权、本人子商户收款开关和额度 |
| 风控拦截 | 检查单笔/单日限额、付款 IP、支付账号限制和下单频率 |
| 退款要求 V2 | 升级调用方或插件,增加 merchantRefundNo 和 V2 公共字段 |
| 退款超时 / 状态未知 | 保留原退款编号核对订单与后台退款记录,不用新编号再次退款 |
为现有系统接入支付服务,选择对应插件,下载后按说明配置。
适用于 易支付 pro。下载后请参照包内说明安装配置。