四方支付商户接口对接文档

本文档提供给商户服务端系统对接,用于接入平台下单、查单和订单状态通知。除固定联调产品编码 0001 外,所有示例中的商户号、密钥、产品编码和域名均为占位内容,实际值以平台后台提供为准。

提交方式:POST Form Content-Type:application/x-www-form-urlencoded; charset=UTF-8 字符集:UTF-8 金额单位:分 签名方式:HMAC-SHA256 V2(新商户强制)/ MD5 V1(存量兼容) 时间格式:yyyyMMddHHmmss

1. 对接资料

平台运营会给商户技术或对接系统提供以下资料:

名称说明
mchId商户号。每个商户唯一,下单、查单和验签都需要使用。
secretKey商户签名密钥。运营后台在开户或重置密钥时展示;绑定商户群内的授权管理员/操作人执行开户或查询对接信息后,机器人会将密钥以折叠遮罩写入当前商户群。请妥善保存并勿转发,只在商户服务端使用。
productId产品编码。固定联调产品 0001 会自动绑定全部现有及后续新增商户,费率固定为0且不可修改或解绑;平台内部上游测试编码 0002 不向正式商户开放。
下单地址{平台域名}/api/v1/pay/create_order
查单地址{平台域名}/api/v1/pay/query_order
回调IP平台通知商户时的公网 IPv4 出口。商户如果做通知来源白名单,应放行平台提供的全部 IPv4;无需配置 IPv6。
商户API白名单商户调用下单/查单接口的服务器公网 IP,需要提前提供给平台配置。
商户通知地址不在平台后台固定配置。商户每次下单时通过 notifyUrl 传入本笔订单的通知地址。
商户下单、查单、接收支付通知均统一使用 POST Form。商户服务端只需要按本文档这一套格式对接,不需要适配其它提交方式。

2. 编码、URL 解码与字段约束

请求和通知均使用 application/x-www-form-urlencoded; charset=UTF-8。签名与业务处理都以服务端完成一次 Form URL 解码后的字段名和值为准,不以 HTTP 原始编码串为准。

字段最大值补充约束
mchId64 UTF-8 字节必填标识,不得有首尾空白。
mchOrderNo128 UTF-8 字节必填标识,不得有首尾空白。
productId64 UTF-8 字节必填标识,不得有首尾空白。
amount1 至 9,007,199,254,740,991 分仅 ASCII 十进制正整数;上限为 JavaScript Number.MAX_SAFE_INTEGER,避免 JSON 数字在商户系统中被静默舍入。实际通道金额规则通常远小于该值。
notifyUrl2048 UTF-8 字节必填;生产环境必须是公网 https 地址,平台会在每次通知前重新校验解析结果。
subject128 UTF-8 字节可选订单标题。
body512 UTF-8 字节可选订单描述。
param1 / param2各 256 UTF-8 字节可选透传字段。
reqTime14 UTF-8 字节固定为 14 位数字 yyyyMMddHHmmss
signVersion16 UTF-8 字节新开户固定必填 v2;只有后台仍明确保留 V1 最低策略的存量商户,省略时才按 v1 兼容。
sign64 UTF-8 字节必填;V2 为 64 位大写 HMAC-SHA256,V1 为 32 位大写 MD5。
整个 HTTP 请求体最大为 1 MiB,超过会返回 HTTP 413。字段长度按 UTF-8 编码后的字节数计算,不是中文字符数;商户应在发送前自行校验。

3. 签名规则

新开户统一强制使用 signVersion=v2。存量商户在迁移期间可由管理员保留 V1 最低策略,升级完成后应提升为 V2;平台会记住首次下单使用的签名版本,并用相同版本签署该订单后续通知。查单请求也必须满足该商户当前最低签名策略。

V2:HMAC-SHA256(推荐)

  1. 参数中明确加入 signVersion=v2,取完成一次 Form URL 解码后的全部参数,只排除小写 sign;V2 会保留空字符串字段。
  2. 按参数名 ASCII 从小到大排序,以 UTF-8 字节处理字段名和值。
  3. 签名原文先写入固定字节 SFPAY-MERCHANT-SIGN-V2\0;随后对每个字段依次写入:4 字节无符号大端字段名字节长度、字段名字节、4 字节无符号大端值字节长度、值字节。
  4. 使用商户密钥作为 HMAC key,对完整原文计算 HMAC-SHA256,结果转为大写 64 位十六进制。
