接入指南
两类账户,分别接入。先完成签名与请求,再通过查询或通知确认订单结果。
创建收款订单
商户 PID / 密钥 → 创建订单 → 打开付款链接 → 查询或接收支付通知。
查看商户接入 →提交核销订单
核销商 PID / 密钥 → 上传目标账号及金额 → 查询或接收处理结果。
查看核销商接入 →开始前,准备这三项
- 对应账户的 PID 和密钥。商户与核销商的凭证不能混用,密钥只保存在自己的服务器。
- 自己的业务订单号。为每笔业务保存唯一订单号;超时、重试、查询和通知必须对应同一笔订单。
- 通知接收地址。准备可被平台访问的 HTTP/HTTPS 地址,完成验签、金额核对和幂等保存;核销上传可不传通知地址,改用查询确认。
四个接口,一张速查表
| 账户 | 用途 | 方法与路径 | 接下来 |
|---|---|---|---|
| 商户 | 创建订单 | POST /payment/orders | 使用响应顶层的 payurl 打开付款页 |
| 商户 | 查询订单 | POST /payment/orders/query | data.status=1 才是已支付 |
| 核销商 | 上传订单 | POST /supplier/orders | 上传成功仅表示已受理 |
| 核销商 | 查询订单 | POST /supplier/orders/query | data.status=2 才是处理成功 |
application/x-www-form-urlencoded,也支持 JSON 对象。UTF-8 编码,金额单位人民币元(CNY)。金额建议按字符串发送,如 100.00,按实际字符串签名。所有请求均由服务端发起。这些字段不要混用
| 字段 / 场景 | 正确含义 |
|---|---|
创建订单的 type=1/2 | 商户支付编码:1 订单支付、2 登录充值;不是通道 ID 或归属 |
查询商户订单的 type=1/2 | 订单号种类:1 平台订单号、2 商户订单号 |
核销商的 order_code=hx | 上传和查询都固定传 hx,不能填入商户 type |
trade_no / out_trade_no | 平台生成的号码 / 接入方自己的业务订单号 |
notify_url / return_url | 服务器通知 / 用户浏览器跳转;不能根据浏览器跳转记账 |
HTTP 200 / code=200 / success | HTTP 正常 / 接口调用成功 / 通知已处理;都不能独立证明订单成功 |
/dy/{32位标识},直接使用返回的 payurl,不要自行拼接 token。时间字段采用北京时间(UTC+8),格式 YYYY-MM-DD HH:mm:ss,原始字符串不含时区后缀。签名规则
商户下单与核销上传接口使用同一套签名规则。账户号和密钥由平台管理端分配。
- 移除
sign、sign_type,并忽略值为空字符串的参数。数值0必须保留;不要传入数组、对象或 null 作为业务字段值。 - 按参数名 ASCII 升序排列,以
key=value形式使用&连接。 - 在拼接结果末尾直接追加商户密钥,不添加
&key=。 - 对完整字符串计算 MD5,输出 32 位小写签名。
sign 和 sign_type,再按同样规则计算。创建订单的支付编码 type=1、type=2 和核销上传/查询的 order_code=hx 均按实际提交值参与签名,不转换成其他编码再签名。
先用这个固定样例校验
以下 PID 和密钥仅用于本地计算,不是真实账户,不能用于请求平台。应得到完全相同的 32 位签名。
{
"pid": "20001",
"order_code": "hx",
"out_trade_no": "UPLOAD_20260927_000001"
}演示密钥:DOC_DEMO_KEY_NOT_FOR_PRODUCTION
按键排序并在末尾追加密钥后的完整原文:
order_code=hx&out_trade_no=UPLOAD_20260927_000001&pid=20001DOC_DEMO_KEY_NOT_FOR_PRODUCTION期望 MD5:
6788a699a88b86a10c9fce9ba2c35994100 与 100.00 的签名不同;空字符串不参与签名,但 0 要保留。URL、中文和 & 先以原始值参与签名,再统一做表单编码。额外提交的非空字段也参与签名。四语言签名函数
PHP / Python 的独立请求和完整流程示例已包含签名函数;Java / C# 此处为签名函数参考,需自行放入项目并引入对应标准库。
function makeSign(array $params, string $merchantKey): string
{
unset($params['sign'], $params['sign_type']);
$params = array_filter($params, static fn($value) => $value !== '');
ksort($params);
$pairs = [];
foreach ($params as $key => $value) {
$pairs[] = $key . '=' . $value;
}
return md5(implode('&', $pairs) . $merchantKey);
}static String makeSign(Map<String, String> input, String merchantKey) throws Exception {
TreeMap<String, String> sorted = new TreeMap<>(input);
sorted.remove("sign");
sorted.remove("sign_type");
String source = sorted.entrySet().stream()
.filter(item -> item.getValue() != null && !item.getValue().isEmpty())
.map(item -> item.getKey() + "=" + item.getValue())
.collect(Collectors.joining("&")) + merchantKey;
byte[] digest = MessageDigest.getInstance("MD5")
.digest(source.getBytes(StandardCharsets.UTF_8));
StringBuilder result = new StringBuilder();
for (byte value : digest) result.append(String.format("%02x", value & 0xff));
return result.toString();
}static string MakeSign(IDictionary<string, string> input, string merchantKey)
{
var source = string.Join("&", input
.Where(item => item.Key != "sign" && item.Key != "sign_type" && item.Value != "")
.OrderBy(item => item.Key, StringComparer.Ordinal)
.Select(item => item.Key + "=" + item.Value)) + merchantKey;
return Convert.ToHexString(
MD5.HashData(Encoding.UTF8.GetBytes(source))
).ToLowerInvariant();
}import hashlib
def make_sign(params: dict, merchant_key: str) -> str:
pairs = [
f"{name}={value}"
for name, value in sorted(params.items())
if name not in ("sign", "sign_type") and value != ""
]
source = "&".join(pairs) + merchant_key
return hashlib.md5(source.encode("utf-8")).hexdigest()错误码与重试
code=200、msg=查询成功,只表示查到了订单;必须继续读取 data.status。订单失败时仍可返回 code=200,此时 data.status=3。| 场景 / 字段 | 含义 |
|---|---|
| HTTP 200 | HTTP 请求正常返回,不能据此判定订单结果 |
/supplier/orders/query 顶层 code | 200:接口调用成功;201:接口调用失败 |
/supplier/orders、/supplier/orders/query 的 data.status | 0:待处理;1:处理中;2:处理成功;3:处理失败 |
上传订单 GET 回调参数 status | 2:处理成功;3:处理失败 |
回调接收方回复 success | 已接收并处理这次通知;成功通知和失败通知都需要确认 |
支付商户 /payment/orders/query 的 status | 1 表示已支付。此接口与上传订单的状态定义不同,不能共用状态映射 |
JSON 接口的接口调用失败(如验签失败、参数错误、订单不存在)使用 code=201,并增加稳定的 error_code 和 retryable。请依据错误码处理,不要依赖中文提示文本。
{
"code": 201,
"error_code": "AMOUNT_INVENTORY_EMPTY",
"msg": "当前暂无匹配金额的可用订单,请稍后重试",
"retryable": true,
"data": null
}
| error_code | 含义 | 可重试 | 处理建议 |
|---|---|---|---|
PID_REQUIRED | 账户号缺失 | 否 | 补齐账户号后重新签名 |
SIGN_INVALID | 签名验证失败 | 否 | 检查参数排序、空值处理和账户密钥 |
ACCOUNT_UNAVAILABLE | 账户不存在或已停用 | 否 | 联系平台检查账户状态 |
ACCOUNT_ROLE_DENIED | 账户角色不能发起支付 | 否 | 支付下单必须使用商户账户 |
UPLOAD_PERMISSION_DENIED | 账户无订单上传权限 | 否 | 订单上传必须使用上传账户 |
ORDER_NO_REQUIRED | 商户或外部订单号缺失 | 否 | 补齐订单号后重新签名 |
ORDER_CODE_INVALID | 订单上传编码无效 | 否 | 使用平台指定的订单编码 |
TARGET_ACCOUNT_INVALID | 目标账号格式无效 | 否 | 检查长度、空格和控制字符 |
PAYMENT_CODE_REQUIRED | 支付编码缺失 | 否 | 提交后台授权的支付编码 |
PAYMENT_CODE_UNAVAILABLE | 支付编码无效或已停用 | 否 | 检查编码与启用状态 |
PAYMENT_CODE_RETIRED | 请求使用了已替换的旧支付编码 | 否 | 改用 1 或 2,并按新编码重新签名 |
PAYMENT_CODE_CONFLICT | 数字编码与现有通道配置冲突 | 否 | 联系管理员核对配置,不自动切换通道 |
PAYMENT_PERMISSION_DENIED | 商户未授权该支付编码 | 否 | 联系平台配置收款权限 |
AMOUNT_INVALID | 金额格式或额度规则不符 | 否 | 按支付编码金额规则修正请求 |
BALANCE_INSUFFICIENT | 商户余额不足 | 否 | 补充余额后再提交 |
NOTIFY_URL_INVALID | 异步回调地址缺失或无效 | 否 | 提交有效的 HTTP/HTTPS 地址 |
RETURN_URL_INVALID | 同步跳转地址缺失或无效 | 否 | 提交有效的 HTTP/HTTPS 地址 |
DUPLICATE_ORDER | 外部或商户订单号重复 | 否 | 先查询原订单,不要生成重复业务单 |
AMOUNT_INVENTORY_EMPTY | 暂时没有匹配金额的可用订单 | 是 | 稍后使用原业务订单号重试 |
CHANNEL_UNAVAILABLE | 当前没有可用支付通道 | 是 | 建议 5 秒后重试 |
CHANNEL_BUSY | 支付通道暂时繁忙 | 是 | 建议 5 至 10 秒后重试 |
ORDER_NOT_FOUND | 未找到当前账户的订单 | 否 | 检查订单号类型及账户号 |
ORDER_NO_TYPE_INVALID | 商户查询的订单号类型不正确 | 否 | type=1 查平台号;type=2 查自己的业务订单号 |
VALID_MINUTES_INVALID | 核销上传有效期不是 3–1440 的整数 | 否 | 修正 valid_minutes 后重新签名;不修改有效期业务范围 |
ORDER_NAME_REQUIRED | 商户下单缺少订单名称 | 否 | 补充 name 后重新签名 |
ORDER_NAME_REJECTED | 名称不符合平台规则 | 否 | 检查等号及禁用词 |
PAYMENT_ROUTE_DISABLED | 商户支付路由停用 | 否 | 联系平台核对授权及路由状态 |
VERIFICATION_TASK_UNAVAILABLE | 商户暂无可用核销任务 | 是 | 先核对原单,再退避重试 |
TARGET_RATE_WAIT | 同目标任务正在排队 | 是 | 先查询原单,不频繁更换业务订单号 |
UPSTREAM_CREATE_INCIDENT | 上游创建服务异常 | 是 | 保存错误码,查询原单后退避重试 |
TARGET_LOOKUP_INCIDENT | 目标账号查询服务异常 | 是 | 查询原单后退避重试 |
UPSTREAM_RESULT_UNCERTAIN | 上游创建结果未确认 | 是 | 先查原单;不要把结果未知当作失败或另起一笔业务 |
UPSTREAM_RESULT_RECOVERY_WAIT | 上游订单仍在确认 | 是 | 等待后查询原订单 |
METHOD_NOT_ALLOWED | 请求方法不正确 | 否 | 改用 POST;HTTP 和 code 都为 405 |
SYSTEM_BUSY | 系统暂时繁忙 | 是 | 退避重试并保留请求日志 |
retryable=true 表示可在核对后重试,不表示应该立即重复提交。网络超时或未拿到明确响应时,应先调用查询接口确认订单是否已创建,再决定是否重试;不要直接更换业务订单号。code=201;请求方法错误返回 HTTP 405。调用方应同时检查 HTTP 状态和响应体。常见问题,按这个顺序排查
| 现象 | 先检查 | 下一步 |
|---|---|---|
| 签名失败 | 是否混用了两类账户密钥;金额格式、空值、额外字段是否一致 | 先跑固定签名样例,再对照自己的原始参数;日志不得记录密钥 |
| 请求超时或响应不是 JSON | 订单是否已被平台受理 | 保留原业务订单号,先查询;不要更换订单号重新创建 |
| DUPLICATE_ORDER | 此前同一订单号是否已提交 | 查询已有订单,而不是重复创建 |
| 查询 code=200 但状态未成功 | 读取的是商户状态还是核销状态 | 按该角色的状态表处理;0/1 在两类业务里含义不同 |
| 没收到通知 | notify_url 是否公网可达;接收日志、HTTP 状态及响应正文 | 先查询业务结果,再排查通知;通知失败不等于订单失败 |
| 到期后仍在处理中 | 结果是否仍在确认 | 保留原单并稍后查询,不以本地时间覆盖平台终态 |
创建订单
创建支付订单,取得付款链接。
商户凭证 · 创建 → 打开 payurl → 等待通知或查询
请求参数
| 参数 | 类型 | 必填 | 说明 | 示例 |
|---|---|---|---|---|
pid | Integer | 是 | 商户号 | 10001 |
type | String | 是 | 1:订单支付;2:登录充值。以商户支付权限中展示的支付编码为准,不填写通道编码、通道 ID 或归属;其他支付模式使用后台分配的编码 | 1 |
out_trade_no | String | 是 | 商户订单号;同一商户下必须唯一 | ORDER_20260927_000001 |
notify_url | String | 是 | 服务器异步回调地址,必须为 HTTP/HTTPS | https://merchant.example/callback |
return_url | String | 是 | 支付完成后的页面跳转地址 | https://merchant.example/result |
name | String | 是 | 订单名称,不得包含等号或后台屏蔽词 | 业务订单 |
money | Decimal String | 是 | 金额单位为元;建议字符串,如 100.00。精度、范围与可用面额受全局及支付编码额度规则限制 | 100.00 |
sitename | String | 否 | 商户站点或业务名称 | 示例业务 |
sign | String | 是 | 按统一签名规则生成 | 32位小写MD5 |
sign_type | String | 否 | 固定为 MD5 | MD5 |
成功返回字段
| 字段 | 类型 | 说明 |
|---|---|---|
code | Integer | 200 表示创建接口调用成功,不代表付款完成 |
msg | String | 提示文本,仅用于展示 |
payurl | String | 付款入口,位于响应顶层,不在 data 内;直接交给用户打开 |
trade_no | String | 平台订单号;按原样保存,商户查询 type=1 使用此号 |
out_trade_no | String | 请求中的商户订单号;商户查询 type=2 使用此号 |
type | String | 原始请求支付编码;额外字段 route_type 在路由编码不同时出现 |
money | Number / String | 订单金额,按精确金额核对,勿用浮点数直接比较 |
code_url | String | 二维码图片地址;二维码内容和付款方式以当前支付页为准 |
collection_mode / collection_mode_name / cashier_mode | String | 收款模式、模式名称和收银展示模式;不要据此判定支付成功 |
payee_name / payee_account | String | 收款人及收款账户展示信息,非转账模式为空字符串 |
支付编码与时限
当前下单入口已停用被替换的旧支付编码。新订单的返回与通知使用对应的新编码;历史订单保留创建时的编码,请按实际返回参数验签,不要在接收端改写编码。
推荐使用表单格式提交参数;也支持 JSON 对象请求。响应中的 payurl 是付款入口,直接交给付款用户打开。示例均为虚构数据。cURL 用于展示报文;PHP / Python 示例会在你的服务器计算签名,运行前需配置真实凭证。
进阶:路由与支付时限
type=1 为订单支付,先创建本地商户订单并返回付款地址,首次打开支付页后才准备上游支付信息;type=2 为登录充值,流程保持不变。按商户实际授权选择,不要把核销上传的 order_code=hx 当作支付编码。使用返回的 payurl 打开平台支付页。桌面端在本页显示付款二维码;移动端尝试打开支付页面,未自动跳转时可点击“去支付”。可用付款方式以实际支付页面为准。valid_minutes 是核销订单自身有效期,两者分别计算。请求示例
curl -X POST 'https://dy.dyzfzx.com/payment/orders' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'pid=YOUR_MERCHANT_ID' \
--data-urlencode 'type=1' \
--data-urlencode 'out_trade_no=ORDER_20260927_000001' \
--data-urlencode 'notify_url=https://merchant.example/callback' \
--data-urlencode 'return_url=https://merchant.example/result' \
--data-urlencode 'name=业务订单' \
--data-urlencode 'money=100.00' \
--data-urlencode 'sign=SIGN_VALUE' \
--data-urlencode 'sign_type=MD5'
下单成功响应
{
"code": 200,
"msg": "获取成功!",
"trade_no": "P20260927000001",
"type": "1",
"out_trade_no": "ORDER_20260927_000001",
"money": "100.00",
"code_url": "LOCAL_QRCODE_URL",
"payurl": "https://dy.dyzfzx.com/dy/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"collection_mode": "h5",
"collection_mode_name": "H5支付",
"cashier_mode": "h5"
}
type 始终返回商户下单时提交的编码。仅当提交一级编码并发生聚合路由时返回 route_type,表示本次实际使用的二级支付编码;直接使用二级编码下单时不返回该字段。/payment/orders 返回原有商户响应结构,下单成功使用 code=200;失败使用 code=201,并返回 error_code、retryable 和 data=null。它不是 /mapi.php 的别名;后者仍保留原 code=1 响应。查询订单
按平台订单号或商户订单号查询当前商户自己的订单,其他商户的订单不会返回。
type=1/2 表示订单号种类,不是创建订单时的支付编码。请求参数按下表填写,返回的 data.type 才是订单的支付编码。| 参数 | 类型 | 必填 | 说明 | 示例 |
|---|---|---|---|---|
pid | Integer | 是 | 商户号 | 10001 |
order_no | String | 是 | 需要查询的订单号 | ORDER_20260927_000001 |
type | Integer | 是 | 1 平台订单号;2 商户订单号 | 2 |
sign | String | 是 | 按统一签名规则生成 | 32位小写MD5 |
sign_type | String | 否 | 固定为 MD5 | MD5 |
标准请求
POST /payment/orders/query
Content-Type: application/x-www-form-urlencoded
pid=YOUR_MERCHANT_ID&order_no=ORDER_20260927_000001&type=2&sign=SIGN_VALUE&sign_type=MD5
返回字段
| 字段 | 类型 | 说明 |
|---|---|---|
code / msg | Integer / String | 200 为查询接口成功;不能只看 msg 判定结果 |
data.id | Integer / 数字字符串 | 商户 ID,不是订单 ID |
data.type | String | 创建时的支付编码,不是本次查询请求的 type |
data.route_type | String,可选 | 路由编码与请求编码不同时返回 |
data.trade_no | String | 平台订单号 |
data.out_trade_no | String | 商户业务订单号,务必与本地记录核对 |
data.name | String | 订单名称 |
data.money | Number / String | 订单金额,单位元;核对时按十进制定点金额比较 |
data.status | Integer | 0 未支付、1 已支付、2 支付超时、3 支付错误、4 订单风控 |
data.status_name | String | 状态中文说明,仅用于展示 |
data.failure_reason | String | 支付错误或订单风控时的原因,其他状态通常为空 |
data.status=1 表示已支付。未知状态、网络异常、查不到订单,不等于已支付;通知和查询冲突时核对并告警,不直接覆盖已有结果。成功响应
{
"code": 200,
"msg": "获取成功!",
"data": {
"id": 10001,
"type": "1",
"trade_no": "P20260927000001",
"out_trade_no": "ORDER_20260927_000001",
"name": "业务订单",
"money": "100.00",
"status": 1,
"status_name": "已支付",
"failure_reason": ""
}
}
type 为原始请求编码;聚合下单时额外返回 route_type。老订单或直接二级编码下单不会返回 route_type。失败响应
{
"code": 201,
"error_code": "ORDER_NOT_FOUND",
"msg": "未找到该商户订单",
"retryable": false,
"data": null
}
0 未支付,1 已支付,2 支付超时,3 支付错误,4 订单风控。status_name 返回对应中文状态;支付错误或订单风控时,failure_reason 返回可公开的失败原因。支付通知
订单支付成功后,平台以 GET 查询参数请求下单时提交的 notify_url。商户应先验签,再依据商户订单号幂等更新业务状态。
| 参数 | 类型 | 说明 |
|---|---|---|
pid | Integer | 商户号 |
trade_no | String | 平台订单号 |
out_trade_no | String | 商户订单号 |
type | String | 商户下单时提交的一级或二级编码 |
route_type | String | 聚合下单实际使用的二级支付编码;直接二级编码下单时不发送 |
name | String | 订单名称;后台启用隐藏名称时不发送 |
money | Decimal | 订单金额 |
trade_status | String | 支付成功固定为 TRADE_SUCCESS |
sign | String | 回调签名 |
sign_type | String | 固定为 MD5 |
标准回调示例
GET /callback?pid=YOUR_MERCHANT_ID&trade_no=P20260927000001&out_trade_no=ORDER_20260927_000001&type=1&name=%E4%B8%9A%E5%8A%A1%E8%AE%A2%E5%8D%95&money=100.00&trade_status=TRADE_SUCCESS&sign=SIGN_VALUE&sign_type=MD5
HTTP/1.1 200 OK
Content-Type: text/plain; charset=UTF-8
success
处理要求
- 验签通过,并确认
pid、out_trade_no、money与本地订单一致。 - 仅当
trade_status=TRADE_SUCCESS时更新订单,重复通知必须幂等返回成功。 - 处理完成后返回 HTTP 2xx,响应正文必须且只能为小写
success。
route_type 参与签名。请对实际收到的全部非空业务字段排序验签,不要使用写死字段列表。success 均视为失败。上传订单
授权账户可上传一笔待处理订单。当前订单编码固定为 hx,target_account 填写该笔业务实际需要处理的目标账号,不是核销商登录名、PID 或订单号;所有请求都必须显式传递 order_code=hx,并将该字段计入签名。
| 参数 | 类型 | 必填 | 说明 | 示例 |
|---|---|---|---|---|
pid | Integer | 是 | 核销商账户号 | 20001 |
order_code | String | 是 | 订单编码,固定为 hx;该字段参与签名 | hx |
out_trade_no | String | 是 | 外部订单号;同一账户下唯一,最长 100 字符且不能含空格 | UPLOAD_20260927_000001 |
target_account | String | 是 | 实际业务目标账号,最长 64 字符,不能含空格或控制字符;不要填登录名/PID | 123456(虚构示例) |
money | Integer | 是 | 订单金额,必须为大于 0 的整数 | 100 |
valid_minutes | Integer | 否 | 订单有效期,范围 3–1440 分钟;不传时使用平台全局默认值,该字段传入后参与签名 | 60 |
notify_url | String | 否 | HTTP/HTTPS 结果通知地址,最长 500 字符;不传则不通知,需主动查询 | https://uploader.example/callback |
sign | String | 是 | 使用核销商账户密钥签名 | 32位小写MD5 |
sign_type | String | 否 | 固定为 MD5 | MD5 |
受理成功后看什么
code=200 只代表受理成功。保存 data.trade_no 和 data.out_trade_no;data.status=0/1 等待,2 成功,3 失败。完整返回字段见下方与查询章节。| 字段 | 类型 | 说明 |
|---|---|---|
code / msg | Integer / String | 接口调用结果及提示文本 |
data | Object | 订单信息,与查询接口 data 的字段一致 |
data.trade_no | String | 新核销订单为 J + 27 位数字;完整字符串保存,历史号码不改 |
data.status | Integer | 0 待处理、1 处理中、2 处理成功、3 处理失败 |
data.expires_at | String | 订单有效期,不是付款倒计时 |
data.finished_at | String | 完成时间,未完成为空字符串;通知字段名为 finish_time |
请求示例
curl -X POST 'https://dy.dyzfzx.com/supplier/orders' \
-H 'Content-Type: application/json' \
-d '{
"pid": "YOUR_UPLOAD_ACCOUNT_ID",
"order_code": "hx",
"out_trade_no": "UPLOAD_20260927_000001",
"target_account": "123456",
"money": 100,
"valid_minutes": 60,
"notify_url": "https://uploader.example/callback",
"sign": "SIGN_VALUE",
"sign_type": "MD5"
}'
成功响应
{
"code": 200,
"msg": "订单上传成功,等待处理",
"data": {
"order_code": "hx",
"trade_no": "J202609271000001234567890123",
"out_trade_no": "UPLOAD_20260927_000001",
"target_account": "123456",
"money": "100.00",
"status": 0,
"status_name": "待处理",
"failure_reason": "",
"created_at": "2026-09-27 10:00:00",
"expires_at": "2026-09-27 11:00:00",
"finished_at": ""
}
}
失败响应
{
"code": 201,
"error_code": "DUPLICATE_ORDER",
"msg": "外部订单号已存在,请勿重复上传",
"retryable": false,
"data": null
}
valid_minutes 可按订单传入 3–1440 分钟;不传时使用平台全局默认有效期(初始值 1440 分钟)。到期后由平台执行超时处理;支付中或结果尚未确认时,可能继续核对。不能仅凭本地倒计时将订单判为失败,以查询或验签通知的终态为准。修改全局默认值只影响之后新建的订单。查询核销订单
查询当前上传账户自己的订单。逐笔查单、对账和更新本地订单时,必须使用上传时的外部订单号 out_trade_no,并核对响应里的 data.out_trade_no。
查询请求必须使用 order_code=hx。本版本新订单返回 hx;历史订单的返回与通知保留原订单编码,不回写历史记录。
out_trade_no 查询,并按该订单号处理返回结果。此处 out_trade_no 是上传方提交的外部订单号;平台返回的任务号 trade_no 不是本接口的查询参数。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
pid | Integer | 是 | 核销商账户号 |
order_code | String | 是 | 订单编码,固定为 hx;该字段参与签名 |
out_trade_no | String | 对账必填 | 上传时提交的外部订单号;逐笔对账使用此字段 |
sign | String | 是 | 使用核销商账户密钥签名 |
sign_type | String | 否 | 固定为 MD5 |
标准请求
POST /supplier/orders/query
Content-Type: application/json
{
"pid": "YOUR_UPLOAD_ACCOUNT_ID",
"order_code": "hx",
"out_trade_no": "UPLOAD_20260927_000001",
"sign": "SIGN_VALUE",
"sign_type": "MD5"
}
补充查询方式:按目标账号查询最近一笔
接口也允许仅传 target_account 而不传 out_trade_no,此时返回当前账户该目标账号最近一笔订单。两者都传时必须同时匹配。该方式不能用于逐笔对账,主流程始终传 out_trade_no。
返回字段(上传成功也使用同一 data 结构)
| 字段 | 类型 | 说明 |
|---|---|---|
code / msg | Integer / String | 接口调用结果及提示;200 不代表订单处理成功 |
data.order_code | String | 新订单 hx;历史订单按实际值验签 |
data.trade_no | String | 平台核销订单号;新号为 J + 27 位数字,不作为本接口查询参数 |
data.out_trade_no | String | 上传时的外部订单号 |
data.target_account | String | 业务目标账号 |
data.money | String | 金额,单位元、两位小数,如 100.00 |
data.status | Integer | 0 待处理、1 处理中、2 处理成功、3 处理失败 |
data.status_name | String | 中文状态名称,仅用于展示 |
data.failure_reason | String | 失败原因,仅 status=3 有值,其他状态为空字符串 |
data.created_at | String | 创建时间,北京时间,YYYY-MM-DD HH:mm:ss |
data.expires_at | String | 订单有效期;到期不一定立即返回失败终态 |
data.finished_at | String | 完成时间,未完成为空字符串;通知中的同义字段为 finish_time |
data.status 状态说明
查询成功,且订单处理成功
{
"code": 200,
"msg": "查询成功",
"data": {
"order_code": "hx",
"trade_no": "J202609271000001234567890123",
"out_trade_no": "UPLOAD_20260927_000001",
"target_account": "123456",
"money": "100.00",
"status": 2,
"status_name": "处理成功",
"failure_reason": "",
"created_at": "2026-09-27 10:00:00",
"expires_at": "2026-09-27 10:30:00",
"finished_at": "2026-09-27 10:06:18"
}
}
查询成功,但订单处理失败
{
"code": 200,
"msg": "查询成功",
"data": {
"order_code": "hx",
"trade_no": "J202609271000001234567890124",
"out_trade_no": "UPLOAD_EXAMPLE_000002",
"target_account": "123456",
"money": "100.00",
"status": 3,
"status_name": "处理失败",
"failure_reason": "订单已过期",
"created_at": "2026-09-27 10:00:00",
"expires_at": "2026-09-27 11:00:00",
"finished_at": "2026-09-27 11:00:00"
}
}查询成功,订单仍在处理中
{
"code": 200,
"msg": "查询成功",
"data": {
"order_code": "hx",
"trade_no": "J202609271000001234567890124",
"out_trade_no": "UPLOAD_EXAMPLE_000002",
"target_account": "123456",
"money": "100.00",
"status": 1,
"status_name": "处理中",
"failure_reason": "",
"created_at": "2026-09-27 10:00:00",
"expires_at": "2026-09-27 11:00:00",
"finished_at": ""
}
}只有 data.status=2 才能将对应上传订单记为成功;0 和 1 应继续等待,3 记为失败并保存 failure_reason。接口调用失败、网络异常或未知状态不能当作订单成功,也不能直接当作订单处理失败。
核销结果通知
上传订单变为处理成功或处理失败时,平台以 GET 查询参数请求上传时提交的 notify_url。未传回调地址时不发送。
新订单通知使用 order_code=hx 并参与签名;历史订单沿用原编码。请直接对收到的原始字段验签,不要先替换编码。
| 参数 | 类型 | 说明 |
|---|---|---|
pid | Integer | 核销商账户号 |
order_code | String | 新订单为 hx;历史订单保留原编码 |
trade_no | String | 平台任务订单号 |
out_trade_no | String | 外部订单号 |
target_account | String | 订单目标账号 |
money | Decimal | 订单金额 |
status | Integer | 2 处理成功;3 处理失败 |
status_name | String | 处理成功或处理失败 |
failure_reason | String | 失败原因,成功时为空 |
finish_time | String | 完成时间,北京时间 YYYY-MM-DD HH:mm:ss;查询接口字段名为 finished_at |
sign | String | 使用核销商账户密钥生成的签名 |
sign_type | String | 固定为 MD5 |
处理成功通知示例(status=2)
GET /callback?pid=YOUR_UPLOAD_ACCOUNT_ID&order_code=hx&trade_no=J202609271000001234567890123&out_trade_no=UPLOAD_20260927_000001&target_account=123456&money=100.00&status=2&status_name=%E5%A4%84%E7%90%86%E6%88%90%E5%8A%9F&failure_reason=&finish_time=2026-09-27%2010%3A06%3A18&sign=SIGN_VALUE&sign_type=MD5
HTTP/1.1 200 OK
Content-Type: text/plain; charset=UTF-8
success
处理失败通知示例(status=3)
GET /callback?pid=YOUR_UPLOAD_ACCOUNT_ID&order_code=hx&trade_no=J202609271000001234567890124&out_trade_no=UPLOAD_EXAMPLE_000002&target_account=123456&money=100.00&status=3&status_name=%E5%A4%84%E7%90%86%E5%A4%B1%E8%B4%A5&failure_reason=%E8%AE%A2%E5%8D%95%E5%B7%B2%E8%BF%87%E6%9C%9F&finish_time=2026-09-27%2011%3A00%3A00&sign=SIGN_VALUE&sign_type=MD5
HTTP/1.1 200 OK
Content-Type: text/plain; charset=UTF-8
successsuccess,仅确认通知已处理。不得据此将订单记为成功。示例中的 SIGN_VALUE 是占位符,实际签名按收到并 URL 解码后的全部非空业务参数计算。处理要求
- 使用核销商账户密钥验签;按当前账户及
out_trade_no定位本地订单,核对pid、order_code=hx、目标账号、金额及已保存的平台任务号。不要只按目标账号更新订单。 - 依据 GET 参数
status分支:2更新为处理成功;3更新为处理失败并记录原因。不要依据status_name文本或验签成功判断订单成功。 - 业务结果须先事务落库并完成幂等处理,再返回 HTTP 2xx,接入端规范响应正文为小写
success;相同通知重复到达时不能重复结算。 - 验签失败、订单信息不符、未知状态或本地保存失败时,不返回
success。如本地已有相冲突的终态,应按外部订单号核对并告警,不直接覆盖。
完整流程示例
按账户类型选择一份脚本。每份都包含签名、表单请求、响应检查,以及使用同一个业务订单号查询一次。PHP 8 需启用 cURL;Python 3 仅使用标准库。
| 环境变量 | 商户脚本 | 核销商脚本 |
|---|---|---|
| MERCHANT_PID / MERCHANT_KEY | 商户 PID / 密钥 | 不使用 |
| SUPPLIER_PID / SUPPLIER_KEY | 不使用 | 核销商 PID / 密钥 |
| BUSINESS_ORDER_NO | 你已保存的唯一商户订单号 | 你已保存的唯一外部订单号 |
| NOTIFY_URL | 服务器支付通知地址 | 服务器结果通知地址;示例选择传入 |
| RETURN_URL | 用户返回页面地址 | 不使用 |
| TARGET_ACCOUNT | 不使用 | 真实业务目标账号 |
示例金额为 100 元,核销有效期为 60 分钟。运行前按你的真实业务调整。每次重试保留 BUSINESS_ORDER_NO;超时后先使用查询接口,不要重新运行整段创建流程。
商户:创建订单 → 查询付款结果
<?php
// PHP 8 + cURL。请先在服务端配置本页列出的环境变量。
function requiredEnv(string $name): string {
$value = getenv($name);
if ($value === false || $value === '') throw new RuntimeException('缺少环境变量: ' . $name);
return $value;
}
function makeSign(array $params, string $key): string {
unset($params['sign'], $params['sign_type']);
$params = array_filter($params, static fn($value) => $value !== '');
ksort($params, SORT_STRING);
$pairs = [];
foreach ($params as $name => $value) $pairs[] = $name . '=' . $value;
return md5(implode('&', $pairs) . $key);
}
function postForm(string $url, array $params, string $key): array {
$params['sign'] = makeSign($params, $key);
$params['sign_type'] = 'MD5';
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($params),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 15,
CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2,
]);
$body = curl_exec($ch);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
// 超时/非 JSON/HTTP 异常都不能证明下单失败;先按原业务订单号查询。
if ($body === false) throw new RuntimeException('结果未确认,请先查单: ' . $error);
if ($http < 200 || $http >= 300) throw new RuntimeException('HTTP ' . $http . ': ' . $body);
$result = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
if (!is_array($result) || (int)($result['code'] ?? 0) !== 200) {
throw new RuntimeException(json_encode($result, JSON_UNESCAPED_UNICODE));
}
return $result;
}
$baseUrl = 'https://dy.dyzfzx.com';
$key = requiredEnv('MERCHANT_KEY');
$outTradeNo = requiredEnv('BUSINESS_ORDER_NO'); // 保存后重用,不随重试更换
$params = [
'pid' => requiredEnv('MERCHANT_PID'),
'type' => '1',
'out_trade_no' => $outTradeNo,
'notify_url' => requiredEnv('NOTIFY_URL'),
'return_url' => requiredEnv('RETURN_URL'),
'name' => '业务订单',
'money' => '100.00',
];
$result = postForm($baseUrl . '/payment/orders', $params, $key);
echo '付款链接: ' . $result['payurl'] . PHP_EOL;
// 将 payurl 交给付款用户;此处不会自动打开或完成付款。
// 演示用同一个业务订单号查询一次;未完成时等待通知或稍后查询。
$query = postForm($baseUrl . '/payment/orders/query', [
'pid' => $params['pid'],
'type' => '2', 'order_no' => $outTradeNo,
], $key);
$order = $query['data'];
if ((string)$order['out_trade_no'] !== $outTradeNo) throw new RuntimeException('订单号不一致');
$status = (int)$order['status'];
$states = [0=>'未支付', 1=>'已支付', 2=>'支付超时', 3=>'支付错误', 4=>'订单风控'];
if (!isset($states[$status])) throw new RuntimeException('未知状态,请人工核对');
echo $states[$status] . PHP_EOL;
// 实际记账前还需核对账户、金额等字段,并在事务中幂等保存。# Python 3:仅使用标准库。先在服务端配置本页列出的环境变量。
import hashlib
import json
import os
import urllib.parse
import urllib.request
def required_env(name):
value = os.environ.get(name, "")
if not value:
raise RuntimeError("缺少环境变量: " + name)
return value
def make_sign(params, key):
source = "&".join(f"{k}={v}" for k, v in sorted(params.items())
if k not in ("sign", "sign_type") and v != "")
return hashlib.md5((source + key).encode("utf-8")).hexdigest()
def post_form(url, params, key):
fields = dict(params)
fields["sign"] = make_sign(fields, key)
fields["sign_type"] = "MD5"
request = urllib.request.Request(url, method="POST",
data=urllib.parse.urlencode(fields).encode("utf-8"),
headers={"Content-Type": "application/x-www-form-urlencoded"})
# HTTPS 验证默认开启。超时/非 JSON/HTTP 异常时先按原业务订单号查单。
with urllib.request.urlopen(request, timeout=15) as response:
result = json.load(response)
if not isinstance(result, dict) or int(result.get("code", 0)) != 200:
raise RuntimeError(json.dumps(result, ensure_ascii=False))
return result
base_url = "https://dy.dyzfzx.com"
key = required_env("MERCHANT_KEY")
out_trade_no = required_env("BUSINESS_ORDER_NO") # 保存后重用
params = {
"pid": required_env("MERCHANT_PID"),
"type": "1",
"out_trade_no": out_trade_no,
"notify_url": required_env("NOTIFY_URL"),
"return_url": required_env("RETURN_URL"),
"name": "业务订单",
"money": "100.00",
}
result = post_form(base_url + "/payment/orders", params, key)
print("付款链接:", result["payurl"])
# 将 payurl 交给付款用户;此处不会自动打开或完成付款。
# 用同一个业务订单号查询一次;未完成时等待通知或稍后查询。
query = post_form(base_url + "/payment/orders/query", {
"pid": params["pid"],
"type": "2", "order_no": out_trade_no,
}, key)
order = query["data"]
if str(order["out_trade_no"]) != out_trade_no:
raise RuntimeError("订单号不一致")
status = int(order["status"])
states = {0: "未支付", 1: "已支付", 2: "支付超时", 3: "支付错误", 4: "订单风控"}
if status not in states:
raise RuntimeError("未知状态,请人工核对")
print(states[status])
# 实际记账前还需核对账户、金额等字段,并在事务中幂等保存。核销商:上传订单 → 查询处理结果
<?php
// PHP 8 + cURL。请先在服务端配置本页列出的环境变量。
function requiredEnv(string $name): string {
$value = getenv($name);
if ($value === false || $value === '') throw new RuntimeException('缺少环境变量: ' . $name);
return $value;
}
function makeSign(array $params, string $key): string {
unset($params['sign'], $params['sign_type']);
$params = array_filter($params, static fn($value) => $value !== '');
ksort($params, SORT_STRING);
$pairs = [];
foreach ($params as $name => $value) $pairs[] = $name . '=' . $value;
return md5(implode('&', $pairs) . $key);
}
function postForm(string $url, array $params, string $key): array {
$params['sign'] = makeSign($params, $key);
$params['sign_type'] = 'MD5';
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($params),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 15,
CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2,
]);
$body = curl_exec($ch);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
// 超时/非 JSON/HTTP 异常都不能证明下单失败;先按原业务订单号查询。
if ($body === false) throw new RuntimeException('结果未确认,请先查单: ' . $error);
if ($http < 200 || $http >= 300) throw new RuntimeException('HTTP ' . $http . ': ' . $body);
$result = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
if (!is_array($result) || (int)($result['code'] ?? 0) !== 200) {
throw new RuntimeException(json_encode($result, JSON_UNESCAPED_UNICODE));
}
return $result;
}
$baseUrl = 'https://dy.dyzfzx.com';
$key = requiredEnv('SUPPLIER_KEY');
$outTradeNo = requiredEnv('BUSINESS_ORDER_NO'); // 保存后重用,不随重试更换
$params = [
'pid' => requiredEnv('SUPPLIER_PID'),
'order_code' => 'hx',
'out_trade_no' => $outTradeNo,
'target_account' => requiredEnv('TARGET_ACCOUNT'),
'money' => '100',
'valid_minutes' => '60',
'notify_url' => requiredEnv('NOTIFY_URL'),
];
$result = postForm($baseUrl . '/supplier/orders', $params, $key);
// 演示用同一个业务订单号查询一次;未完成时等待通知或稍后查询。
$query = postForm($baseUrl . '/supplier/orders/query', [
'pid' => $params['pid'],
'order_code' => 'hx', 'out_trade_no' => $outTradeNo,
], $key);
$order = $query['data'];
if ((string)$order['out_trade_no'] !== $outTradeNo) throw new RuntimeException('订单号不一致');
$status = (int)$order['status'];
$states = [0=>'待处理', 1=>'处理中', 2=>'处理成功', 3=>'处理失败'];
if (!isset($states[$status])) throw new RuntimeException('未知状态,请人工核对');
echo $states[$status] . PHP_EOL;
// 实际记账前还需核对账户、金额等字段,并在事务中幂等保存。# Python 3:仅使用标准库。先在服务端配置本页列出的环境变量。
import hashlib
import json
import os
import urllib.parse
import urllib.request
def required_env(name):
value = os.environ.get(name, "")
if not value:
raise RuntimeError("缺少环境变量: " + name)
return value
def make_sign(params, key):
source = "&".join(f"{k}={v}" for k, v in sorted(params.items())
if k not in ("sign", "sign_type") and v != "")
return hashlib.md5((source + key).encode("utf-8")).hexdigest()
def post_form(url, params, key):
fields = dict(params)
fields["sign"] = make_sign(fields, key)
fields["sign_type"] = "MD5"
request = urllib.request.Request(url, method="POST",
data=urllib.parse.urlencode(fields).encode("utf-8"),
headers={"Content-Type": "application/x-www-form-urlencoded"})
# HTTPS 验证默认开启。超时/非 JSON/HTTP 异常时先按原业务订单号查单。
with urllib.request.urlopen(request, timeout=15) as response:
result = json.load(response)
if not isinstance(result, dict) or int(result.get("code", 0)) != 200:
raise RuntimeError(json.dumps(result, ensure_ascii=False))
return result
base_url = "https://dy.dyzfzx.com"
key = required_env("SUPPLIER_KEY")
out_trade_no = required_env("BUSINESS_ORDER_NO") # 保存后重用
params = {
"pid": required_env("SUPPLIER_PID"),
"order_code": "hx",
"out_trade_no": out_trade_no,
"target_account": required_env("TARGET_ACCOUNT"),
"money": "100",
"valid_minutes": "60",
"notify_url": required_env("NOTIFY_URL"),
}
result = post_form(base_url + "/supplier/orders", params, key)
# 用同一个业务订单号查询一次;未完成时等待通知或稍后查询。
query = post_form(base_url + "/supplier/orders/query", {
"pid": params["pid"],
"order_code": "hx", "out_trade_no": out_trade_no,
}, key)
order = query["data"]
if str(order["out_trade_no"]) != out_trade_no:
raise RuntimeError("订单号不一致")
status = int(order["status"])
states = {0: "待处理", 1: "处理中", 2: "处理成功", 3: "处理失败"}
if status not in states:
raise RuntimeError("未知状态,请人工核对")
print(states[status])
# 实际记账前还需核对账户、金额等字段,并在事务中幂等保存。通知接收指南
平台向创建订单时提交的 notify_url 发送 GET 请求。它是接入方自己的服务器地址,不是平台新增的固定回调路径,也不是 JSON POST 通知。
- 验证来源使用对应账户密钥验签。
- 保存结果核对订单,事务落库且幂等。
- 确认接收返回 HTTP 2xx 和 success。
按业务分别判断结果
接收方必须做到
- 对实际收到的全部非空业务参数验签,移除
sign、sign_type;新增的可选参数不能漏签。 - 按当前账户和业务订单号定位本地记录,核对金额、账号及该业务的相关字段,不能只凭验签成功就更新任意订单。
- 重复通知不得重复记账;终态冲突时先查询核对,不直接覆盖已保存的结果。
- 仅在验证和保存完成后确认接收。验签失败、未知状态或落库失败时,不返回
success。
确认响应
HTTP/1.1 200 OK
Content-Type: text/plain; charset=UTF-8
success实现会对正文去首尾空白并忽略大小写,但接入端请统一输出小写纯文本 success,不要依赖宽松兼容。
避免这些确认方式
{ "success": true } // 不是纯文本
SUCCESS // 请统一输出规范的小写 success
<html>success</html> // 不是纯文本
失败订单的通知也需要在保存后
回复 success;这不会把订单改成成功。最小验签参考(PHP)
下例只做验签,不处理业务、不确认接收。复用签名章节或完整示例中的 makeSign 函数,密钥选用本回调对应的商户或核销商密钥。验签后还必须核对订单并完成事务保存。
$params = $_GET; // PHP 已完成一次 URL 解码,不要再次 urldecode
$received = $params['sign'] ?? '';
foreach ($params as $value) {
if (!is_string($value)) throw new RuntimeException('非法参数类型');
}
if (!is_string($received) || !preg_match('/^[a-f0-9]{32}$/D', $received)
|| !hash_equals(makeSign($params, $key), $received)) {
http_response_code(400);
exit('invalid signature');
}
// 此后必须按账户和 out_trade_no 读本地订单,核对金额、目标及状态。
// 在事务内幂等保存;成功提交后才输出 success。
// 尚未实现业务保存时不要返回 success,此参考不能直接作为完整回调上线。