UniPay / 开发者文档 商户后台

UNIPAY / INTEGRATION

从第一笔支付开始。

接入收款、发起付款,让你的业务与 UniPay 连通。
协议、签名、回调和代码示例,都在这里。

开始接入 读取纯文本

UniPay 对接指南

UniPay 提供易支付 v1 收款和独立的支付宝付款接口。收款资金直接进入商户自己的支付宝账户;付款从商户自己的支付宝账户转出。用户余额、订单履约和提现审核由接入的业务系统管理。

本文与运行中的服务一同发布,无需登录即可读取。接口地址以当前部署域名为准,以下使用 https://pay.example.com 作为占位域名。

快速接入

  1. 登录商户后台,在「对接信息」获取网关地址、商户 PID 和收款密钥。完整密钥仅在生成或重置时显示,指纹不能用于签名。
  2. 在「渠道管理」配置自己的支付宝应用,并启用已签约的当面付、电脑网站支付或手机网站支付产品。
  3. 已支持易支付 v1 的系统(如使用 go-epay 的 new-api、Sub2API)填写网关、PID、收款 key,支付方式选择 alipay。若配置项要求基础地址,填域名;若要求提交地址,填完整 /submit.php,避免重复拼接路径。
  4. 自行开发时,在业务服务端创建本地订单并签名,再让买家浏览器打开签名后的支付地址或提交表单。
  5. 实现异步通知验签、订单和金额核对、数据库幂等入账,再返回 success;使用主动查单补偿未收到的通知。
配置说明
网关基础地址当前 UniPay 部署域名,不带末尾 /
收款提交GET /submit.php 或表单 POST /submit.php
收款查询GET /api.php?act=order,仅供服务端调用
pid商户 PID,按字符串处理;不是支付宝 APPID
收款 key易支付收款签名和查单使用的密钥,仅放在业务服务端
付款 API 密钥在「付款记录」单独生成,与收款 key 不通用

公开文档和示例不包含真实密钥。向 AI 提供完整文档即可;真实密钥通过部署环境注入,勿放入聊天、前端代码、仓库或日志。

接口一览

方法与路径协议 / 认证结果
GET /submit.php易支付参数 + MD5302 跳转收银台
POST /submit.phpURL 编码表单 + MD5,不是 JSON302 跳转收银台
GET /api.phpact=order + PID + 收款 keyJSON,检查 code 与 trade_status
POST /api/v1/payoutsJSON + HMAC-SHA256 请求头202 受理;相同单据重试返回 200
POST /api/v1/payouts/queryJSON + 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 签名

  1. 从全部参数中移除 sign、sign_type 和值为空字符串的项。额外发送的非空参数也参与签名。
  2. 按参数名 ASCII 升序排序,使用原始参数值拼成 k1=v1&k2=v2。不要先 URL 编码,不要去掉值中的空格。
  3. 在末尾直接追加原始收款 key,不加 &key= 或其他分隔符。
  4. 对整个字符串的 UTF-8 字节计算 MD5,输出小写十六进制。
  5. 添加 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&notify_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_noUniPay 平台订单号
out_trade_no业务订单号
type / name原订单支付类型 / 商品名
money人民币元字符串,固定两位小数
trade_statusTRADE_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"}:

codemsg处理
-1key error核对商户 PID 与收款 key
-2unsupported act仅支持 act=order
-4order 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。

支付宝付款接口

UniPay 执行商户的打款指令。用户余额、冻结、提现资格和审核由业务系统负责;资金从商户自己的支付宝账户直接转入收款人的支付宝账户。

配置

  1. 为出款应用开通支付宝对应转账产品(TRANS_ACCOUNT_NO_PWD / DIRECT_TRANSFER),并在支付宝配置真实业务场景。当面付收款权限不能替代转账权限。
  2. 在商户后台「付款记录」生成独立付款 API 密钥,并保存到业务系统的服务端密钥存储。密钥只显示一次,与易支付收款 key 独立。
  3. 在「配置出款」上传应用私钥、应用公钥证书、支付宝公钥证书和支付宝根证书,填写 APPID 和单笔限额,再启用付款。默认关闭,默认单笔限额 100 元。
  4. 从「对接信息」获取已有商户 PID。付款接口仅供业务系统服务端调用。

出款证书与现有收款配置分别管理,可以使用不同的支付宝应用。服务端校验证书有效期、私钥与应用证书的匹配关系,自动计算证书序列号。实际产品权限、业务场景和额度由支付宝校验。当前仅支持转账到支付宝登录账号(邮箱或手机号),不支持银行卡。

接口和签名

接口用途
POST /api/v1/payouts受理付款,返回 202;相同订单重复请求返回 200
POST /api/v1/payouts/query按业务提现单号查询,返回 200