reqTime 默认必传并参与签名,格式为 yyyyMMddHHmmss。服务器允许时间误差为 5 分钟,超时会拒绝,防止重放攻击。

V2 Node.js 签名示例

import crypto from 'node:crypto';

function makeSign(params, secretKey) {
  const chunks = [Buffer.from('SFPAY-MERCHANT-SIGN-V2\0', 'utf8')];
  for (const key of Object.keys(params).filter((k) => k !== 'sign').sort()) {
    const keyBytes = Buffer.from(key, 'utf8');
    const valueBytes = Buffer.from(String(params[key] ?? ''), 'utf8');
    const keyLength = Buffer.alloc(4); keyLength.writeUInt32BE(keyBytes.length);
    const valueLength = Buffer.alloc(4); valueLength.writeUInt32BE(valueBytes.length);
    chunks.push(keyLength, keyBytes, valueLength, valueBytes);
  }
  return crypto.createHmac('sha256', Buffer.from(secretKey, 'utf8'))
    .update(Buffer.concat(chunks)).digest('hex').toUpperCase();
}

V1:旧 MD5 兼容期

只有后台仍明确保留 V1 最低策略的存量商户,省略 signVersion(或传 v1)时,才按“排除空值与 sign、ASCII 排序、拼接 key=value&...、末尾追加 &key=密钥、计算大写 MD5”验证。为消除旧格式字段边界碰撞,V1 的非空字段名和值不得含 &=,字段名 key 也不允许使用。带查询参数的 notifyUrl 必须升级 V2。新开户和已提升到 V2 策略的商户会直接拒绝 V1。

4. 下单接口

地址:POST {平台域名}/api/v1/pay/create_order

字段必填最大值说明
mchId64 字节商户号;不得有前导或尾随空白。
mchOrderNo128 字节商户订单号。同一商户下必须唯一,且不得有前导或尾随空白。重复请求只有金额、产品、通知地址、标题、描述、param1、param2 和签名版本全部一致时才返回原订单,任何一项变化都会返回 409。
productId64 字节平台提供的产品编码;首次联调固定使用 0001;不得有前导或尾随空白。
amount1 至 9,007,199,254,740,991 分订单金额,单位为分,必须为十进制正整数。例如 100.00 元传 10000
notifyUrl必填2048 字节本笔订单状态通知地址。生产环境必须使用公网 https,且不允许内网、localhost、链路本地地址;本地联调只有在服务器显式开启测试开关时才允许 HTTP。
subject128 字节订单标题。为空时平台会使用默认标题。
body512 字节订单描述。
param1256 字节商户自定义透传字段,订单状态通知时原样返回。
param2256 字节商户自定义透传字段,订单状态通知时原样返回。
reqTime14 字节请求时间,固定为 14 位数字格式 yyyyMMddHHmmss,参与签名。
signVersion新接入是16 字节固定传 v2;旧接入省略时按 V1。
sign64 字节按第 3 节对应版本生成。

请求示例

curl -X POST 'https://pay.example.com/api/v1/pay/create_order' \
  -H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' \
  --data-urlencode 'mchId=M10001' \
  --data-urlencode 'mchOrderNo=TEST202607090001' \
  --data-urlencode 'productId=0001' \
  --data-urlencode 'amount=10000' \
  --data-urlencode 'notifyUrl=https://merchant.example.com/pay/notify' \
  --data-urlencode 'subject=商品订单' \
  --data-urlencode 'param1=merchant-extra-1' \
  --data-urlencode 'reqTime=20260709213000' \
  --data-urlencode 'signVersion=v2' \
  --data-urlencode 'sign=按V2规则计算后的HMAC-SHA256大写值'

成功响应

