UniPay 对接指南
UniPay 提供易支付 v1 收款和独立的支付宝付款接口。收款资金直接进入商户自己的支付宝账户;付款从商户自己的支付宝账户转出。用户余额、订单履约和提现审核由接入的业务系统管理。
本文与运行中的服务一同发布,无需登录即可读取。接口地址以当前部署域名为准,以下使用 https://pay.example.com 作为占位域名。
快速接入
- 登录商户后台,在「对接信息」获取网关地址、商户 PID 和收款密钥。完整密钥仅在生成或重置时显示,指纹不能用于签名。
- 在「渠道管理」配置自己的支付宝应用,并启用已签约的当面付、电脑网站支付或手机网站支付产品。
- 已支持易支付 v1 的系统(如使用 go-epay 的 new-api、Sub2API)填写网关、PID、收款 key,支付方式选择
alipay。若配置项要求基础地址,填域名;若要求提交地址,填完整/submit.php,避免重复拼接路径。 - 自行开发时,在业务服务端创建本地订单并签名,再让买家浏览器打开签名后的支付地址或提交表单。
- 实现异步通知验签、订单和金额核对、数据库幂等入账,再返回
success;使用主动查单补偿未收到的通知。
| 配置 | 说明 |
|---|---|
| 网关基础地址 | 当前 UniPay 部署域名,不带末尾 / |
| 收款提交 | GET /submit.php 或表单 POST /submit.php |
| 收款查询 | GET /api.php?act=order,仅供服务端调用 |
pid | 商户 PID,按字符串处理;不是支付宝 APPID |
收款 key | 易支付收款签名和查单使用的密钥,仅放在业务服务端 |
| 付款 API 密钥 | 在「付款记录」单独生成,与收款 key 不通用 |
公开文档和示例不包含真实密钥。向 AI 提供完整文档即可;真实密钥通过部署环境注入,勿放入聊天、前端代码、仓库或日志。
接口一览
| 方法与路径 | 协议 / 认证 | 结果 |
|---|---|---|
GET /submit.php | 易支付参数 + MD5 | 302 跳转收银台 |
POST /submit.php | URL 编码表单 + MD5,不是 JSON | 302 跳转收银台 |
GET /api.php | act=order + PID + 收款 key | JSON,检查 code 与 trade_status |
POST /api/v1/payouts | JSON + HMAC-SHA256 请求头 | 202 受理;相同单据重试返回 200 |
POST /api/v1/payouts/query | JSON + HMAC-SHA256 请求头 | 查询付款持久化状态 |
GET /api/v1/healthz | 无认证 | {"status":"ok"},仅表示进程存活 |
完整字段定义可下载 OpenAPI 3.1 JSON。本文只描述商户业务系统需要调用的接口;后台管理接口使用独立登录会话。
收款:创建订单
请求为 UTF-8。GET 使用查询参数;POST 使用 Content-Type: application/x-www-form-urlencoded。
| 字段 | 必填 | 说明 |
|---|---|---|
pid | 是 | 商户 PID |
type | 是 | 推荐 alipay;alipay_page 指定电脑网站支付,alipay_wap 指定手机网站支付 |
out_trade_no | 是 | 业务系统生成的非空订单号,同一商户内唯一 |
notify_url | 是 | 业务系统接收异步 GET 通知的公网地址,推荐 HTTPS |
return_url | 是 | 买家支付完成后返回的业务页面地址 |
name | 是 | 非空商品名称 |
money | 是 | 正数人民币元字符串,如 "12.34";最多两位小数,不接受科学计数法 |
sign | 是 | 下节算法生成的 32 位 MD5 十六进制字符串 |
sign_type | 按协议发送 | 固定 MD5 |
type=alipay 会按商户已启用产品选择收银方式,手机浏览器优先使用已启用的 WAP;显式指定的产品需已配置并启用。当前不支持微信支付、退款接口或其他易支付 act。
成功返回 HTTP 302,Location: /cashier/{trade_no}。这是交给买家浏览器的收银台地址,不是 JSON API。服务器收到 302 不代表已经付款。
订单有效期为 15 分钟。同一商户重复提交同一 out_trade_no:待支付订单复用原订单和参数;过期或失败订单会以本次参数重新激活;已支付订单返回 400 order already paid。业务系统应保持订单参数稳定,修改商品或金额时使用新的业务单号。
收款:MD5 签名
- 从全部参数中移除
sign、sign_type和值为空字符串的项。额外发送的非空参数也参与签名。 - 按参数名 ASCII 升序排序,使用原始参数值拼成
k1=v1&k2=v2。不要先 URL 编码,不要去掉值中的空格。 - 在末尾直接追加原始收款 key,不加
&key=或其他分隔符。 - 对整个字符串的 UTF-8 字节计算 MD5,输出小写十六进制。
- 添加
sign和sign_type=MD5后,才对参数做表单或 URL 编码。
sign = MD5_HEX(UTF8(sorted_nonempty_parameters + epay_key))
以下是可公开使用的合成签名测试向量,不是真实商户凭据:
{
"params": {"pid": "1001", "money": "1.00", "name": "topup", "type": "alipay", "out_trade_no": "20260707001", "notify_url": "https://a.com/n", "return_url": "https://a.com/r", "sign_type": "MD5", "empty": ""},
"test_key": "testkey123",
"expected_sign": "e1a8ad540d38032abe568da728c12c1f",
"canonical_without_key": "money=1.00&name=topup¬ify_url=https://a.com/n&out_trade_no=20260707001&pid=1001&return_url=https://a.com/r&type=alipay"
}
收款:Python 示例
标准库示例只定义函数,不会自动创建订单或发送支付请求。UNIPAY_URL、UNIPAY_PID、UNIPAY_EPAY_KEY 由业务服务端环境提供。
import hashlib
import hmac
import json
import os
import urllib.parse
import urllib.request
def epay_sign(params, key):
# Protocol values must be strings. "0" participates; only "" is empty.
canonical = "&".join(
f"{k}={params[k]}" for k in sorted(params)
if k not in ("sign", "sign_type") and params[k] != ""
)
return hashlib.md5((canonical + key).encode("utf-8")).hexdigest()
def payment_url(out_trade_no, name, money, notify_url, return_url):
params = {
"pid": os.environ["UNIPAY_PID"], "type": "alipay",
"out_trade_no": out_trade_no, "name": name, "money": money,
"notify_url": notify_url, "return_url": return_url,
"sign_type": "MD5",
}
params["sign"] = epay_sign(params, os.environ["UNIPAY_EPAY_KEY"])
return os.environ["UNIPAY_URL"].rstrip("/") + "/submit.php?" + urllib.parse.urlencode(params)
def verify_payment_notification(params):
# Parse query values once; reject duplicate parameter names in your HTTP handler.
signature = params.get("sign", "")
if len(signature) != 32 or any(c not in "0123456789abcdefABCDEF" for c in signature):
return False
expected = epay_sign(params, os.environ["UNIPAY_EPAY_KEY"])
return (
params.get("pid") == os.environ["UNIPAY_PID"]
and params.get("trade_status") == "TRADE_SUCCESS"
and hmac.compare_digest(expected, signature.lower())
)
def query_payment(out_trade_no):
params = {
"act": "order", "pid": os.environ["UNIPAY_PID"],
"key": os.environ["UNIPAY_EPAY_KEY"], "out_trade_no": out_trade_no,
}
url = os.environ["UNIPAY_URL"].rstrip("/") + "/api.php?" + urllib.parse.urlencode(params)
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# The URL includes a secret: do not log it or raw HTTP exception details.
with urllib.request.build_opener(NoRedirect()).open(url, timeout=15) as response:
return json.load(response)
验签函数只证明通知签名有效。随后必须在数据库事务中核对本地 out_trade_no、PID、金额和订单归属,并仅对尚未入账的订单执行一次业务处理。金额用整数分或十进制定点数比较,避免浮点误差。
收款:异步通知与页面返回
UniPay 确认付款后向 notify_url 发送 GET,包含以下签名参数:
| 字段 | 内容 |
|---|---|
pid | 商户 PID |
trade_no | UniPay 平台订单号 |
out_trade_no | 业务订单号 |
type / name | 原订单支付类型 / 商品名 |
money | 人民币元字符串,固定两位小数 |
trade_status | TRADE_SUCCESS |
sign / sign_type | 收款 MD5 签名 / MD5 |
验签规则与创建订单一致。通知地址建议不自带查询参数,避免业务自定义参数与签名参数混淆。完成验签、订单金额核对和持久化幂等入账后,返回 HTTP 200,正文仅为小写 success。处理失败时不要返回成功确认。
通知可能重复、延迟或先于买家返回到达。首次立即投递,失败后按 1 分钟、5 分钟、15 分钟、1 小时、6 小时间隔重试(当前最多 6 次投递);后台支持手动重发。主动查询和回调应调用相同的幂等入账逻辑。
买家在收银台等待后端确认支付成功后,会看到三秒倒计时,再跳转 return_url,携带与通知相同的签名字段。页面返回用于展示订单状态,不能仅凭浏览器参数发货或充值;业务服务端应依据已验签的异步通知或认证查单结果确认入账。
收款:主动查单
GET /api.php?act=order&pid=<PID>&key=<EPAY_KEY>&out_trade_no=<BUSINESS_ORDER_NO>
全部四个字段都要填写。当前实现只按 out_trade_no 查询,不支持用 trade_no 替代。此接口直接携带收款 key,仅可从服务端通过 HTTPS 调用;不要把 URL 放入浏览器、访问日志或错误上报。
成功响应示例:
{
"code": 1,
"trade_no": "01EXAMPLE",
"out_trade_no": "order-20260909-0001",
"type": "alipay",
"name": "账户充值",
"money": "12.34",
"trade_status": "TRADE_SUCCESS",
"addtime": "2026-09-09 10:00:00",
"endtime": "2026-09-09 10:01:00"
}
code=1 表示查到订单;仅 trade_status=TRADE_SUCCESS 表示支付成功。所有其他内部状态(包括过期、失败)在此接口均映射为 WAIT_BUYER_PAY,不能据此区分失败原因。addtime、endtime 为 UTC 时间,格式 YYYY-MM-DD HH:mm:ss;未付款时 endtime 为空字符串。
查单业务错误仍返回 HTTP 200,正文如 {"code":-4,"msg":"order not found"}:
code | msg | 处理 |
|---|---|---|
-1 | key error | 核对商户 PID 与收款 key |
-2 | unsupported act | 仅支持 act=order |
-4 | order not found | 核对当前商户的业务订单号,不要据此确认支付成功 |
收款:错误排查
| 场景 | 响应 / 处理 |
|---|---|
| 提交参数缺失 | HTTP 400,纯文本 missing <field> |
| PID 或签名错误 | HTTP 400,unknown pid / bad sign;检查原始值、排序、空值和 UTF-8 |
| 商户关闭 | HTTP 400,merchant disabled |
| 金额无效 | HTTP 400,illegal money;使用正数元字符串,最多两位小数 |
| 已支付单重复提交 | HTTP 400,order already paid;查单后幂等处理 |
| 请求过频 | HTTP 429;提交按 PID + 来源 IP 限制每分钟 30 次,退避重试 |
| 超时或 HTTP 5xx | 先用原业务单号查单;不可当作已付款或直接重复入账 |
| 通知未到 | 检查公网回调地址、验签和 success 响应,主动查单,必要时后台重发通知 |
付款接口的 HMAC 签名、整数分金额、幂等规则和终态通知见下方完整付款说明,或单独读取付款 Markdown。