请求为 UTF-8 JSON,最大 64 KiB。金额字段为整数分,禁止浮点金额。所有请求使用以下 HTTP 头:

  • X-Unipay-Pid:商户 PID。
  • X-Unipay-Timestamp:Unix 秒级时间戳,允许前后 300 秒偏差。
  • X-Unipay-Nonce:每次 HTTP 请求生成不同的 16~64 位字母、数字、_ 或 - 字符串。
  • X-Unipay-Signature:下述字符串的 HMAC-SHA256,小写十六进制。

签名字符串各行以单个 LF(\n)连接,最后一行后没有换行:

unipay-payout-v1
<PID>
<TIMESTAMP>
<NONCE>
POST
<PATH>
<SHA256_HEX_OF_RAW_BODY>

PATH 是上述固定路径,不含域名和查询字符串。先生成实际发送的 JSON 字节,再对这些字节做 SHA256;不要签名后重新序列化 JSON。HMAC 密钥使用后台显示的 64 个十六进制字符的 UTF-8 字节,不要 hex-decode。 签名覆盖方法、路径、PID、时间、nonce 和请求正文。

每个商户限流 30 次付款/查询请求每分钟。每次重试使用新的 timestamp/nonce/签名,但必须保持原业务提现单号和付款参数。

发起付款

{
  "out_biz_no": "withdrawal-20260909-0001",
  "amount_cents": 1234,
  "payee_account": "recipient@example.com",
  "payee_name": "收款人实名",
  "title": "用户提现",
  "notify_url": "https://business.example.com/callbacks/payout",
  "transfer_scene_name": "按支付宝签约填写的真实业务场景",
  "transfer_scene_report_infos": [
    {"info_type": "场景要求的信息类型", "info_content": "真实业务信息"}
  ]
}

场景名称和上报字段必须按商户签约产品要求填写,示例中的占位文字不能直接用于实付。transfer_scene_report_infos 默认为空数组;具体场景要求上报时必须填写。

除 transfer_scene_report_infos 外,上述字段全部必填;不接受未知字段。文本字段禁止首尾空白和控制字符。字段限制如下:

字段限制
amount_cents正整数分,最多 100000000,且不超过商户配置的单笔限额
payee_account / payee_name / title各 1~100 个字符
transfer_scene_name1~64 个字符
notify_url最多 2048 UTF-8 字节,公网 HTTPS,不允许用户信息或 URL 片段
transfer_scene_report_infos最多 10 项;每项必填 info_type(1~64 字符)和 info_content(1~300 字符)

out_biz_no 是业务系统的提现单号,1~64 位字母、数字、_ 或 -。UniPay 生成独立的全局 payout_no,并将它作为支付宝的 out_biz_no,防止不同商户共用一个支付宝应用时发生单号碰撞。

同一商户、同一 out_biz_no 永远对应同一笔付款。参数全部相同时返回原记录;更换金额、收款人、通知地址或其他字段时返回 409,不创建新付款。返回结果示例:

{
  "payout_no": "P01EXAMPLE",
  "out_biz_no": "withdrawal-20260909-0001",
  "amount_cents": 1234,
  "status": "queued",
  "channel_order_id": null,
  "failure_code": null,
  "created_at": "2026-09-09T00:00:00Z",
  "completed_at": null
}

查询与余额处理

查询请求:{"out_biz_no":"withdrawal-20260909-0001"},响应结构与发起付款一致。查询返回 UniPay 的持久化状态,后台独立向支付宝查单。

状态含义业务系统处理
queued已落库,尚未发送给支付宝保持冻结
processing已开始发送,最终结果尚未确认保持冻结,继续查询
succeeded支付宝签名结果确认成功完成提现扣款,仅处理一次
failed确定未打款成功解冻余额,仅处理一次

202 仅表示受理,HTTP 请求成功不等于付款成功。客户端超时、5xx、验签失败、支付宝系统异常、重复受理和查无此单均不能当作付款失败。用原单号查询或重试原受理请求,切勿另起新单。

failure_code 在 processing 时只是诊断信息(例如 RESULT_UNKNOWN、ORDER_NOT_EXIST),不能作为解冻依据;只有终态 failed 才表示明确失败。公开结果不包含收款账号和姓名。

结果通知

终态 succeeded / failed 触发 HTTPS POST JSON 通知。正文是查询结果,加上 pid。验签所需的 PID、timestamp、nonce、signature 与请求使用同名头;另有 X-Unipay-Key-Id,为通知密钥 UTF-8 字节 SHA256 的前 12 位十六进制,用于选择轮换前后的密钥。

通知签名字符串使用独立前缀,各行 LF 分隔,末尾无换行:

unipay-payout-notify-v1
<PID>
<TIMESTAMP>
<NONCE>
<SHA256_HEX_OF_RAW_BODY>