{
  "code": 0,
  "message": "ok",
  "data": {
    "order_no": "SF20260709213000A1B2C3D4",
    "merchant_order_no": "TEST202607090001",
    "pay_url": "https://pay.example.com/api/v1/pay/open/随机付款令牌",
    "amount": 10000,
    "status": 0
  },
  "request_id": "..."
}
下单成功返回 code=0status=0 只表示平台已受理,不代表付款成功。商户入账必须以后续通知或查单 status=1 为准。
付款链接和设备信息:pay_url 是平台生成的安全打开链接,请原样交给最终付款用户打开。商户下单和查单都不需要、也不接受设备类型字段。平台仅在付款用户实际打开该链接时,根据浏览器 User-Agent 保守识别 iOS、安卓或电脑;无法可靠识别时保持“未知”,不影响跳转和支付。
超时或非 200 不能换新单号重下:平台可能已经先保存订单,响应 data.order_created=true 时还会返回原 order_nomerchant_order_nostatus 和动作 QUERY_OR_RETRY_SAME_MCH_ORDER_NO。商户必须先查原单,或以完全相同的全部字段和同一个 mchOrderNo 重试;禁止生成新 mchOrderNo,否则原单随后成功时可能造成重复支付。即使网络中断没有拿到响应体,也按同一规则处理。
固定联调产品 0001:全天 24 小时可用,单笔金额为 10.00 至 1000.00 元(即 1000100000 分),不受外部上游通道开关或平台正式业务停单时段影响。商户下单、订单持久化、查单、签名通知和通知重试都走与正式订单相同的线上接口;付款页使用平台内置联调收银台,点击“模拟支付成功”或“模拟支付失败”后,平台会向本笔订单必填的 notifyUrl 真实发送签名通知。0001 不会把正式商户的测试流量发送给外部供应商,也不进入商户余额、供应商余额、对账或经营统计。

5. 查单接口

地址:POST {平台域名}/api/v1/pay/query_order

字段必填最大值说明
mchId64 字节商户号;不得有前导或尾随空白。
mchOrderNo128 字节商户订单号;不得有前导或尾随空白。
reqTime14 字节请求时间,固定为 14 位数字格式 yyyyMMddHHmmss,参与签名。
signVersion新接入是16 字节固定传 v2;旧接入省略时按 V1。
sign64 字节按第 3 节对应版本生成。

成功响应

{
  "code": 0,
  "message": "ok",
  "data": {
    "order_no": "SF20260709213000A1B2C3D4",
    "merchant_order_no": "TEST202607090001",
    "amount": 10000,
    "status": 1,
    "pay_url": "https://pay.example.com/api/v1/pay/open/随机付款令牌"
  },
  "request_id": "..."
}

6. 订单状态通知

订单进入终态后,平台会从已公布的公网 IPv4 出口向商户下单时传入的 notifyUrl 发送 UTF-8 POST Form 通知:交易成功为 status=1,交易失败为 status=2;如果订单之后出现可信的上游成功结果或成功订单被交易冲正,平台会再次发送对应的状态更正通知。进行中状态不会发送终态通知。Content-Type 为 application/x-www-form-urlencoded; charset=UTF-8。商户收到每次通知后都必须按第 3 节对解码后的字段验签、校验金额和订单归属,并按订单号幂等更新状态;处理成功后响应纯文本 success

平台设有全局等待时限,默认 60 分钟(实际时长以平台运营配置为准)。超过时限仍未取得上游明确结果时,平台会按超时失败发送 status=2,避免订单无限等待。该状态表示“平台等待时限已到”,不代表上游绝不可能晚到成功;平台仍会核验原上游。默认安全策略下,后续经认证的成功结果先进入管理员二次复核,只有管理员同意后平台才把原订单更正为 status=1 并再次通知;管理员拒绝时订单继续保持失败。商户必须允许同一订单在平台复核通过后从 2 更正为 1,同时保证成功入账只执行一次。
字段说明
mchId商户号。
payOrderId平台订单号。
mchOrderNo商户订单号。
amount订单金额,单位为分。
status1 表示交易成功;2 表示交易失败;3 表示该成功状态已被冲正,是必须处理的更正通知。状态含义见第 7 节。
param1下单时传入的自定义字段,可能为空。
param2下单时传入的自定义字段,可能为空。
reqTime通知时间,格式 yyyyMMddHHmmss
signVersion首次下单为 V2 时固定返回 v2;历史 V1 订单不返回此字段。
sign平台按该订单首次下单时的签名版本和商户密钥生成。

通知示例

mchId=M10001
payOrderId=SF20260709213000A1B2C3D4
mchOrderNo=TEST202607090001
amount=10000
status=1
param1=merchant-extra-1
param2=
reqTime=20260709213120
signVersion=v2
sign=平台签名

同一订单发生冲正时,平台会再次发送上述字段,其中 status=3,并使用新的 reqTime 和对应签名。商户不得因为该订单曾处理过 status=1 而忽略这次更正。

商户处理要求

平台会分别记录当前目标状态的通知结果。无论 status=1status=2 还是 status=3,商户未返回 success 时平台都会自动重试;短期重试耗尽后会标记“通知失败”用于告警,但不会永久停止,会继续进行最长每日一次的长尾重试。重启或队列短暂故障后也会从数据库恢复,后台同时支持人工补发通知。