业务系统先验签并核对 PID、订单和金额,在数据库事务中按业务提现单号幂等处理余额,成功持久化后返回 HTTP 2xx 和字节完全为 success 的响应正文。不要仅依据通知参数改余额。通知至少一次投递,重复通知和主动查询可能同时到达;两者应使用同一个幂等终态处理入口。

第一次投递立即安排,失败后间隔 1 分钟、5 分钟、15 分钟、1 小时,之后每 6 小时重试,总计最多 10 次。重试耗尽后可在付款记录中「重发通知」,这只重发结果,不会再次转账。投递计划和记录持久化,重启后继续。

通知只允许公开 HTTPS 地址;拒绝私网、回环、元数据地址及域名解析到这些地址的目标,不跟随重定向,也不使用环境代理。回调端点需要直接返回结果。请为业务系统提供公网可达的 HTTPS 回调入口。

重置付款密钥后,新请求立即使用新密钥;已受理订单的通知继续使用受理时的密钥。业务系统应保留旧通知验签密钥,直到这些订单的通知全部处理完毕。

Python 接入示例

下面的函数只定义接口调用方式,不会自动发起付款。密钥从进程环境读取,不在命令行或日志中输出。

import hashlib
import hmac
import json
import os
import secrets
import time
import urllib.request


def payout_request(path, payload):
    base_url = os.environ["UNIPAY_URL"].rstrip("/")
    pid = os.environ["UNIPAY_PID"]
    key = os.environ["UNIPAY_PAYOUT_KEY"].encode("utf-8")
    body = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
    timestamp = str(int(time.time()))
    nonce = secrets.token_hex(16)
    canonical = "\n".join([
        "unipay-payout-v1", pid, timestamp, nonce, "POST", path,
        hashlib.sha256(body).hexdigest(),
    ])
    signature = hmac.new(key, canonical.encode("utf-8"), hashlib.sha256).hexdigest()
    request = urllib.request.Request(base_url + path, data=body, method="POST", headers={
        "Content-Type": "application/json",
        "X-Unipay-Pid": pid,
        "X-Unipay-Timestamp": timestamp,
        "X-Unipay-Nonce": nonce,
        "X-Unipay-Signature": signature,
    })
    # Forbid redirects: do not forward signed instructions to another target.
    class NoRedirect(urllib.request.HTTPRedirectHandler):
        def redirect_request(self, req, fp, code, msg, headers, newurl):
            return None
    with urllib.request.build_opener(NoRedirect()).open(request, timeout=15) as response:
        return json.load(response)


def verify_payout_notification(headers, raw_body, expected_pid, key):
    # key must be selected from your key store using X-Unipay-Key-Id.
    try:
        pid = headers["X-Unipay-Pid"]
        timestamp = headers["X-Unipay-Timestamp"]
        nonce = headers["X-Unipay-Nonce"]
        signature = headers["X-Unipay-Signature"]
        if pid != expected_pid or abs(int(time.time()) - int(timestamp)) > 300:
            return False
        canonical = "\n".join([
            "unipay-payout-notify-v1", pid, timestamp, nonce,
            hashlib.sha256(raw_body).hexdigest(),
        ])
        expected = hmac.new(key.encode("utf-8"), canonical.encode("utf-8"), hashlib.sha256).hexdigest()
        return hmac.compare_digest(expected, signature)
    except (KeyError, TypeError, ValueError):
        return False

业务系统还需在验签后持久化通知幂等记录;这个验签函数本身不处理余额,也不代替订单核对。

错误与运维

HTTP 状态含义
400参数无效、超过单笔限额或配置不完整
401密钥、签名、时间戳或商户状态无效
403付款关闭、商户禁用或没有对应后台权限
404当前商户查不到该付款
409nonce 重复,或同一业务单号对应不同参数
413请求正文超过 64 KiB
429请求过于频繁
5xx / 网络超时受理或查询结果不确定;按原单号查询/重试

出款配置、API 密钥、通知密钥及含收款人信息的请求均加密保存。每笔订单绑定不可变的出款配置版本,配置更新不会将旧订单误查到新支付宝应用。禁用出款阻止新付款及尚未发送的订单,已发送订单继续查单和通知。

任务在发送前持久化 processing,避免重启或并发造成二次发送。若恰好在写入该状态后、请求真正发送前进程崩溃,该订单也会保留 processing 并持续查单;系统不会自动补发转账或因「查无此单」解冻余额。长期未确认的订单需人工用 payout_no 到支付宝核对,确认资金事实后在业务系统处理。UniPay 不提供强制成功/失败或人工重复打款按钮。

当前自动查询间隔逐渐增加到 5 分钟,非终态不会自动超时改为失败。通知最多重试 10 次,后台支持手动重发。数据库备份必须包含新增 payout 表,更新时保持原 UNIPAY_MASTER_KEY。

该功能的自动化验证使用合成测试密钥和模拟支付宝网关;首次正式出款前,应配置实际签约应用,在业务系统中发起可核对的小额提现完成联调。