交易状态与通知状态

交易状态表示订单本身的处理结果;通知状态只表示平台向商户 notifyUrl 投递该结果后,是否已收到符合协议的确认响应。二者相互独立,通知失败不会把交易成功改成交易失败。

后台通知状态含义
等待通知订单尚未进入成功、失败或冲正终态,平台正在等待可通知的最终结果。
通知成功订单已进入终态,并且商户接口已返回 HTTP 2xx 和纯文本 success
通知失败订单已进入终态,但平台尚未收到商户 notifyUrl 返回的符合协议确认响应;平台会按重试策略继续投递。该状态不改变订单交易结果。
成功和失败都必须接收:交易成功 status=1 与交易失败 status=2 都会生成商户回调;商户不能只处理成功通知,必须验签后按状态幂等更新本地订单。

超时与自动重试

每次通知 HTTP 请求最长等待 10 秒。首次通知立即发起;只有收到 HTTP 2xx 且响应体去除首尾空白后不区分大小写等于 success 才视为成功。请固定返回小写纯文本 success。网络错误、超时、非 2xx 或其他响应体均会重试。

发送次数计划时间说明
1订单进入成功、失败或冲正后立即当前状态的首次通知。
2第 1 次失败后约 2 秒自动重试。
3-9随后约 5、10、20、30、60、120、180 秒短期自动重试;第 9 次仍失败时进入告警状态。
长尾随后约 5 分钟、30 分钟、1、3、6、12、24 小时自动恢复重试,不需要人工重新开启。
持续失败每 24 小时按封顶节奏永久继续,直到商户返回 success;运营仍可人工立即补发。

7. 状态与错误

订单状态

status含义商户处理建议
0待支付不要入账,等待通知或继续查单。
1交易成功验签、校验金额后入账。
2交易失败不要入账,可提示用户重新发起支付。若属于平台等待超时,平台管理员复核并同意已认证的迟到成功后,仍可能收到同单状态 1 更正通知,必须验签并幂等更新。
3交易冲正平台会发送可靠的更正通知。商户侧如已按状态 1 入账,验签并幂等校验后必须冲账或扣回,并将订单最终状态更新为冲正。

通用响应结构

{
  "code": 0,
  "message": "ok",
  "data": {},
  "request_id": "..."
}
HTTPcode常见含义
40040000参数错误、重复字段、金额不合法、签名版本不支持或 reqTime 超出允许范围。
40140100签名验证失败。
40340300请求 IP 不在白名单、商户停用、产品不可用或未绑定产品费率。
40440400订单不存在。
40940900商户订单号重复,但本次任一不可变下单字段与原订单不一致。
41340000请求体超过 1 MiB;包括没有 Content-Length 的 chunked 请求。
50250200渠道处理异常;如果 data.order_created=true,必须查询或用同一商户单号和完全相同字段重试。
50350200当前金额没有可用通道;如果 data.order_created=true,原订单已经保存,禁止换新单号。
50050000系统繁忙;若响应含 data.order_created=true 或客户端未拿到完整响应,先查询原商户单号,禁止直接换新单号。

8. 上线检查

商户侧必须完成
  • 商户号和密钥只保存在商户服务端,不放到任何公开页面、客户端代码或日志中。
  • 新接入下单、查单都带 signVersion=v2reqTimesign,并按解码后的 UTF-8 字段签名。
  • 金额按分传整数,不传元、不传小数。
  • 提交前校验字段 UTF-8 字节长度、控制字符和标识字段首尾空白。
  • 下单超时、网络中断或非 200 时先查原单,只能用同一 mchOrderNo 和完全相同字段重试。
  • 通知接口先验签,再校验金额,最后幂等入账。
  • 通知处理成功只返回 success
联调前确认
  • 商户调用服务器公网 IP 已提供给平台加入 API 白名单。
  • 正式产品编码已绑定给该商户且状态启用;固定联调产品 0001 由系统自动绑定、费率固定为0,使用平台内置联调收银台并真实签名通知商户,但不发送外部供应商、不进入平台资金账;内部编码 0002 仅供平台固定测试商户验证真实上游。
  • 商户 notifyUrl 是公网可访问地址。
  • 商户服务器已放行平台提供的回调 IP。
  • 服务器时间已同步,和北京时间误差不超过 5 分钟。