# HissPay 商户开发文档 版本:v1.13.0 · 更新:2026-10-27。本文面向接入 HissPay 的下游商户,依据当前生产接口源码和产品配置编写;各版本改动见第 17 节「变更记录」。示例中的应用、订单和账户均为占位值。产品配置可能变化;下单前以商户实际签约及平台通知为准。 ## 1. 开始接入 生产网关:`https://transaction.hisspay.com`;沙箱网关:`https://sandbox-api.hisspay.com`。使用前请登录商户后台(mch.hisspay.com)→「应用密钥」核对环境和应用ID(appId),生产与沙箱凭据不得混用。下文通用示例以 `{GATEWAY}` 代替。 所有接口为 HTTPS POST,使用 `Content-Type: application/json`。交易请求由商户服务端发起,不要把密钥放在网页、App 安装包或公开仓库中。 接入前向平台确认: 1. 商户已审核并启用;取得商户号。 2. 创建并启用 API 应用,取得该应用的 `appId` 和 `appSecret`。商户号不能替代 appId。 3. 开通交易币种和账户,分别签约代收、代付产品,取得对应产品编码。 4. 平台已配置可用交易路由、金额范围、费率和结算周期。应用可用不代表所有产品可交易。 5. 代付申请需配置商户服务器的公网出口 IP 白名单;多出口机器应提供完整清单。回调来源 IP 另向平台确认,不能用上游服务商 IP 替代平台通知来源。 6. 提供公网 HTTPS 回调地址;准备正确验签、事务幂等和订单核对逻辑。 示例使用 INR。其他币种和支付方式以商户实际开通结果为准,不能仅改变 `currency` 就跨币种交易。名称包含 test 的商户或应用不等于沙箱;生产请求涉及真实业务。 ### 当前币种及产品目录(2026-10-27 生产快照) 产品在目录中出现,**不等于该商户可交易**。服务端还会检查应用、商户币种、有效协议、金额及可用路由/渠道。下表由产品清单自动生成,状态含义如下: - `LIVE`:可用。仍须商户签约、开通币种并有可用路由才能交易;不保证任一商户都已开通,也不保证实际交易成功。 - `PLANNED`:即将开放,**不可下单**。 - `BLOCKED`:暂不可用,**不可下单**。 「说明」列出该产品在通用字段之外对下单请求的要求,具体字段见第 6 节「按支付方式的下单要求」。标为 PLANNED 或 BLOCKED 的产品,开放时间以平台通知为准。 共 30 个目录产品:LIVE 7 个,PLANNED 13 个,BLOCKED 10 个。 | 状态 | 币种 | 方向 | 产品编码 | 名称 | 支付方式 | 说明 | |---|---|---|---|---|---|---| | LIVE(可用) | AUD | 代收 | `AUD-PAYPLUS-APPLE-M85` | 澳元 Apple Pay(AUD Apple Pay) | 钱包 | 已向部分商户开放;此编码仅适用于实际签约该产品的应用。请以后台签约产品为准,其他商户不要直接复用。 | | LIVE(可用) | AUD | 代收 | `AUD-PAYPLUS-GOOGLE-M85` | 澳元 Google Pay(AUD Google Pay) | 钱包 | 已向部分商户开放;此编码仅适用于实际签约该产品的应用。请以后台签约产品为准,其他商户不要直接复用。 | | LIVE(可用) | AUD | 代收 | `AUD-VKDPWMRT` | AU-PAYIN(Australia AUD pay-in) | 银行卡 | 澳洲银行卡托管支付:须传 payType=CARD、clientIp(真实付款人 IP,键名区分大小写)、cardExtra(JSON 字符串,格式见第 6 节「澳洲银行卡(AUD-VKDPWMRT)」)和 https 的 returnUrl;金额 1–500 AUD,整数或 .99(如 20、20.00、19.99);仅支持澳洲手机号与澳洲账单信息;返回支付链接 payUrl,结果以回调或查单为准。 | | LIVE(可用) | INR | 代收 | `INR-HBGMMNKI` | 印度卢比-收款T0(India INR pay-in (T+0)) | 未细分 | — | | LIVE(可用) | INR | 代付 | `INR-NPLMSUEX` | 印度卢比-默认代付(India INR default payout) | 未细分 | — | | LIVE(可用) | THB | 代收 | `THB-PROMPTPAY-PAYIN` | 泰铢-PromptPay 扫码代收(Thailand THB PromptPay QR pay-in) | 未细分 | 按商户逐个开通,以开通通知为准。不传 payType;cardExtra 须为含 payerAccountNo、payerBankCode 的 JSON 字符串;默认单笔 500.00–49999.00 THB;返回收银台 payUrl,结果以回调或查单为准(见第 16 节)。 | | LIVE(可用) | USD | 代收 | `USD-EKCBBONA` | 美元-国际卡收款(USD international card pay-in) | 银行卡 | 银行卡托管收银台:下单返回支付链接 payUrl,引导付款人跳转付款;结果以回调或查单为准。 | | PLANNED(即将开放,不可下单) | EUR | 代收 | `EUR-CARD-PAYIN` | 欧元-银行卡代收(托管收银台,需购物车/购物者信息)(EUR card pay-in (hosted checkout, cart & shopper required)) | 银行卡 | 银行卡代收须在下单请求中提供 cardExtra(购物车和购物者信息,见第 6 节「按支付方式的下单要求」);金额两位小数;返回支付链接 payUrl,结果以回调或查单为准。 | | PLANNED(即将开放,不可下单) | GBP | 代收 | `GBP-CARD-PAYIN` | 英镑-银行卡代收(托管收银台,需购物车/购物者信息)(GBP card pay-in (hosted checkout, cart & shopper required)) | 银行卡 | 银行卡代收须在下单请求中提供 cardExtra(购物车和购物者信息,见第 6 节「按支付方式的下单要求」);金额两位小数;返回支付链接 payUrl,结果以回调或查单为准。 | | PLANNED(即将开放,不可下单) | JPY | 代收 | `JPY-CARD-PAYIN` | 日元-银行卡代收(托管收银台,需购物车/购物者信息)(JPY card pay-in (hosted checkout, cart & shopper required)) | 银行卡 | 银行卡代收须在下单请求中提供 cardExtra(购物车和购物者信息,见第 6 节「按支付方式的下单要求」);JPY 金额须为整数(否则 10006);返回支付链接 payUrl,结果以回调或查单为准。 | | PLANNED(即将开放,不可下单) | THB | 代付 | `THB-BANK-PAYOUT` | 泰铢-银行转账代付(Thailand THB bank transfer payout) | 银行转账 | 需单独开通,收到开通通知(含可用银行、户名长度上限、单笔范围与费率)前不可下单;bizCode 固定 BANK(见第 16 节)。 | | PLANNED(即将开放,不可下单) | USD | 代收 | `USD-APPLEPAY-PAYIN` | 美元-Apple Pay 托管代收(USD Apple Pay hosted pay-in) | 钱包 | Apple Pay 托管页,仅 USD:须提供付款人公网 IPv4(clientIp);单笔限额待确认;返回支付链接 payUrl,结果以回调或查单为准。 | | PLANNED(即将开放,不可下单) | USD | 代收 | `USD-CARD-PAYIN` | 美元-银行卡代收(托管收银台,需购物车/购物者信息)(USD card pay-in (hosted checkout, cart & shopper required)) | 银行卡 | 银行卡代收须在下单请求中提供 cardExtra(购物车和购物者信息,见第 6 节「按支付方式的下单要求」);金额两位小数;返回支付链接 payUrl,结果以回调或查单为准。 | | PLANNED(即将开放,不可下单) | USD | 代收 | `USD-CASHAPP-PAYIN` | 美元-Cash App 代收(USD Cash App pay-in) | 钱包 | Cash App 代收,仅 USD,单笔 1–99:须提供付款人公网 IPv4(clientIp);返回支付链接 payUrl;实付可能比订单金额少 0.01–0.20,按通知中的 realAmount 入账。 | | PLANNED(即将开放,不可下单) | USD | 代收 | `USD-HOSTED-PAYIN` | 美元-托管收银台代收(USD hosted checkout pay-in) | 托管收银台 | 托管收银台:须提供格式有效的付款人 email;返回支付链接 payUrl(有有效期),结果以回调或查单为准;不能指定只收银行卡。 | | PLANNED(即将开放,不可下单) | USD | 代付 | `USD-LOCAL-PAYOUT` | 美国-代付(待配置)(USD payout (draft)) | 钱包 | Cash App 代付,仅 USD:收款人 Cash App 账户名填入 account(字段映射待确认;格式规则待确认:1–64 位字母或数字,不能是纯数字,不含 $ 或 @),accountName 必填;处理可能长达约 1 小时,期间为处理中,不是失败。 | | PLANNED(即将开放,不可下单) | USDT | 代收 | `USDT-BSC` | USDT-BSC 网络代收(USDT pay-in on BSC) | 加密资产 | USDT 代收,金额最多 4 位小数;下单返回 payUrl 与收款信息 paymentInfo;结果以回调或查单为准,链上确认后查单返回 txHash。 | | PLANNED(即将开放,不可下单) | USDT | 代收 | `USDT-ETHEREUM` | USDT-Ethereum 网络代收(USDT pay-in on Ethereum) | 加密资产 | USDT 代收,金额最多 4 位小数;下单返回 payUrl 与收款信息 paymentInfo;结果以回调或查单为准,链上确认后查单返回 txHash。 | | PLANNED(即将开放,不可下单) | USDT | 代收 | `USDT-SOLANA` | USDT-Solana 网络代收(USDT pay-in on Solana) | 加密资产 | USDT 代收,金额最多 4 位小数;下单返回 payUrl 与收款信息 paymentInfo;结果以回调或查单为准,链上确认后查单返回 txHash。 | | PLANNED(即将开放,不可下单) | USDT | 代收 | `USDT-TRON` | USDT-Tron 网络代收(USDT pay-in on Tron) | 加密资产 | USDT 代收,金额最多 4 位小数;下单返回 payUrl 与收款信息 paymentInfo;结果以回调或查单为准,链上确认后查单返回 txHash。 | | BLOCKED(暂不可用,不可下单) | AUD | 代收 | `AUD-CARD` | 澳洲-银行卡代收(待开通)(Australia card pay-in (pending)) | 银行卡 | — | | BLOCKED(暂不可用,不可下单) | AUD | 代付 | `AUD-LOCAL-PAYOUT` | 澳大利亚-代付(待配置)(Australia payout (draft)) | 未细分 | — | | BLOCKED(暂不可用,不可下单) | BDT | 代收 | `BDT-LOCAL-PAYIN` | 孟加拉-代收(待配置)(Bangladesh pay-in (draft)) | 未细分 | — | | BLOCKED(暂不可用,不可下单) | BDT | 代付 | `BDT-LOCAL-PAYOUT` | 孟加拉-代付(待配置)(Bangladesh payout (draft)) | 未细分 | — | | BLOCKED(暂不可用,不可下单) | IDR | 代收 | `IDR-LOCAL-PAYIN` | 印度尼西亚-代收(待配置)(Indonesia pay-in (draft)) | 未细分 | — | | BLOCKED(暂不可用,不可下单) | IDR | 代付 | `IDR-LOCAL-PAYOUT` | 印度尼西亚-代付(待配置)(Indonesia payout (draft)) | 未细分 | — | | BLOCKED(暂不可用,不可下单) | PHP | 代收 | `PHP-LOCAL-PAYIN` | 菲律宾-代收(待配置)(Philippines pay-in (draft)) | 未细分 | — | | BLOCKED(暂不可用,不可下单) | PHP | 代付 | `PHP-LOCAL-PAYOUT` | 菲律宾-代付(待配置)(Philippines payout (draft)) | 未细分 | — | | BLOCKED(暂不可用,不可下单) | PKR | 代收 | `PKR-LOCAL-PAYIN` | 巴基斯坦-代收(待配置)(Pakistan pay-in (draft)) | 未细分 | — | | BLOCKED(暂不可用,不可下单) | PKR | 代付 | `PKR-LOCAL-PAYOUT` | 巴基斯坦-代付(待配置)(Pakistan payout (draft)) | 未细分 | — | 生产产品状态 `0` 为正常(对应 LIVE)、`1` 为锁定(本表记为 PLANNED 或 BLOCKED);以上仅供识别目录,不构成交易授权。平台还有更广的支付方式编码(`CARD`、`PAYPAL`、`CASHAPP`、`GOOGLE_PAY` 等,见第 6 节「支付方式编码」),但编码并不代表已经签约或有可用通道。没有明确开通的组合不能交易。现金类产品编码以平台分配为准,目录中暂无现金类产品行。币种余额查询还要求商户有相应账户。 ## 2. 接口清单 | 操作 | 路径 | 说明 | |---|---|---| | 创建代收订单 | `/transaction/payin/v1/apply` | 返回支付页面及平台订单号 | | 查询代收订单 | `/transaction/payin/v1/query` | 查询本应用的订单 | | 创建代付订单 | `/transaction/payout/v1/apply` | 受理成功后异步处理,不代表已到账 | | 查询代付订单 | `/transaction/payout/v1/query` | 查询最终付款状态 | | 查询账户余额 | `/transaction/payout/v1/balance` | 返回账面、可用和冻结余额 | | 创建收款人 | `/transaction/payout/v1/beneficiaries/create` | 仅已开通 USD 银行卡代付的应用,见第 7 节「收款人管理」 | | 查询收款人 | `/transaction/payout/v1/beneficiaries/query` | 同上 | | 更新收款人 | `/transaction/payout/v1/beneficiaries/update` | 同上 | | 沙箱:查看来源 IP | `/transaction/sandbox/v1/network` | **仅沙箱**。返回 `environment` 与平台看到的来源 IP `sourceIp`,用于配置代付白名单(沙箱默认不校验白名单,开启「模拟生产白名单」后才需要) | | 沙箱:查询 / 修改沙箱设置 | `/transaction/sandbox/v1/settings/query`、`/transaction/sandbox/v1/settings/update` | **仅沙箱**。`payoutIpWhitelistMode`:`SANDBOX_OPEN`(默认,不校验代付来源 IP)或 `PRODUCTION`(模拟生产白名单,名单外返回 10010) | | 沙箱:通知投递记录 | `/transaction/sandbox/v1/notify-log/query` | **仅沙箱**。本商户最近的通知投递结果(限速顺延、熔断、拒绝非公网目标等),见[沙箱测试指南](sandbox-guide.html)「回调规则」 | | 沙箱:查询模拟单 | `/transaction/sandbox/v1/query` | **仅沙箱**。查看模拟渠道状态与投递记录,见[沙箱测试指南](sandbox-guide.html) | | 沙箱:手动终态 | `/transaction/sandbox/v1/trigger` | **仅沙箱**。对可手动场景的模拟单触发 `PAID` / `FAILED` | | 沙箱:重发通知 | `/transaction/sandbox/v1/resend` | **仅沙箱**。请平台重发最近终态通知 | 所有接口都使用第 3 节的请求签名。上述沙箱接口只在沙箱网关存在,生产网关返回 404。生产代付始终校验来源 IP 白名单。 代收查单、代付查单、余额查询当前分别按 appId 限制 **每秒 5 次**。建议订单轮询间隔至少 3–5 秒,并在失败时退避。下单 TPS、日额度和路由限额需单独确认;不要把查询限频当作下单容量承诺。 ## 3. 请求签名 ### 必填请求头 | 请求头 | 类型 | 说明 | |---|---|---| | `appId` | string | 应用标识 | | `timestamp` | string | Unix 时间戳,**毫秒**,与平台时间差不得超过 30 秒 | | `nonce` | string | 至少 10 个字符;推荐随机 UUID,每次请求重新生成 | | `sign` | string | SHA-256 小写十六进制,64 位 | 服务器应同步时间。相同 appId 下的 nonce 在防重放窗口(当前 **60 秒**,即时间戳允许误差的两倍)内不能重复:重复使用已成功验签的 nonce 返回业务码 400(msg 形如 `nonce has been used`);两个并发请求同时使用同一 nonce 时,后到者返回 900(`Request repeated`)。重试查询时更换 nonce、timestamp、sign,但保留原订单标识。 签名相关的常见拒绝(均为业务码 400,msg 仅供诊断):缺少请求头(如 `AppId cannot be null`)、nonce 少于 10 个字符(`nonce cannot be null`)、时间戳不是整数或超出 30 秒(`timestamp invalid` / `timestamp has expired`)、请求体不是 JSON 对象或含 `null` 值(`Signed JSON body invalid`)、签名不一致(`Sign invalid`)。签名校验服务暂时不可用时返回 500(`Signature verification temporarily unavailable`),换新 nonce 稍后重试。代付类接口(第 7 节)把这些 msg 统一改写为 `Payout request rejected`,业务码不变;但代付下单**缺少签名请求头或 `Content-Type` 不是 `application/json`** 时返回业务码 500(`Payout request not confirmed; query original order`),请先确认请求头齐全再按第 11 节处理。 ### 计算规则 1. 合并请求体字段与请求头 `appId`、`timestamp`、`nonce`;不要加入 `sign`。 2. 字段名按字典序升序排序,拼成 `key=value`,各项以 `&` 连接。 3. 在拼接结果末尾**直接追加 appSecret**,不加分隔符。 4. 对完整字符串按 UTF-8 编码计算 SHA-256,输出小写十六进制。 ```text sign = lowercase_hex(SHA256(UTF8(sorted_key_value_pairs + appSecret))) ``` 下游签名不是 HMAC,也不需要 RSA;不要套用上游通道文档的签名算法。无需对待签名值做 URL 编码,不把整个 JSON 原文当成待签名串。HTTP 请求中无需传 appSecret。 请求体使用扁平 JSON 对象。推荐字段值使用字符串,金额统一写为 `"100.00"`;省略无值的可选字段:**不能发送 `null`**(任何字段为 `null` 都会被拒绝,业务码 400),也不要发送数组或嵌套对象。若发送空字符串,应将该字段的 `key=` 纳入签名。不要签名后再更改金额格式或字段值。 例如,以下为余额查询的固定签名演示,密钥完全虚构: ```text body: {"currency":"INR"} appId: demo-app timestamp: 1700000000000 nonce: demo-nonce-0001 appSecret: demo-secret 待签名串: appId=demo-app¤cy=INR&nonce=demo-nonce-0001×tamp=1700000000000demo-secret ``` 此时间戳只用于核对算法,不能直接请求生产接口。可下载[固定测试向量](signature-vectors.json)、[Python 示例](examples/client.py)和 [Node.js 示例](examples/client.mjs)。两个示例默认只做只读余额查询,从服务器环境变量读取网关 `HISSPAY_GATEWAY` 与凭据 `HISSPAY_APP_ID`、`HISSPAY_APP_SECRET`;旧变量名 `PAYPLAT_APP_ID`、`PAYPLAT_APP_SECRET` 暂时仍可使用,但会提示已弃用,请改名。 ## 4. 金额与订单号 - 下游接口金额单位是**币种主单位**。100 INR 传 `"100.00"`,不要传上游 paise 值 `10000`。 - 使用十进制定点计算,避免二进制浮点金额误差。精度按币种: - 一般法币(INR、USD、AUD、THB 等)最多两位小数; - 零小数位币种(JPY、KRW、VND、CLP、UGX 等)必须为整数,否则返回 10006; - 三位小数币种(BHD、JOD、KWD、OMR、TND)**当前不支持**,下单返回 10006; - USDT 最多 **4 位小数**(四个网络一致,见第 6 节「USDT 代收」)。 - 超出精度的金额会被拒绝(10006);实际通道可要求整数金额或更严格的范围。 - 代收金额必须大于 0。 - 代付金额必须大于 0,且不低于代付最低金额。**通道下限**由平台为代付通道配置;未配置时为该币种默认值:AUD、USD 为 **1.00**;INR、PKR、BDT 为 **100.00**;其他币种为 **100.00**(平台可为通道设置其他值,可高于或低于默认值)。**签约下限**为商户签约中配置的最低金额,未配置时不额外限制。实际下限取通道下限与签约下限中的**较高者**(签约下限低于通道下限时按通道下限),并仍需满足签约路由的上下限。低于下限返回 `10006`;代付接口当前把 msg 统一改写为 `Payout request rejected`(见第 11 节),不会显示具体下限,请以签约与开通通知中的金额范围自查。 - 银行账户、商户号、appId、平台订单号均作为字符串处理,保留前导零。 - `merchantOrderNo` 由商户生成;建议使用商户前缀+时间+随机串,控制在 64 个 ASCII 字符以内,使用字母、数字、`_`、`-`。接口上限为 **255 个字符**,超过会被拒绝(业务码 400;代收 msg 形如「请求参数不正确:merchantOrderNo too long」,代付 msg 统一为 `Invalid payout request`,见第 11 节)。代收、代付上限相同。 - `product` 为平台分配的产品编码,不要自行拼接或改写。代收下单最长 **64 个字符**,代付下单最长 **20 个字符**;超长返回业务码 400(`product too long`)。 - 每笔业务使用唯一订单号。同一应用下,用已存在的 `merchantOrderNo` 再次下单,**无论参数是否相同**,通常返回 `10020`(商户订单号重复),不会返回原受理结果。平台可为个别产品单独启用业务幂等(目前仅可能用于 USD 银行卡代付):启用后同一单号、同一参数返回原受理结果,参数改变返回 `409`。是否启用以开通通知为准;未说明时按 10020 处理。 - 无论是否启用幂等,下单超时或结果不确定时都先用原单号查单,不要换单号重试未知付款,也不要把 10020 当作下单失败。 ### 实际到账金额规则 - `amount` 始终为创建订单时的原订单金额,用于核对订单,不能被回调改写。 - `realAmount` 为平台核验后的实际收款总额(主币种单位、扣手续费前),是代收成功后下游入账的金额依据。它可能小于或大于 `amount`,尤其是孟加拉实际付款金额通道。商户账户结算净额另按签约费率及固定费用计算,不能把 `realAmount` 当作扣费后的可用余额。 - **不要要求 `realAmount == amount` 才接受成功通知,也不要收到成功就按 `amount` 入账。** 例如 BDT 原订单 100.00、实际收到 95.00,则按 95.00 记实际收款;实际收到 105.00,则按 105.00 记实际收款。商品交付、少付补款或多付处理由商户业务规则决定,不能伪造平台收款金额。 - 仅在成功状态、验签及订单归属核验通过、`realAmount` 为有效正数时执行幂等入账。未成功、金额缺失/非法、同一订单重复通知却金额不同、查询与通知矛盾时,保留待核对状态;**不能回退使用原订单金额补记账**。 - 平台仅对已完成实际金额验证的渠道自动处理差额;其他金额异常进入待核对。本文不表示尚未开通的国家或渠道已经可交易。 ## 5. 通用响应 ```json {"code":0,"msg":"Success","data":{}} ``` 同时检查 HTTP 状态和 JSON `code`。HTTP 200 也可能包含业务错误。`code=0` 表示接口操作成功;是否支付成功要看订单状态。`msg` 供诊断展示,不作为稳定的程序判断条件。金额在响应中是 JSON 数字,小数位数不固定(可能是 `100`、`100.0` 或 `100.0000`,平台账务按 4 位小数存储),商户应使用支持 Decimal 的解析方式并按数值比较,不要按字符串比较。尚未成功的代收订单查单时 `realAmount` 可能为 `0` 或 `0.0`,不是实际收款。 接口响应通过 HTTPS 返回;当前交易接口没有统一的响应签名字段。**异步回调另有签名,必须验签。** ## 6. 代收下单 `POST /transaction/payin/v1/apply` | 字段 | 必填 | 说明 | |---|---|---| | `merchantOrderNo` | 是 | 商户唯一订单号,最长 255 个字符 | | `amount` | 是 | 主币种金额,如 `"100.00"`;法币最多两位小数,零小数位币种(如 JPY)须为整数,USDT 最多 4 位小数(如 `"12.3456"`);不符合返回 10006 | | `currency` | 是 | 如 `INR`,须已开通;USDT 代收固定为 `USDT`。四字母代码只支持 `USDT`,其他返回 10006 | | `product` | 是 | 平台分配、该商户已签约的代收产品编码,最长 64 个字符 | | `name` | 是 | 付款人姓名;具体格式受支付通道要求约束。AUD-VKDPWMRT 仍须填写但不使用,名/姓以 `cardExtra` 为准(见下文「澳洲银行卡(AUD-VKDPWMRT)」) | | `phone` | 是 | 付款人手机号;按通道要求填写,不能留空。AUD-VKDPWMRT 只能是 5–15 位数字,不带 `+`、空格、横线和国家码(见下文「澳洲银行卡(AUD-VKDPWMRT)」) | | `email` | 是 | 付款人邮箱,不能留空 | | `userId` | 是 | 商户侧付款用户的唯一标识,字符串,最长 32 个字符;同一付款人保持不变,参与签名。超过 32 个字符返回业务码 400 | | `clientIp` | 按渠道要求 | 真实付款人的公网 IPv4/IPv6 地址;部分渠道(包括印度原生 UPI)必填,按签约产品要求传入。值非空白(不是空串或全空白)时必须是合法的 IPv4 或 IPv6 文本,否则返回 10021(超过 45 个字符也返回 10021);空串或全空白在这一步按未传处理。此字段必须参与签名,不要使用商户服务器 IP 代替。键名区分大小写,必须写作 `clientIp`。AUD-VKDPWMRT 必填,见下文「澳洲银行卡(AUD-VKDPWMRT)」 | | `notifyUrl` | 否 | 本单公网 HTTPS 通知地址;未填时使用商户默认通知地址。非空时(空字符串视为未填)必须是合法的 http(s) URL,否则返回业务码 400 | | `returnUrl` | 否 | 支付后浏览器跳转地址;非空时(空字符串视为未填)必须是合法的 http(s) URL,否则返回业务码 400;部分通道要求必填的 HTTPS 地址。AUD-VKDPWMRT 必填 HTTPS,见下文「澳洲银行卡(AUD-VKDPWMRT)」 | | `addr` | 否 | 付款人地址,视通道要求。AUD-VKDPWMRT 不使用 | | `payType` | 否 | 支付方式编码,见下文「支付方式编码」表;显式传入时须与币种匹配,否则返回 10012;不传时按产品路由。不使用时请省略字段:传空字符串也返回 10012。编码不代表已开放。AUD-VKDPWMRT 必须显式传 `CARD`,见下文「澳洲银行卡(AUD-VKDPWMRT)」 | | `otherExtend` | 否 | 扩展字符串;当前下单响应可返回,不保证查单/回调携带 | | `cardExtra` | 按产品要求 | 通道扩展信息,字符串。AUD-VKDPWMRT 必填,格式见下文「澳洲银行卡(AUD-VKDPWMRT)」。部分即将开放的银行卡代收产品也须提交购物车和购物者信息,见下文「银行卡代收(需提交购物车和购物者信息)」;这些产品使用前向平台确认格式 | ```json { "merchantOrderNo":"DEMO_IN_20260912_001", "amount":"100.00", "currency":"INR", "product":"INR-HBGMMNKI", "name":"Test User", "phone":"YOUR_TEST_PHONE", "email":"payer@example.com", "notifyUrl":"https://merchant.example.com/payments/notify", "returnUrl":"https://merchant.example.com/payments/result" } ``` 示例响应: ```json {"code":0,"msg":"Success","data":{"appId":"YOUR_APP_ID","merchantOrderNo":"DEMO_IN_20260912_001","orderNo":"PLATFORM_ORDER_NO","currency":"INR","product":"INR-HBGMMNKI","amount":100.00,"payUrl":"https://cashier.example.com/example","payStatus":0,"otherExtend":null}} ``` 响应中的 `amount` 为 JSON 数字,请用 Decimal 解析;所有代收下单响应都返回 `product`。法币订单不返回 `paymentInfo` 键(仅 USDT 产品返回)。 记录平台 `orderNo`,再让付款人访问 `payUrl`。浏览器跳转到 returnUrl 不能作为到账凭证;以验签通过的回调或查单结果确认付款。 ### 支付方式与实际开通 `payType` 是可选字段;不传时由已签约产品及路由决定。显式传入时平台只校验编码与币种的组合(不匹配返回 10012),不按 `payType` 选择通道,也不证明相应通道已在生产开通。当前已配置产品见上表;任何支付方式均须向平台确认适用产品、商户协议和通道要求。`returnUrl` 推荐公网 HTTPS,非空时须为合法 http(s) URL,不强制 HTTPS;部分上游可要求其必填。不要向本接口提交卡号或 CVV。例外:AUD-VKDPWMRT 必须显式传 `payType=CARD`,且 `returnUrl` 必填并必须为 HTTPS(见下文「澳洲银行卡(AUD-VKDPWMRT)」)。 ### 支付方式编码 | `payType` | 支付方式 | 可用币种 | 说明 | |---|---|---|---| | `CARD` | 银行卡 | 24 个币种(含 USD、EUR、GBP、JPY、AUD、INR 等) | 卡支付/托管收银台 | | `PAYPAL` | PayPal | 20 个币种 | PayPal 钱包 | | `CASHAPP` | Cash App Pay | USD | Cash App 钱包支付;不是现金交付 | | `GOOGLE_PAY` | Google Pay(GPay) | USD、EUR、GBP、JPY、AUD | Google Pay 钱包支付;不设独立产品编码,配合银行卡、托管收银台或钱包类产品使用 | | `APPLE_PAY` | Apple Pay | USD、AUD | Apple Pay 托管页;AUD 见第 15 节(沙箱暂未同步 AUD + `APPLE_PAY`,当前返回 10012) | | `CASH` | 现金 | USD、AUD、EUR、GBP、INR、BDT、PKR、PHP、IDR | 现金类代收;现金类产品编码以平台分配为准 | | `HOSTED` | 托管收银台 | USD、AUD、EUR、GBP | 托管支付页,由付款人在页面选择方式 | | `CRYPTO` | 加密资产 | USDT | USDT 代收,见下文「USDT 代收」 | | `WALLET` | 钱包 | INR | 印度钱包/UPI | | `JAZZCASH`、`EASYPAISA` | 钱包 | PKR | 巴基斯坦钱包 | | `BKASH`、`NAGAD` | 钱包 | BDT | 孟加拉钱包 | 币种列是编码级校验范围,不是商户开通范围;不在范围内的组合返回 10012。具体某个产品能否使用某编码,以平台开通结果为准。商户只传标准 `payType`;上游通道字段由平台映射。 ### 按支付方式的下单要求 平台将陆续开放银行卡、钱包和托管收银台等支付方式。本节说明这些支付方式对**本接口**下单请求的额外要求。对应产品在上方目录中标为 `LIVE` 之前不可下单。 - 商户只对接 HissPay 接口,只传本文列出的字段。上游所需的参数由平台配置,商户无需也不能传入。 - 标为「待确认」的内容尚未定稿。请勿自行猜测格式,接入前向平台索取该产品的字段说明。 - 请求签名、nonce 和通用字段规则不变(第 3、4 节)。新增要求只通过已有字段表达,不需要新的请求头。 #### 通用要求 - 币种与金额:订单币种必须是该产品的币种。金额为主币种单位,最多两位小数;平台按支付方式换算金额时不做舍入,超出精度的金额会被拒绝(10006)。JPY 等零小数位币种的请求金额必须为整数,否则返回 10006。 - 付款人信息:`name`、`email`、`phone` 请填写付款人真实信息,不要使用占位值。部分支付方式会在平台侧校验邮箱格式、付款人 IP 等;不合格的请求会被拒绝,具体响应形式待确认。AUD-VKDPWMRT 的情况已经确定:请求先过通用校验,不合规时返回各自原有的业务码(400、10006、10012、10021 等);通过通用校验后,在本产品校验中不合规的,当前返回 9999(见下文「澳洲银行卡(AUD-VKDPWMRT)」与第 11 节「9999 自查清单」)。无法判断结果时,按下文「下单结果不确定时」处理。 - `returnUrl`:使用公网 HTTPS。部分托管收银台产品付款后不会跳回 `returnUrl`。无论是否跳回,都不能据此判断付款结果。 - 不要向本接口提交卡号、CVV 或钱包凭据。这些支付方式都由托管支付页收集支付信息。 - 付款结果只以验签通过的平台回调或查单为准(第 8、10 节)。部分通道会在付款明确失败时发送失败通知(`status="2"`,见第 10 节「代收失败通知」);其他通道的失败、放弃或过期订单不发送通知,可能长期保持 `payStatus=0`,需要联系平台核对。 #### 银行卡代收(需提交购物车和购物者信息) AUD-VKDPWMRT 不适用本小节,见下文「澳洲银行卡(AUD-VKDPWMRT)」。 部分银行卡代收产品要求商户在 `cardExtra` 中提交购物车和购物者信息,产品目录的「说明」列会注明;目录中暂无需要 `cardExtra` 的钱包产品。`cardExtra` 缺失或信息不全时,下单会被拒绝。 `cardExtra` 是字符串字段,与其他字段一样原样参与签名;请求体仍须是扁平 JSON,不要把 `cardExtra` 写成 JSON 对象或数组。**`cardExtra` 内部的键名和编码格式待平台确认**,下表只列需要提供的信息项和示例值: | 信息项 | 是否必填 | 示例值 | 说明 | |---|---|---|---| | 购物车商品列表 | 是 | 编号 SKU-1,名称 Demo item,单价 25.00,数量 1 | 至少一项;每项包含商品编号、名称、单价、数量,单价按主币种两位小数。各项合计是否必须等于 `amount`,待平台确认 | | 购物者名(first name) | 是 | Test | 与姓分开提供 | | 购物者姓(last name) | 是 | User | 与名分开提供 | | 付款人公网 IP | 是 | 203.0.113.10 | 真实付款人 IP。由顶层 `clientIp` 提供还是须写入 `cardExtra`,待平台确认 | | 邮箱 | 是 | payer@example.com | 是否须在 `cardExtra` 中重复顶层 `email`,待平台确认 | | 电话 | 按产品要求 | YOUR_TEST_PHONE | 产品要求账单信息时必填(见产品目录「说明」) | | 账单地址、城市、邮编 | 按产品要求 | Example Street 1,Example City,00000 | 同上 | | 国家 | 按产品要求 | US | 同上;取值格式(是否为两位国家代码)待确认 | | 州或省 | 按产品要求 | Example State | 同上 | | 收货地址 | 否 | — | 可选 | | 付款人浏览器 User-Agent、语言 | 否 | Mozilla/5.0 …,en-US | 可选 | | 商户侧客户编号 | 否 | CUST0001 | 可选 | 下单请求示例(`cardExtra` 的值是占位符,实际内容按平台提供的格式填写): ```json { "merchantOrderNo":"DEMO_CARD_20260930_001", "amount":"25.00", "currency":"USD", "product":"YOUR_CARD_PAYIN_PRODUCT", "name":"Test User", "phone":"YOUR_TEST_PHONE", "email":"payer@example.com", "clientIp":"203.0.113.10", "notifyUrl":"https://merchant.example.com/payments/notify", "returnUrl":"https://merchant.example.com/payments/result", "cardExtra":"CART_AND_SHOPPER_INFO_IN_PLATFORM_FORMAT" } ``` - 成功时响应与普通代收相同。`payUrl` 是托管支付页的 HTTPS 地址,可能不在 HissPay 域名下;请直接引导付款人打开,不要按域名白名单拦截或改写链接。之后按下文「托管收银台」的方式等待结果。 - 支付方式由所签约的产品决定,商户无需传入其他参数;可用币种以商户实际开通为准。 #### 澳洲银行卡(AUD-VKDPWMRT) AUD-VKDPWMRT 是澳元银行卡托管支付:下单成功返回托管支付页 `payUrl`,付款人在支付页填写卡信息,商户不接触、也不得提交卡号或 CVV。除通用字段规则(第 3、4 节及上方字段表)外,下列要求须**全部满足**。错误码按校验顺序返回:请求先经过通用校验,不合规时返回原有业务码,例如必填字段为空、URL 非法返回 400,金额 ≤0 或精度超限返回 10006,`payType` 与币种不匹配或为空字符串返回 10012,`clientIp` 非空白但格式非法返回 10021,产品、协议、路由问题返回 10004、10005 等;**只有通过这些通用校验之后**,在本产品自身的校验中不满足下列要求时,当前才返回业务码 9999,平台正在改为具体业务码。收到 9999 时按第 11 节「9999 自查清单」处理。 **当前仅支持澳洲手机号(area_code 固定 61)与澳洲账单信息(country=AU、state 取下表所列、4 位邮编);海外手机号/海外账单地址暂不支持(平台正在向通道确认)。** ##### 顶层字段 | 字段 | 本产品要求 | |---|---| | `payType` | 必填,必须显式传 `CARD`。传空字符串或与 AUD 不匹配的编码返回 10012;不传时平台按 `WALLET` 记录订单,或传了其他与 AUD 匹配的编码(如 `PAYPAL`),都会被本产品拒绝(当前 9999) | | `amount` | AUD 1–500(含两端),只能是整数或 .99 结尾,最多两位小数表示:`"20"`、`"20.00"`、`"19.99"`、`"500"` 合规;`"20.50"`、`"0.99"`、`"500.99"` 不合规。金额 ≤0 或超过两位小数(如 `"19.999"`)返回 10006;超出签约路由金额范围按路由规则返回(如 10005);其余不合规当前返回 9999 | | `clientIp` | 必填。键名必须精确为 `clientIp`(区分大小写);写成 `client_ip`、`clientIP`、`ip` 等能通过验签,但平台不读取该键(未知字段被忽略),等同未传,下单被拒。值为真实付款人 IP 的字面量:IPv4 点分形式(如 `203.0.113.10`)或 IPv6;不能是域名,不能带端口或方括号,IPv4 不要写成 `::ffff:203.0.113.10` 这类映射形式。不要传商户服务器 IP;不要放在请求头或 URL 查询参数里(同样不会被读取)。值非空白但不是合法 IPv4/IPv6(含超过 45 个字符)返回 10021;不传、`null`、空字符串、全空白或键名错误都视为缺失,本产品当前返回 9999;能通过通用格式检查、但不是本产品接受的字面量(如 `::ffff:` 映射形式)当前也返回 9999 | | `returnUrl` | 必填。必须以 `https://` 开头;主机名须含点(如 `merchant.example.com`),不能是 `localhost`;不含用户信息(如 `user@`);端口省略或为 443;总长不超过 512 个字符。非空且不是合法 http(s) URL 返回 400;不传、空字符串,或是合法 URL 但不满足上述要求(如 `http://`、`localhost`、端口 8443)当前返回 9999 | | `phone` | 必填,只能是 5–15 位数字:不带 `+`、空格、横线或括号,也不要加国家码 61(国家码由 `cardExtra` 中 `account_info.area_code` 提供)。为空返回 400;非空但不符合上述格式当前返回 9999 | | `email` | 必填,形如 `payer@example.com`:不含空白,只有一个 `@`,`@` 之后含点;不超过 254 个字符。为空返回 400;非空但不符合上述格式当前返回 9999 | | `name` | 仍必填(不能为空),但本产品不使用;付款人名、姓以 `cardExtra` 中的 `first_name`、`last_name` 为准 | | `addr` | 本产品不使用,可省略 | | `cardExtra` | 必填,JSON 字符串,格式见下 | ##### cardExtra 格式 `cardExtra` 的值是一段**字符串**:先把下面描述的 JSON 对象序列化成文本,再把这段文本作为 `cardExtra` 的字符串值放进请求体。顶层请求体仍是扁平 JSON,不要把 `cardExtra` 作为嵌套对象或数组发送。平台把该字符串解析为 JSON 后按以下规则校验: - 顶层必须恰好是 `connection_info`、`account_info` 两个对象,不允许任何其他键。 - 两个对象中只能出现下表列出的键;出现未列出的键(包括拼写或大小写错误)即被拒。必填键缺失也被拒。 - 除 `create_time` 外,所有值都必须是 JSON 字符串:不能为空或全空白,不能含控制字符(换行、制表符等),长度和格式须完全符合下表。 - 任何文本值都不得含 `cvv`、`cvc`、`card number`、`card no`、`security code` 字样(不区分大小写,`cardnumber`、`card_no`、`security-code` 等写法同样禁止),不得含疑似卡号(13–19 位数字串,含以空格或 `-` 分隔的写法,且通过 Luhn 校验)。 - 整个 `cardExtra` 字符串按 UTF-8 编码不超过 8192 字节。 - 禁止放入卡号、CVV 等任何卡信息;卡信息只在托管支付页填写。 `connection_info`: | 键 | 必填 | 类型 | 规则 | |---|---|---|---| | `country` | 是 | 字符串 | 固定为 `"AU"` | | `state` | 是 | 字符串 | `NSW`、`VIC`、`QLD`、`ACT`、`NT`、`SA`、`TAS`、`WA` 之一(大写) | | `zip_code` | 是 | 字符串 | 4 位数字,如 `"2000"` | | `media_source` | 是 | 字符串 | 来源标识,由商户自定:1–64 个字符,只能是字母、数字、`_`、`-`(`[A-Za-z0-9_-]{1,64}`) | | `user_agent` | 否 | 字符串 | 付款人浏览器 User-Agent,不超过 1024 个字符,不含控制字符 | | `uuid` | 否 | 字符串 | 不超过 64 个字符,只能是字母、数字、`.`、`_`、`:`、`-`(`[A-Za-z0-9._:-]+`) | | `language` | 否 | 字符串 | 不超过 16 个字符,形如 `en`、`en-AU`(`[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})?`) | `account_info`: | 键 | 必填 | 类型 | 规则 | |---|---|---|---| | `merchant_user_id` | 是 | 字符串 | 付款人在商户侧的稳定用户 ID;不超过 64 个字符,只能是字母、数字、`.`、`_`、`:`、`@`、`-`(`[A-Za-z0-9._:@-]+`);与 email、phone 一一绑定,见下 | | `create_time` | 是 | **整数** | 该用户在商户侧的注册时间,Unix **毫秒**时间戳,必须是 JSON 整数(不能加引号写成字符串,不能带小数);不小于 `1000000000000`,且不晚于当前时间 | | `role` | 是 | 字符串 | `GUEST` 或 `PRIVATE`(大写) | | `area_code` | 是 | 字符串 | 固定为 `"61"`(字符串,不是数字) | | `first_name` | 是 | 字符串 | 付款人名:不超过 64 个字符,只能是英文字母、空格、`'`、`-` | | `last_name` | 是 | 字符串 | 付款人姓:规则同 `first_name` | **用户身份绑定**:`merchant_user_id` 与 `email`、`phone` 一一绑定。同一 `merchant_user_id` 必须始终使用同一 email 和同一 phone;同一 email 或同一 phone 也不能出现在另一个 `merchant_user_id` 下(email 比较不区分大小写)。任一组合与之前的订单冲突时下单被拒。请使用商户侧稳定的用户 ID,不要每单随机生成,也不要多个用户共用一个 ID;用户更换邮箱或手机号前请联系平台。 `cardExtra` 示例(为便于阅读而换行;实际发送时用下方请求示例中的单行文本): ```json { "connection_info":{ "country":"AU", "state":"NSW", "zip_code":"2000", "media_source":"demo_site", "user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64)", "language":"en-AU" }, "account_info":{ "merchant_user_id":"demo-user-0001", "create_time":1767225600000, "role":"PRIVATE", "area_code":"61", "first_name":"Test", "last_name":"User" } } ``` ##### 完整请求示例 以下全部为合成数据(IP 为文档保留地址段,邮箱与域名为 example.com,手机号为虚构值): ```json { "merchantOrderNo":"DEMO_AUD_20261003_001", "amount":"20.00", "currency":"AUD", "product":"AUD-VKDPWMRT", "payType":"CARD", "name":"Test User", "phone":"0491570156", "email":"payer@example.com", "clientIp":"203.0.113.10", "notifyUrl":"https://merchant.example.com/payments/notify", "returnUrl":"https://merchant.example.com/payments/result", "cardExtra":"{\"connection_info\":{\"country\":\"AU\",\"state\":\"NSW\",\"zip_code\":\"2000\",\"media_source\":\"demo_site\",\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64)\",\"language\":\"en-AU\"},\"account_info\":{\"merchant_user_id\":\"demo-user-0001\",\"create_time\":1767225600000,\"role\":\"PRIVATE\",\"area_code\":\"61\",\"first_name\":\"Test\",\"last_name\":\"User\"}}" } ``` ##### 该示例的待签名串 签名规则与第 3 节相同。以虚构的 `appId=demo-app`、`timestamp=1700000000000`、`nonce=demo-nonce-0001`、`appSecret=demo-secret` 为例,待签名串为(实际是一整行,此处未换行): ```text amount=20.00&appId=demo-app&cardExtra={"connection_info":{"country":"AU","state":"NSW","zip_code":"2000","media_source":"demo_site","user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64)","language":"en-AU"},"account_info":{"merchant_user_id":"demo-user-0001","create_time":1767225600000,"role":"PRIVATE","area_code":"61","first_name":"Test","last_name":"User"}}&clientIp=203.0.113.10¤cy=AUD&email=payer@example.com&merchantOrderNo=DEMO_AUD_20261003_001&name=Test User&nonce=demo-nonce-0001¬ifyUrl=https://merchant.example.com/payments/notify&payType=CARD&phone=0491570156&product=AUD-VKDPWMRT&returnUrl=https://merchant.example.com/payments/result×tamp=1700000000000demo-secret ``` - `cardExtra` 作为字符串**原样**参与签名:取的是请求体 JSON 解码后的字符串值本身。请求体里的 `\"` 在待签名串中就是 `"`;JSON 转义只是传输写法,不改变待签名值。 - 先生成一次 `cardExtra` 文本,并用同一段文本既放进请求体、又参与签名。签名后不要再重新序列化或格式化 `cardExtra`:键顺序或空格的任何变化都会改变字符串内容,导致验签失败。 - 值中的空格(如 `Test User`、User-Agent)原样保留,不做 URL 编码。 - 本例时间戳已过期、密钥虚构,不能请求生产接口,因此不给出 sign 值;请用[Python 示例](examples/client.py)或 [Node.js 示例](examples/client.mjs)按第 3 节算法自行计算并核对。 ##### 结果 成功时响应与普通代收相同:返回平台 `orderNo` 和 `payUrl`(托管支付页 HTTPS 地址,可能不在 HissPay 域名下),`payStatus=0` 只表示已创建。之后按下文「托管收银台」的方式引导付款人打开 `payUrl`,以验签通过的回调或查单结果为准,按 `realAmount` 入账;跳回 `returnUrl` 不代表已付款。 #### 托管收银台(hosted checkout) 托管收银台产品下单后返回托管支付页链接,付款人在该页面选择方式并付款,商户不接触支付信息。 1. 调用代收下单,保存平台 `orderNo`,把 `payUrl` 交给付款人:网页端整页跳转或在新窗口打开。能否嵌入 iframe,待确认。 2. 支付页有有效期,因支付方式而异(例如 15 分钟或 1 小时)。不要缓存或重复发放旧链接。 3. 付款后付款人可能跳回 `returnUrl`,也可能停留在支付页。跳回不代表已付款,未跳回也不代表未付款。 4. 等待平台代收成功通知(`status="1"`),或低频查单直到 `payStatus=1`;按 `realAmount` 入账(第 4 节)。 5. 付款人放弃或链接过期时,平台通常不发送失败通知,查单可能一直是 `payStatus=0`;只有通道明确确认付款失败时才可能收到失败通知或查到 `payStatus=2`(第 8、10 节)。如需让付款人重新付款,先联系平台确认原单已关闭,再用**新的** `merchantOrderNo` 创建订单;不要用原订单号重新下单。 6. 收银台页面展示哪些支付方式由产品配置决定;商户不能通过本接口要求只收银行卡。 #### USDT 代收(加密资产) USDT 代收使用与法币相同的下单、查单和异步通知接口,`currency` 固定为 `USDT`,通过 `product` 选择网络;商户不传链编号或网络字段。对应产品在上方目录中标为 `LIVE` 之前不可下单(当前为 PLANNED,即将开放,不可下单)。 | 产品编码 | 资产 | 网络 | 金额精度 | |---|---|---|---| | `USDT-TRON` | USDT | Tron | 最多 4 位小数 | | `USDT-ETHEREUM` | USDT | Ethereum | 最多 4 位小数 | | `USDT-BSC` | USDT | BSC | 最多 4 位小数 | | `USDT-SOLANA` | USDT | Solana | 最多 4 位小数 | - 金额使用十进制字符串,四个网络统一最多 4 位小数(最小单位 `0.0001` USDT),且须满足签约路由的金额下限;超出精度返回 10006。后续如放宽精度会另行通知。 - `payType` 可省略;显式传入时为 `CRYPTO`。`clientIp`、`cardExtra` 沿用通用规则。 下单请求示例: ```json { "merchantOrderNo":"DEMO_USDT_20261001_001", "amount":"12.3456", "currency":"USDT", "product":"USDT-TRON", "name":"Test User", "phone":"YOUR_TEST_PHONE", "email":"payer@example.com", "notifyUrl":"https://merchant.example.com/payments/notify", "returnUrl":"https://merchant.example.com/payments/result" } ``` 下单成功时响应比法币多返回收款信息 `paymentInfo`(`product` 所有代收均返回);商户可跳转 `payUrl`,也可按自身页面规范展示地址和二维码: ```json {"code":0,"msg":"Success","data":{"appId":"YOUR_APP_ID","merchantOrderNo":"DEMO_USDT_20261001_001","orderNo":"PLATFORM_ORDER_NO","currency":"USDT","product":"USDT-TRON","amount":12.3456,"payUrl":"https://cashier.example.com/example","payStatus":0,"paymentInfo":{"asset":"USDT","network":"TRON","address":"NETWORK_RECEIVING_ADDRESS","amount":"12.3456","qrCodeImage":null,"expiresAt":"2026-10-01T12:00:00Z"},"otherExtend":null}} ``` - `paymentInfo` 仅 USDT 产品且平台已分配收款地址时返回,否则不返回该键;返回时 6 个字段全部输出;`network` 为 `TRON`、`ETHEREUM`、`BSC` 或 `SOLANA`;`qrCodeImage` 为预留字段,当前版本恒为 null,请按 `address` 与 `amount` 自行生成二维码;`expiresAt` 为 UTC ISO 8601 时间,由平台给出,可能为 null;有值时过期后不要继续引导付款。响应中的 `amount` 为 JSON 数字,`paymentInfo.amount` 为字符串,均最多 4 位小数,请用 Decimal 解析。 - 付款人须按 `paymentInfo.amount` 足额转账;`payStatus=0` 只表示待付款,不代表链上到账。 - 查单(第 8 节)额外返回 `product` 和 `txHash`;`txHash` 仅在链上确认后返回,确认前不返回该键。建议轮询间隔 10–15 秒并退避。 - 成功通知(第 10 节)在有 `txHash` 时追加该字段,参与签名;USDT 通知金额保留 4 位小数。 - 按通知或查单中的 `realAmount` 幂等入账;不要用订单 `amount` 替代缺失的 `realAmount`。 #### 订阅与定期扣款 平台目前没有订阅或定期扣款接口。不要用代收下单模拟订阅或周期扣款;有需求请联系平台。 ### 下单结果不确定时 下单请求超时、断线、HTTP 5xx、返回非 JSON 或无法判断的结果时,订单可能已经创建。平台遇到不确定结果时不会自动重新提交,商户也不要重新提交。 1. 保留原 `merchantOrderNo`,不要再次调用代收下单:既不要换新订单号重下,也不要用同一订单号重下。 2. 用原 `merchantOrderNo` 低频查单,并等待验签通过的成功通知;以回调或查单的 `payStatus=1` 为准。 3. 不要用同一订单号修改金额或其他参数后重下:同一订单号再次下单返回 10020(启用业务幂等的产品参数不同返回 409,第 4 节)。 4. 长时间保持 `payStatus=0` 时联系平台核对,不要自行判定失败,也不要让付款人再次付款。 ### 16.3 THB 代收(PromptPay 扫码) `POST /transaction/payin/v1/apply`,字段在第 6 节字段表基础上按下表填写。下表是**统一契约**:同时满足所有付款通道的要求。不同付款通道实际做的校验不完全相同,部分不合规的值可能被某些通道接受、或被替换为默认值(如不合规的 `returnUrl` 可能被替换为平台默认结果页),因此**平台不保证对不符合下表的请求一律拒绝**;请按下表在提交前自行校验,不要以平台是否拒绝作为判断依据。 | 字段 | 必填 | THB 代收要求 | |---|---|---| | `currency` | 是 | 固定 `THB` | | `product` | 是 | 平台分配的 THB 代收产品编码 | | `amount` | 是 | 见 16.2,默认 500.00–49999.00 | | `name` | 是 | **付款银行账户的户名**,须与付款账户户名一致。去掉首尾空格后 1–50 个字符;只能使用泰文或英文字母(含泰文元音及声调符号)、空格、`.`、`'`、`-`,首字符须为字母;不能含数字或其他符号 | | `phone` | 是 | 付款人手机号:6–15 位数字,可带一个前导 `+`(如 `0812345678`、`+66812345678`),不含空格或 `-` | | `email` | 是 | **必填**。付款人邮箱,形如 `payer@example.com`,不含空格,不超过 64 个字符。不传或传空字符串时返回 400(msg 形如「请求参数不正确:email invalid」);格式不符或超长返回 9999(见下方自查清单) | | `cardExtra` | 是 | JSON **字符串**,格式见下 | | `payType` | 否 | **请勿传**。付款方式由 THB 代收产品决定(PromptPay 扫码),目前没有对应的 `payType` 编码。平台编码表中的 THB `CARD`、`PAYPAL` 组合能通过编码校验,但与本产品无关,传入也不会改变付款方式 | | `returnUrl` | 否 | 付款后浏览器跳转地址。填写时必须是 `https://` 绝对地址,**不能包含 `?` 查询参数或 `#` 片段**,不含空白字符,不超过 128 个字符;开通通知给出更短的上限时以通知为准。不填时付款后显示平台的默认结果页。开通通知未确认长度上限前,建议不传 | | `notifyUrl` | 否 | 与第 6 节相同;平台通知只发往这里或商户默认通知地址 | | `clientIp`、`addr` | 否 | 本产品不使用,可省略 | `cardExtra` 的值是一段**字符串**:把下面的 JSON 对象序列化成文本,再作为 `cardExtra` 的字符串值放进请求体(与第 6 节其他产品相同,不要作为嵌套对象发送)。对象必须**恰好**包含以下两个键,不允许其他键,值都必须是 JSON 字符串,序列化后的文本不超过 512 个字符: | 键 | 必填 | 规则 | |---|---|---| | `payerAccountNo` | 是 | 付款人的泰国**银行账号**(不是银行卡号,也不是手机号或 PromptPay ID),6–20 位纯数字,保留前导零,不含空格或 `-` | | `payerBankCode` | 是 | 付款银行,取 16.1 表中「代收」为「支持」的编码 | 说明:本产品要求提交付款人的银行账号与户名,用于让付款人使用该账户完成扫码付款;这是第 6 节「不要提交卡号」规则的唯一例外,仍然不得提交银行卡号、CVV、密码或任何验证码。付款人使用其他账户或其他户名付款时,可能无法自动确认到账,订单会进入人工核对。 完整请求示例: ```json { "merchantOrderNo":"DEMO_THB_20261026_001", "amount":"1500.50", "currency":"THB", "product":"YOUR_THB_PAYIN_PRODUCT", "name":"Somchai Jaidee", "phone":"0812345678", "email":"payer@example.com", "notifyUrl":"https://merchant.example.com/payments/notify", "cardExtra":"{\"payerAccountNo\":\"1234567890\",\"payerBankCode\":\"SCB\"}" } ``` 该示例的待签名串(虚构 `appId=demo-app`、`timestamp=1700000000000`、`nonce=demo-nonce-0001`、`appSecret=demo-secret`;实际是一整行): ```text amount=1500.50&appId=demo-app&cardExtra={"payerAccountNo":"1234567890","payerBankCode":"SCB"}¤cy=THB&email=payer@example.com&merchantOrderNo=DEMO_THB_20261026_001&name=Somchai Jaidee&nonce=demo-nonce-0001¬ifyUrl=https://merchant.example.com/payments/notify&phone=0812345678&product=YOUR_THB_PAYIN_PRODUCT×tamp=1700000000000demo-secret ``` 对应 `sign`(SHA-256 小写十六进制):`2857fc38acbfdcbaec806757360be995c4fa32349ffb5b6a273e14f6c3304bd4` `cardExtra` 按解码后的字符串原样参与签名,规则同第 6 节「澳洲银行卡(AUD-VKDPWMRT)」中的说明:先生成一次文本,同一段文本既放进请求体又参与签名。 成功响应与普通代收相同(第 6 节),例如: ```json {"code":0,"msg":"Success","data":{"appId":"YOUR_APP_ID","merchantOrderNo":"DEMO_THB_20261026_001","orderNo":"PLATFORM_ORDER_NO","currency":"THB","product":"YOUR_THB_PAYIN_PRODUCT","amount":1500.50,"payUrl":"https://cashier.example.com/example","payStatus":0,"otherExtend":null}} ``` - `payUrl` 是收银台页面(含 PromptPay 二维码)的地址,域名可能是第三方收银台,不在 HissPay 域名下,协议可能是 `https` 或 `http`。请把付款人**直接跳转**到 `payUrl`:不要校验或白名单限制其域名,不要改写、截断或追加参数,也不要自行重新生成二维码。`payStatus=0` 只表示已创建。 - 引导付款人用 `cardExtra` 中声明的银行账户扫码付款。付款页的有效期由付款页面显示;过期后不要让付款人重复付款,按下文处理。 下单结果与处理: | 下单结果 | 订单状态 | 处理 | |---|---|---| | `code=0` 且有 `payUrl` | 处理中 | 引导付款,等待成功通知或查单 | | 400、10006、10012 等通用校验错误 | 未创建或未提交 | 按第 11 节修正后用新单号下单 | | 9999,且原单查单 `paymentStatus=CANCELLED` 或查无此单 | 本节要求不满足,订单**未提交**付款通道,没有 `payUrl` | 按下方自查清单修正后用**新的** `merchantOrderNo` 下单 | | 9999,且原单查单 `paymentStatus=PROCESSING` | 结果未知:请求可能已到达付款通道 | 按第 6 节「下单结果不确定时」处理,不要换单号重下 | | 10009 | 付款通道明确未受理,没有 `payUrl`;当前订单仍显示 `PROCESSING` | 不要用原单号重试,不要引导付款;联系平台核对后再决定是否用新单号下单 | | HTTP 5xx、超时、非 JSON | 结果未知 | 同第 6 节「下单结果不确定时」 | 9999 自查清单(THB 代收):`cardExtra` 是字符串且恰好含两个键;`payerAccountNo` 为 6–20 位数字;`payerBankCode` 是 16.1 的 6 个代收编码之一;`name` 符合户名规则且不超过 50 个字符;`phone`、`email` 都已填写且符合上表格式(不传会先返回 400);`returnUrl` 不传,或为不含 `?`/`#`、不超过上限的 `https://` 地址;金额在单笔范围内;没有传 `payType`。 结果确认: - 平台核实付款状态与金额后才把订单置为成功,并发送成功通知(第 10 节,`status="1"`,`realAmount` 等于订单金额)。THB 代收**只发送成功通知**。 - 付款失败、放弃或过期的订单不发送通知。生产环境中这类订单一般**长期保持 `PROCESSING`**(旧字段 `payStatus=0`),平台目前不会把 THB 代收自动置为 `FAILED`;沙箱失败场景(尾数 .01、.07)会显示 `paymentStatus=FAILED`、`isFinal=true`,仅用于演练。不要把长期处理中当作失败,也不要换单号让付款人再付一次。 - 创建超过 3 天仍未确认成功的订单,平台可能无法再自动核实,请联系平台人工核对;不要换单号重复收款。 ## 7. 代付下单 `POST /transaction/payout/v1/apply` | 字段 | 必填 | 说明 | |---|---|---| | `merchantOrderNo` | 是 | 商户唯一代付订单号,最长 255 个字符 | | `amount` | 是 | 主币种金额,须大于 0 且不低于代付最低金额(通道下限与签约下限中的较高者,见第 4 节);低于最低金额返回 10006 | | `currency` | 是 | 已开通币种 | | `product` | 是 | 已签约的代付产品编码,与代收产品分开,最长 20 个字符 | | `accountName` | 是 | 收款账户姓名 | | `account` | 是 | 收款银行账号或适用支付账户,字符串 | | `phone` | 是 | 收款人手机号 | | `email` | 是 | 收款人邮箱 | | `bizCode` | 条件必填 | 业务类型编码,按平台提供的编码填写。仅下文「免填 bizCode / branchCode 的条件」三项**同时**满足时可不传;否则缺失或为空返回业务码 400 | | `payCode` | 是 | 支付方式编码;由平台按产品提供 | | `branchCode` | 条件必填 | 分行或银行标识;印度银行代付通常填写收款账户 IFSC,THB 填银行编码(第 16 节)。免填条件同 `bizCode` | | `bankName` | 否 | 银行名称,视通道要求;最长 255 个字符,超长返回业务码 400 | | `notifyUrl` | 否 | 本单 HTTPS 回调地址;可回退到商户默认通知地址 | | `address` | 否 | 收款人地址 | | `accountId` | 是 | 收款人在商户系统中的唯一标识,同一收款人保持不变;平台按通道要求作为收款用户标识上送 | | `swiftCode` | 否 | SWIFT 标识,视通道要求 | | `extra` | 否 | 扩展信息,使用前确认格式 | **免填 bizCode / branchCode 的条件**(三项同时满足才可省略,否则两者都必填): 1. `payCode` 为 `CARD`; 2. `product` 为 `USD-PAYPLUS-PAYOUT`、`USD-LOCAL-PAYOUT` 或 `AUD-LOCAL-PAYOUT` 之一; 3. `currency` 与产品匹配:前两个产品为 `USD`,`AUD-LOCAL-PAYOUT` 为 `AUD`。 例如上述产品传 `payCode=BANK`,或币种不匹配时,缺少 bizCode / branchCode 会返回业务码 400。 ```json { "merchantOrderNo":"DEMO_OUT_20260912_001", "amount":"100.00", "currency":"INR", "product":"INR-NPLMSUEX", "accountName":"RECIPIENT_NAME", "account":"RECIPIENT_ACCOUNT", "phone":"RECIPIENT_PHONE", "email":"recipient@example.com", "bizCode":"ASSIGNED_BIZ_CODE", "payCode":"ASSIGNED_PAY_CODE", "branchCode":"RECIPIENT_IFSC", "notifyUrl":"https://merchant.example.com/payments/notify" } ``` 成功受理返回 `appId`、`merchantOrderNo`、`orderNo`、`currency`、`amount`、`payStatus:0`。平台会校验可用余额,并按签约费用及账务规则占用资金,异步完成付款。不要收到 `code=0` 就向用户宣称“已到账”,也不要因为尚未回调再次提交新代付。 代付下单返回非 0 业务码时,订单通常**未创建**、未占用资金;返回 500、超时或非 JSON 时结果未确认,须先查原单。代付接口的 msg 当前统一改写、不指出具体字段,各类拒绝的返回形式与处置见第 11 节「代付下单的拒绝与 msg」。 ### USD 银行卡代付接入(按商户开通) 本节描述 USD 银行卡代付的接口格式,**不代表该产品已向您的应用开放**。先由平台确认产品编码、USD 账户、额度、费用、来源 IP 白名单及收款人确认,再使用生产应用。当前只支持单笔 CARD,不支持该产品批量代付或自动重付;收款人通过下方独立接口创建、查询和更新。请勿复用 USD 钱包代付产品编码。 | 字段 | USD 银行卡要求 | |---|---| | `currency` / `payCode` | `USD` / `CARD` | | `product` | 平台为该应用开通的银行卡代付产品编码;不是渠道名称或代收产品 | | `account` | 收款卡号,13–19 位数字,通过卡号校验;不要提交 CVV | | `extra` | JSON **字符串**,仅含 `beneficiary_id`(正整数)和 `card_valid`(`MM/YYYY`);有效期不能已过期 | | `extra.beneficiary_id` | 收款人接口查询 READY 后返回的 beneficiaryId;不可猜测,不可跨应用、产品或环境使用 | | `accountName` / `phone` / `email` | 必填,填写该收款人的合法信息 | | `bizCode` / `branchCode` | 此美国卡代付产品不需要,新订单省略这两个字段;不填写 IMPS、IFSC 或占位值。历史订单重试保留首次请求原值,避免幂等冲突 | | `notifyUrl` | 您自己的 HTTPS 接收地址。接收的是本平台商户通知,不是资金服务商的通知格式 | 以下为结构示例。替换所有占位值后使用,金额须符合实际签约范围;编号 `12345` 仅用于展示类型。 ```json { "merchantOrderNo":"MERCHANT_OUT_UNIQUE_001", "amount":"100.00", "currency":"USD", "product":"USD-PAYPLUS-PAYOUT", "accountName":"RECIPIENT_NAME", "account":"RECIPIENT_CARD_NUMBER", "phone":"RECIPIENT_PHONE", "email":"recipient@example.com", "payCode":"CARD", "extra":"{\"beneficiary_id\":12345,\"card_valid\":\"12/2030\"}", "notifyUrl":"https://merchant.example.com/payout/notify" } ``` 签名沿用第 3 节:请求体平面字段与 `appId`、`timestamp`、`nonce` 合并,按键排序,拼接 `key=value`(`&` 分隔),末尾直接追加 appSecret,再计算 UTF-8 SHA-256 小写十六进制。`extra` 是字符串,以**传输时相同的字符串内容**参与签名;不要把 JSON 内部字段单独展开或改空格。timestamp 为毫秒,时间误差不超过 30 秒,每次 HTTP 请求使用新 nonce。商户只使用本平台 appId/appSecret。 可下载 [Python 请求示例](examples/payout_request.py)、[Java 请求示例](examples/PayoutRequest.java) 和 [JSON 配置模板](examples/payout-request.json)、[Java 配置模板](examples/payout-request.properties)。Python 默认只预检,Java 默认只预检;均需显式 `--send` 才请求。先用 `query` 或 `balance` 验证鉴权,单笔提交时保存原单号。两种示例都不会自动重试或跟随重定向;通信中断后用原单号查单,不换号重付。示例配置可能含卡资料,仅保存在受限服务器,不提交 Git。 ```sh python3 payout_request.py balance payout-request.json python3 payout_request.py query payout-request.json --send javac PayoutRequest.java java PayoutRequest balance payout-request.properties java PayoutRequest query payout-request.properties --send ``` 生产请求地址由 `HISSPAY_GATEWAY` 指定(`https://transaction.hisspay.com`);商户模拟测试使用 `https://sandbox-api.hisspay.com` 和独立沙箱凭据。`HISSPAY_APP_ID`、`HISSPAY_APP_SECRET` 只在服务器环境变量配置。 **结果判定与幂等:**下单 `code=0 / payStatus=0` 只是受理。同一应用与 `merchantOrderNo` 唯一绑定原请求:相同单号再次提交返回 10020;若开通通知说明本产品已启用业务幂等,则相同参数返回原受理结果、改变金额或收款资料返回 409(第 4 节)。出现超时、5xx、无回调、查单暂未找到时均不能推断付款失败。先查原单;`paymentStatus=SUCCEEDED / isFinal=true` 表示成功,`FAILED / true` 表示失败,其他状态继续核对。退款或异常资金调整由平台核对,不能据此自动重付。 **异步通知:**JSON 字段为 `merchantOrderNo`、`orderNo`、`currency`、`product`、`amount`、`status`、`statusDes`,`utr` 可能为空。`status="1"` 成功、`status="2"` 失败;这与查询的 `paymentStatus` 不同。HTTP 头 `appId/timestamp/nonce/sign` 按第 10 节验签(空值不参与签名)。核对订单归属、币种、产品和原金额,在数据库事务中按应用和订单去重并保存结果后,返回 HTTP 200、纯文本 `SUCCESS` 或 `OK`。同一状态重复通知应正常 ACK,冲突终态进入人工核对,不能再次付款。成功响应正文不要包 JSON 或 HTML。失败说明仅用于诊断,不据 `statusDes` 文本判断资金终态。 **常见错误:**400 参数或签名错误;10006 金额限制;10008 可用余额不足;10010 代付来源 IP 未加白;10011 产品无效;10005 无可用路由;10020 商户订单号重复;409 同单号参数冲突(仅启用业务幂等的产品);429 限流。代付接口的 msg 统一改写(见第 11 节「代付下单的拒绝与 msg」)。500、超时及非 JSON 错误均视为结果尚未确认,查原单。完整错误码见第 11 节,HTTP 200 本身不能代表业务成功。 商户可先完成 [沙箱代付完整自测](sandbox-guide.html),包括成功、失败、未知结果查询、手动终态、真实沙箱通知与本地重复通知去重。该测试使用模拟资金,不证明生产银行卡到账或服务商沙箱验收通过。 ### 美国与澳洲:按支付方式阅读 按“适用范围 → 收款人 → 请求路径与方法 → Header → Body → 返回结果 → 查询与回调 → 测试”顺序接入。下表区分服务商规范与本平台接口,不能混用域名、签名或状态码。 | 项目 | 美国 USD 卡代付 | 澳洲 AUD | |---|---|---| | 正式支持 | 已签约并开通 USD-PAYPLUS-PAYOUT 的应用;美国收款人,借记卡出款 | 尚未开放;已核实的正式卡代付规范未列出 AUD,不提供推测的正式请求 | | 平台沙箱 | USD-LOCAL-PAYOUT;模拟收款账户,不发生真实银行卡出款 | AUD-LOCAL-PAYOUT;模拟收款账户,不证明真实 AUD 出款已支持 | | 收款人 | 创建 → 查询 READY → 使用返回编号付款;可复用同一已确认收款人 | 平台模拟使用 SANDBOX-SUCCESS 等场景账户;正式受益人规则待官方规范确认 | | bizCode / branchCode | 新订单省略;不需要平台另发出款或分行编码 | AUD-LOCAL-PAYOUT + AUD + CARD 的模拟订单省略;不可据此构造正式请求 | | 金额 | 以本应用签约限额为准,不从沙箱最低额推断正式限额 | 模拟最低 100 AUD;正式范围不作承诺 | **美国字段对应关系:**平台 `extra.beneficiary_id` 对应服务商 `beneficiary_id`;`account` 对应 `card_number`;`extra.card_valid` 对应 `card_valid`(MM/YYYY);`currency`、`amount` 含义一致。平台生成服务商 `reference_id`,商户使用自己的 `merchantOrderNo` 查单和对账。收款人 `firstName/lastName/payeeId` 是平台字段,平台负责转换和隔离,商户不能直接提交其他账户的服务商编号。`extra` 保持 JSON 字符串格式参与签名。 **平台差异:**商户请求本平台 `/transaction/payout/v1/...`,使用本平台 `appId/timestamp/nonce/sign`;不要使用服务商的 AppId/Authorization 或服务商密钥。`product/payCode/notifyUrl` 是本平台路由和商户通知字段。平台下单受理 `code=0` 不等于到账;查询终态和验签回调按本节说明处理,不直接套用服务商返回码。已有订单重试时保持首次完整请求,包括原 bizCode/branchCode;不要因新版将字段改为空、删掉或换商户单号再次付款。 ### 收款人管理:创建 → 查询确认 → 代付 本节为新增接口接入说明,须在平台通知接口及您签约产品已开放后使用;文档中的格式示例不代表正式代付已开通。当前实际银行卡流程仅支持美国收款人(`country=US`)及 USD。AUD 沙箱模拟仍可测试,但不表示真实 AUD 银行卡代付已获支持。 商户可为不同收款人创建记录,无需平台提前绑定一个固定收款人。为每位收款人在自己的系统中生成稳定的 `payeeId`,相同应用内不可换绑其他人;不要使用邮箱或卡号作为该标识。不同应用、产品及环境的记录不可混用。资料和卡号仅在商户服务端安全保存,不出现在前端、日志或代码仓库。 | 操作 | HTTPS POST 路径 | `operation` | |---|---|---| | 创建收款人 | `/transaction/payout/v1/beneficiaries/create` | `CREATE` | | 查询收款人 | `/transaction/payout/v1/beneficiaries/query` | `QUERY` | | 更新资料 | `/transaction/payout/v1/beneficiaries/update` | `UPDATE` | 三种操作均用本平台 `appId / timestamp / nonce / sign` 签名,规则与代付一致;`operation` 是请求体必填字段,也参与签名,必须匹配路径。资料为平面字符串,不嵌套 JSON;更新时 `version` 必须是 JSON 整数,不能加引号。 | 字段 | 创建 | 查询 | 更新 | 填写方法 | |---|---|---|---|---| | `operation` | 必填 | 必填 | 必填 | 分别为 CREATE、QUERY、UPDATE | | `product` | 必填 | 必填 | 必填 | 您已签约的 USD 银行卡代付产品编码 | | `payeeId` | 必填 | 必填 | 必填 | 商户不可变收款人标识,3–60 位字母、数字、下划线或连字符 | | `language` / `country` | 必填 | 不传 | 必填 | 语言与国家,例如 en / US | | `phone` / `email` | 必填 | 不传 | 必填 | 收款人真实联系电话与邮箱 | | `birthdate` | 必填 | 不传 | 必填 | 真实出生日期,YYYY-MM-DD | | `firstName` / `lastName` | 必填 | 不传 | 必填 | 收款人真实名与姓 | | `zip` / `city` / `state` / `address` | 必填 | 不传 | 必填 | 收款人真实邮编、城市、州及地址 | | `version` | 不传 | 不传 | 必填 | 最近查询返回的版本,JSON 非负整数;不要猜测或固定为 0 | | `requestId` | 不传 | 不传 | 必填 | 本次修改的唯一标识,3–60 位字母、数字、下划线或连字符 | 产品编码最多 20 字符,所有资料不能含控制字符。语言为两个英文字母;电话为 4–12 位数字,或 `+国家码-号码`(国家码 1–3 位,例如 `+1-2025550123`)。名、姓各 3–50 字符,支持拉丁字母、撇号、连字符及内部空格。城市和州为 2–50 字符,支持拉丁字母及撇号、&、括号、连字符、内部空格;美国邮编为 5 位、5+4 位或 5–10 位数字。地址 5–100 字符,支持字母、数字、空格及 `# ' ( ) , - . / : ; °`;邮箱最多 120 字符。出生日期须在 1900-01-01 至当前 UTC 日期之间。更新版本须为 JSON 整数,范围 0–9223372036854775806。 查询仅传 `operation / product / payeeId`,不传个人资料,也不传第三方受益人编号。更新须提交完整资料,不是局部补丁;`payeeId` 不可改名或转给另一人。 响应沿用 `{ "code": 0, "msg": "", "data": ... }`。`data` 包含 `payeeId`、可为空的 `beneficiaryId`、`status` 和 `version`。必须同时检查业务 `code` 和状态;HTTP 200 本身不表示收款人可用于付款。 | `status` | 商户处理 | |---|---| | `READY` | 资料已确认;有有效 `beneficiaryId` 时才可申请代付 | | `UNKNOWN` | 创建或更新结果未确认;查询同一 `payeeId`,不要换号重建 | | `NOT_FOUND` | 查询暂未确认记录;不能据此自动重复创建,先核对原请求或联系平台 | | `REVIEW` | 资料或结果需要核对;暂停对此记录出款 | 推荐流程: 1. 新收款人调用 create,并保存原 `payeeId` 和响应。连接中断或结果未知时改查同一 `payeeId`,不换号重复创建。 2. 调用 query,确认 `READY`,保存返回的 `beneficiaryId` 和 `version`。相同资料的已确认收款人可复用。 3. 以该 `beneficiaryId` 填写原代付请求 `extra` 的 `beneficiary_id`,同时填写真实卡号和 `card_valid`;然后用唯一 `merchantOrderNo` 申请一次代付。收款人创建成功不表示付款已完成。 4. 需要修改尚未用于出款的记录时,使用最近 `version`、新的 `requestId` 和完整资料调用 update,再查询确认。版本冲突返回 409 时先查最新记录,不盲目递增或重放更新。 5. 已被任意出款冻结上下文使用的收款人不能再更新;合法资料变化需要新的 `payeeId`,重新创建并确认。新记录不能替代对旧代付订单的查询,也不能作为重复付款理由。 格式错误通常为 400,未开放或无权限为 403,身份、版本或已使用冲突为 409;500、超时及非 JSON 响应按结果未知处理。所有请求使用新的时间戳和 nonce,但不能把新的签名当成可重复创建、更新或付款的许可。 ### Python / Java 收款人请求示例 下载 [Python 示例](examples/beneficiary_request.py)、[公共签名客户端](examples/client.py)、[Java 示例](examples/BeneficiaryRequest.java),以及创建、查询、更新模板:[创建 JSON](examples/beneficiary-create.json) / [Properties](examples/beneficiary-create.properties)、[查询 JSON](examples/beneficiary-query.json) / [Properties](examples/beneficiary-query.properties)、[更新 JSON](examples/beneficiary-update.json) / [Properties](examples/beneficiary-update.properties)。示例资料全部为虚构,发送前替换产品及真实收款人资料。配置文件执行 `chmod 600`。 ```sh # 默认只预检字段名称,不打印收款人资料,也不请求网络。 python3 beneficiary_request.py create beneficiary-create.json python3 beneficiary_request.py query beneficiary-query.json python3 beneficiary_request.py update beneficiary-update.json javac BeneficiaryRequest.java java BeneficiaryRequest create beneficiary-create.properties java BeneficiaryRequest query beneficiary-query.properties java BeneficiaryRequest update beneficiary-update.properties ``` 环境变量沿用 `HISSPAY_GATEWAY`、`HISSPAY_APP_ID`、`HISSPAY_APP_SECRET`,须选对应环境的凭据。正式环境会创建或修改真实收款人;确认服务开放和资料准确后,才为所需命令加 `--send`。不要一次批量执行创建、更新和付款。示例不自动重试,不跟随重定向;query 确认 `READY` 后,将返回编号放入 [代付 Python 示例](examples/payout_request.py) 或 [Java 示例](examples/PayoutRequest.java) 的配置,再单独提交付款。 平台 USD/AUD 沙箱代付合成标记无需真实收款人资料,仍使用原有沙箱完整自测工具。本节新增收款人接口的沙箱可用性须以平台开放通知为准,不能将合成银行卡标记用到正式网关。 ### USD / AUD 代付沙箱示例 美国USD与澳洲AUD均可按[沙箱测试指南](sandbox-guide.html)使用平台模拟资金完成下单、查单、成功/失败通知和重复通知验收。使用对应币种的独立沙箱应用、已开通代付产品与路由,默认金额100。模拟结果不证明真实银行卡到账或正式产品开通。 下载[完整自测工具](examples/quickstart.zip),其中包含Python与Java,以及[USD JSON](examples/payout-sandbox-usd.json)、[USD Properties](examples/payout-sandbox-usd.properties)、[AUD JSON](examples/payout-sandbox-aud.json)、[AUD Properties](examples/payout-sandbox-aud.properties)。合成收款标记仅限沙箱,未知结果查询原单,不重复提交。 ### 16.4 THB 代付(银行转账) > **THB 代付需单独开通,以开通通知为准。** 未收到开通通知(含代付产品编码、可用银行、户名长度上限、单笔范围与费率)前请勿调用;下文是开通后的接口格式。 `POST /transaction/payout/v1/apply`,字段在第 7 节字段表基础上按下表填写: | 字段 | 必填 | THB 代付要求 | |---|---|---| | `currency` | 是 | 固定 `THB` | | `product` | 是 | 平台分配的 THB 代付产品编码(第 1 节目录,以开通通知为准) | | `payCode` | 是 | 不能为空;填 `BANK`。平台不按 `payCode` 选择 THB 付款通道,开通通知另有说明时以通知为准 | | `amount` | 是 | 收款人应收金额,见 16.2,默认 500.00–49999.00 | | `bizCode` | 是 | 固定为 `BANK` | | `branchCode` | 是 | 收款银行编码,取 16.1 表中的值,且须在开通通知列出的可用银行内(可用银行按付款通道配置) | | `account` | 是 | 收款人泰国银行账号,6–20 位纯数字,保留前导零,不含空格或 `-`;必须真实有效 | | `accountName` | 是 | 收款账户户名,须与收款账户一致:泰文或英文字母(含泰文元音及声调符号)、空格、`.`、`'`、`-`,首字符须为字母,去掉首尾空格后按字符计算长度。**长度上限按付款通道不同,以开通通知为准**:部分通道默认最多 **10 个字符**(付款通道自身限制,平台可在通道确认后放宽,最多 50),部分通道最多 64 个字符。开通通知未写明时按 10 个字符准备;户名超长时不要自行截断,联系平台确认 | | `phone` | 是 | 收款人手机号:6–15 位数字,可带一个前导 `+`(如 `0812345678` 或 `+66812345678`),不含空格或 `-` | | `email` | 是 | **必填**。收款人邮箱,形如 `recipient@example.com`,不含空格,不超过 64 个字符。不传或传空字符串时返回业务码 400(代付 msg 见第 11 节) | | `notifyUrl` | 否 | 与第 7 节相同 | | `bankName`、`swiftCode`、`address`、`accountId`、`extra` | 否 | 本产品不使用,可省略 | 完整请求示例: ```json { "merchantOrderNo":"DEMO_THB_OUT_20261026_001", "amount":"2000.00", "currency":"THB", "product":"YOUR_THB_PAYOUT_PRODUCT", "payCode":"BANK", "bizCode":"BANK", "branchCode":"KBANK", "account":"1234567890", "accountName":"Somchai J", "phone":"0812345678", "email":"recipient@example.com", "notifyUrl":"https://merchant.example.com/payments/notify" } ``` 该示例的待签名串(虚构值同上;实际是一整行): ```text account=1234567890&accountName=Somchai J&amount=2000.00&appId=demo-app&bizCode=BANK&branchCode=KBANK¤cy=THB&email=recipient@example.com&merchantOrderNo=DEMO_THB_OUT_20261026_001&nonce=demo-nonce-0001¬ifyUrl=https://merchant.example.com/payments/notify&payCode=BANK&phone=0812345678&product=YOUR_THB_PAYOUT_PRODUCT×tamp=1700000000000demo-secret ``` 对应 `sign`:`d5e969c9734479c732d7f3a8e74b9e273e9334ccc7f805eb3d067ab663236f4b` 结果与状态: - 下单 `code=0` 只表示平台已受理并占用资金(16.2),付款异步完成;不要据此宣称已到账。 - **本节字段不合规时**同步返回参数校验错误、**本次请求未建单**、不占用资金:msg 形如 `Invalid payout request: `,指出字段与规则(不回显字段值),具体返回码与各字段的 msg 见第 11 节「代付下单的拒绝与 msg」。按 msg 修正后重新提交(可沿用原 `merchantOrderNo`)。 - 平台可能按权重把 THB 代付分配到多个付款通道;校验按本次可能分配到的所有通道规则的**交集**执行(户名取最严上限、可用银行取交集等),与实际分配到哪个通道无关。 - 下单返回 500(`Payout request not confirmed; query original order`)表示平台或上游出现未知异常、结果未确认:保留原单号查单,不要换单号重付(第 11 节)。 - 自查清单:`bizCode=BANK`;`branchCode` 在 16.1 表且在开通通知的可用银行内;`account` 6–20 位数字;`accountName` 只含允许的字符且不超过开通通知的长度上限;`phone` 6–15 位数字可带前导 `+`;`email` 格式正确且不超过 64 个字符;金额在单笔范围内。 - 平台核实付款通道的处理结果和金额后才给出终态:成功通知 `status="1"`(`utr` 为银行流水号,可能为 null),失败通知 `status="2"`。`statusDes` 只用于展示和排查,不要用它做程序判断。 - **超时不等于失败。** 结果无法确认(如付款通道超时、处理中、返回不一致或无法验证)时,订单保持处理中(查单 `payStatus=0`),资金继续占用,由平台核对后再给出终态。**不要因为尚未收到通知或查单仍为 0 就判定失败、退款给用户,或用新的商户订单号重复代付**;超过您内部跟踪时限时联系平台人工核对(第 12 节)。 | 场景 | 查单 `payStatus` | 通知 `status` | 含义 | |---|---|---|---| | 已受理/处理中/结果待核实 | 0 | 不发送 | 继续等待,不要重复提交 | | 付款成功 | 1 | `"1"` | 已核实成功,`utr` 可能为 null | | 付款失败或被拒(含部分通道的提交前校验未通过) | 2 | `"2"` | 未出款,按账务规则释放占用资金 | ## 8. 查单与状态 代收:`POST /transaction/payin/v1/query` 代付:`POST /transaction/payout/v1/query` ```json {"merchantOrderNo":"YOUR_ORIGINAL_MERCHANT_ORDER_NO"} ``` 或使用平台订单号: ```json {"orderNo":"PLATFORM_ORDER_NO"} ``` 两者至少提供一个,建议只传一个。两者同时提供时必须指向**同一笔订单**,否则返回 30001。使用创建订单的同一个 appId;不存在或不属于当前应用的订单也可能返回 30001。 返回字段:`appId`、`merchantOrderNo`、`orderNo`、`currency`、`amount`、`payStatus`,以及第 14 节的 `paymentStatus`、`isFinal`、可选 `failureReason`。代收额外返回 `realAmount`、`product`(当前订单均返回,历史归档订单可能为 null)、`txHash`(仅 USDT 产品在链上确认后返回,法币或确认前不返回该键)和可选 `failureCode`(通道明确确认付款失败时给出,取值如 `PAYMENT_DECLINED`、`PAYMENT_RISK_REJECTED`、`PAYMENT_FAILED`);金额字段为 JSON 数字;代付额外返回 `payStatusDes`。未知的新字段请忽略。 | 场景 | 状态字段 | 值 | 含义 | |---|---|---|---| | 代收下单 | payStatus | 0 | 创建/待支付 | | 代收查单 | payStatus | 1 | 已收款 | | 代收查单 | payStatus | 0 | 未报告成功:创建、处理中、待核对、已取消或结果未知,用 `paymentStatus` 细分 | | 代收查单 | payStatus | 2 | 通道明确确认付款失败(目前仅部分通道),`paymentStatus=FAILED`、`isFinal=true` | | 代付下单 | payStatus | 0 | 已受理、异步处理中 | | 代付查单 | payStatus | 0 | 处理中或尚未得出终态 | | 代付查单 | payStatus | 1 | 付款成功 | | 代付查单 | payStatus | 2 | 付款失败 | 不能把代收查单的 0 当作“确定失败”;代收失败只在 `payStatus=2` 或 `paymentStatus=FAILED`(`isFinal=true`)时确认。成功代收的原金额与实际金额可以不同,按上述实际到账规则处理。金额缺失/非法、长时间未决、查询与回调矛盾时保留订单并联系平台核对。 ## 9. 余额查询 `POST /transaction/payout/v1/balance` ```json {"currency":"INR"} ``` ```json {"code":0,"msg":"Success","data":{"appId":"YOUR_APP_ID","accountNo":"ACCOUNT_NUMBER","currency":"INR","balance":1000.00,"balanceActive":800.00,"balanceFreeze":200.00}} ``` - `balance`:账面余额。 - `balanceActive`:可用余额,是否可代付还要考虑手续费及其他校验。 - `balanceFreeze`:冻结/占用金额,不能再次使用。 页面旧余额不能替代实时查询。扣款、代付或审核中的申请可能已占用资金;应处理原申请,不要重复提交来尝试“清零”。 ## 10. 异步通知与验签 平台以 POST JSON 请求商户 notifyUrl,头部携带 `appId`、`timestamp`、`nonce`、`sign`。 ### 通知体 代收成功示例(字段值当前以字符串发送): ```json {"currency":"INR","merchantOrderNo":"DEMO_IN_20260912_001","orderNo":"PLATFORM_ORDER_NO","product":"YOUR_PAYIN_PRODUCT","amount":"100.00","realAmount":"100.00","status":"1"} ``` 代付成功示例: ```json {"currency":"INR","merchantOrderNo":"DEMO_OUT_20260912_001","orderNo":"PLATFORM_ORDER_NO","product":"YOUR_PAYOUT_PRODUCT","amount":"100.00","utr":"BANK_REFERENCE_IF_AVAILABLE","status":"1","statusDes":"Success"} ``` 代付失败:`status="2"`,`statusDes` 提供原因(仅供展示)。`utr` 可能为 null;不能假设每次都有。法币通知金额字符串固定两位小数,JPY 等零小数币种的通知也如此;USDT 通知金额固定 4 位小数。不要混淆通知体的 `status` 与查单的 `payStatus`。 ### 代收失败通知 代收成功一定发送通知;**失败通知只在部分通道、且通道明确确认付款失败时发送**,其余失败、放弃、过期或取消的订单不发送通知,须查单或联系平台核对。失败通知 `status="2"`、`realAmount="0.00"`,有两种形态,验签规则相同(所有非 null 字段参与签名): - 基本形态:字段与成功通知相同,仅 `status="2"`、`realAmount="0.00"`。 - 扩展形态:另含 `eventId`(事件唯一标识,可用于去重)、`eventType="PAYIN_FAILED"`、`paymentStatus="FAILED"`、`isFinal="true"`、`failureCode`(`PAYMENT_DECLINED`、`PAYMENT_RISK_REJECTED` 或 `PAYMENT_FAILED`)与 `failureReason`(英文说明,仅供展示)。 ```json {"amount":"100.00","currency":"USD","eventId":"EVENT_ID","eventType":"PAYIN_FAILED","failureCode":"PAYMENT_DECLINED","failureReason":"Payment declined","isFinal":"true","merchantOrderNo":"DEMO_IN_20260912_001","orderNo":"PLATFORM_ORDER_NO","paymentStatus":"FAILED","product":"YOUR_PAYIN_PRODUCT","realAmount":"0.00","status":"2"} ``` 收到失败通知时:验签并核对订单归属后,把订单置为失败、不入账,返回 SUCCESS;不要按 `failureReason` 文本做程序判断。已确认成功的订单若再收到矛盾的失败通知,进入人工核对,不要冲正。 USDT 代收成功示例(有链上交易哈希时追加 `txHash`,并参与签名;未知的新字段同样参与验签): ```json {"currency":"USDT","merchantOrderNo":"DEMO_USDT_20261001_001","orderNo":"PLATFORM_ORDER_NO","product":"USDT-TRON","amount":"12.3456","realAmount":"12.3456","status":"1","txHash":"CHAIN_TRANSACTION_HASH"} ``` ### 验签与入账顺序 1. 按不区分大小写的 HTTP 头名读取四个签名头,核对 appId 属于接收方应用。 2. 取通知体所有**非 null**字段,加上头部 appId、timestamp、nonce,按请求签名同一排序方式计算 SHA-256。不要把 sign 本身加入;未知的新字段也应参与计算。 3. 用恒定时间比较校验签名。核对时间戳;建议允许不超过 5 分钟的时钟偏差并同步服务器时间。重试通知会重新生成签名头。 4. 核对商户订单号、平台订单号、币种、产品,确认 `amount` 等于本地原订单金额。代收成功以 `realAmount` 作为实际收款依据,允许其与 `amount` 不同;验签、归属、金额合法性或已入账金额存在矛盾时进入人工核对,不自动重复记账。 5. 在商户数据库事务中持久化通知和状态,以“应用+业务类型+商户订单号”的唯一约束/行锁保证同一笔只入账一次。 6. 数据提交成功后,返回 HTTP 2xx,响应体为纯文本 **`SUCCESS`**。重复通知验签并确认已处理后,也返回 SUCCESS。 响应不能包在 JSON 或 HTML 中,不附加换行或空格。当前兼容纯文本 OK 或 SUCCESS,大小写不敏感;统一使用 SUCCESS 最稳妥。失败或未能持久化时不要提前确认成功。 ### 实际金额不同的成功通知 以下 BDT 仅用于说明金额语义;该币种目前没有完整生产交易配置,不能据此下单。 以下业务体仍使用上文签名头和验签规则,不能仅凭 JSON 内容入账: ```json {"currency":"BDT","merchantOrderNo":"DEMO_BD_001","orderNo":"PLATFORM_ORDER_NO","product":"YOUR_BDT_PAYIN_PRODUCT","amount":"100.00","realAmount":"95.00","status":"1"} ``` 查单成功时对应 `payStatus=1`,`amount=100.00`、`realAmount=95.00`,语义与通知一致。通知使用 `status`,查单使用 `payStatus`,不要混淆字段。同一订单的查询与通知应共用一个事务幂等入账记录,不分别入账。 ### 重试 常规通知任务每笔最多投递 5 次(含首次),未收到有效 ACK 时按分钟级递增间隔重试(间隔约 5、10、30、60 分钟),实际执行时间受队列及网络影响,不应依赖固定送达时刻。超过次数后不再自动重试,可联系平台人工重发;人工重发、沙箱 `resend` 都可能带来额外通知。每次投递都会重新生成 `timestamp`、`nonce` 和 `sign`,不能仅用 nonce 去重,应按订单(或扩展失败通知的 `eventId`)幂等处理。 单次投递的规则: - 目标:可公开访问的 `http://` 或 `https://` 地址,推荐 HTTPS;不得含用户名/密码或 `#` 片段。平台代码不限制端口,但出口网络可能只放行常用端口,**建议使用 443**。内网、回环、链路本地、保留地址会被拦截,不能将 localhost 作为通知地址。**沙箱**更严格:只允许 80 / 443 端口,解析到非公网地址的通知直接判失败、不再重试,并有每商户 / 每目标主机每分钟上限与连续失败熔断,规则与投递记录查询见[沙箱测试指南](sandbox-guide.html)「回调规则」。 - 来源 IP:回调来源 IP 按环境不同:沙箱 `3.109.43.0`(固定);生产 IP 在开通通知中提供。接收端如有入站白名单,请按环境加入对应地址。 - 不跟随重定向:返回 3xx 视为投递失败,请直接配置最终地址。 - 超时:连接约 5 秒,整个请求约 10 秒;请先持久化再快速返回,耗时处理放到异步任务。 - ACK:HTTP 2xx 且响应体**恰好**为 `SUCCESS` 或 `OK`(大小写不敏感,不 trim:前后不能有空格或换行);响应体超过 64 KB 也视为失败。 若未配置本单或默认通知地址,平台不会发送通知,应先补齐配置。 ## 11. 错误码与处理 | code / 情况 | 含义 | 建议 | |---|---|---| | 0 | 接口操作成功 | 继续按支付状态判断 | | 400 | 签名、时间戳、nonce或输入不合法等(HTTP 200、JSON `code=400`;代收接口字段超长或 URL 非法时 msg 形如「请求参数不正确:merchantOrderNo too long」;签名类 msg 见第 3 节;代付类接口 msg 统一改写,见下文「代付下单的拒绝与 msg」) | 看 msg,校时并检查必填字段;不要盲重试下单 | | 404 / 405 | 请求路径不存在 / 请求方法不正确(五个接口都只接受 POST) | 核对 `{GATEWAY}` 与第 2 节路径、使用 POST | | 409 | 启用业务幂等的产品:同业务单号的请求参数与原单不一致;收款人接口:身份、版本冲突或已被出款使用(`Beneficiary request conflicts with existing state`),或创建/更新结果未确认(`Beneficiary not confirmed; query the original payeeId. Do not recreate.`) | 查原单或原 payeeId,不改参数重复提交,不换号重建 | | 500 | 系统异常(HTTP 200、JSON `code=500`,msg 多为「系统异常」):例如请求体无法按字段类型解析(如把 `cardExtra` 写成 JSON 对象或数组)、签名校验服务暂时不可用(`Signature verification temporarily unavailable`)。代付接口的 500 msg 为 `Payout request not confirmed; query original order`,含义见下文「代付下单的拒绝与 msg」 | 按结果不确定处理:保留原单号查单,不要换单号重付;修正请求格式后再处理。代付请先查询余额与原单 | | 900 | 并发重复请求:两个请求同时使用同一 nonce(`Request repeated`)。已验签过的 nonce 在 60 秒内再次使用返回 400 `nonce has been used`(第 3 节) | 每次请求使用新的 nonce、timestamp、sign | | 999 | 功能未实现:本单路由到的支付通道不支持该操作(平台配置问题,仅代收下单可能返回);订单可能已创建,但未提交支付通道 | 用原 merchantOrderNo 查单并联系平台;不要换单号重付 | | 9999 | 系统异常(`An error has occurred. Please try again later.`)。**当前部分下单参数不合规也可能返回 9999**:请求已通过通用校验(400、10006、10012、10021 等不会以 9999 返回),但在产品自身的校验中不合规。例如 AUD-VKDPWMRT 的 cardExtra 缺失或格式不对;clientIp 缺失(不传、空串、全空白、键名错误);payType 未传或不是 CARD;phone/email 非空但格式不符;returnUrl 缺失或不是符合要求的 HTTPS;金额不在 1–500 或不是整数/.99。平台正在改为具体业务码 | 不要按 msg 字面「稍后再试」直接重发,也不要换单号重付;按下文「9999 自查清单」处理 | | 10000 | 应用不存在 | 核对 appId,勿填商户号 | | 10001 | 应用不可用 | 联系平台核对应用状态 | | 10002 | 币种不可用 | 确认币种开通 | | 10003 | 商户不可用 | 核对商户状态 | | 10004 | 产品未开通或协议过期 | 同码可能有不同原因,结合 msg 核对签约 | | 10005 | 无匹配渠道 | 核对产品路由、通道状态、金额范围 | | 10006 | 金额不允许(`Amount not allowed`:超出路由金额范围或币种精度;代收低于路由最低金额时 msg 形如 `The minimum payable amount is 500.00.`;含代付金额低于最低金额(通道下限与签约下限中的较高者),代付接口 msg 为 `Payout request rejected`) | 以实际产品/路由范围、代付最低金额(第 4 节)和币种精度为准(第 4 节),不能只依赖英文提示 | | 10007 | 账户不可用 | 核对币种账户 | | 10008 | 可用余额不足 | 查询可用与冻结余额,预留手续费 | | 10009 | 通道暂不可用 | 原单查单;不要自动换单重付 | | 10010 | 来源 IP 不在白名单 | 提供真实服务器公网出口 IP(沙箱默认不校验,仅开启「模拟生产白名单」后出现) | | 10011 | 产品无效 | 核对代收/代付产品编码 | | 10012 | 支付方式无效(`payType` 与币种不匹配,或传了空字符串) | 对照第 6 节「支付方式编码」表,或省略 `payType` | | 10013 | 通道尚未接入:产品已配置但通道尚未接入,订单**未创建** | 视为该产品暂不可用,不要重试;联系平台确认开放时间 | | 10020 | 商户订单号重复:同一应用下该 `merchantOrderNo` 已存在(无论参数是否相同,第 4 节) | 查询原单,不生成新号重付 | | 10021 | 付款人 IP 不合规:`clientIp` 非空白但不是合法的 IPv4/IPv6 文本(含超过 45 个字符);或部分要求必填的产品(如印度原生 UPI)缺少可用的付款人 IP。注意:AUD-VKDPWMRT 缺失 `clientIp`(不传、`null`、空字符串、全空白、键名写错)当前返回 9999,不是 10021 | 填真实付款人公网 IPv4/IPv6 字面量,键名写作 `clientIp` | | 10022 | 代收准入未通过 | 联系平台确认当前准入规则 | | 10023 / 10024 | 代收 / 代付费率配置不完整 | 联系平台确认签约费用 | | 30001 | 订单未找到或不可访问 | 核对 appId 及订单号。代付下单返回 500 后原单号持续查单 30001,表示该笔未建单(见下文「代付下单的拒绝与 msg」);其他未知代付结果须人工核对 | | 30002 | 币种参数无效或账户未开通 | 同码多义,结合 msg 核对 | | 48001 | **仅沙箱**:模拟订单当前状态不允许手动终态或重发(`Sandbox order state does not allow this operation`) | 先查询模拟单状态,只对可手动场景操作 | | 47001 / 47014 / 47015 | 交易条款未配置 / 条款冲突 / 条款不支持(平台侧签约条款配置问题,仅在平台为商户启用新结算条款后可能由代收下单返回);订单**未创建** | 不要重试;联系平台核对签约条款 | | 429 / 限流 | 请求过快:查询接口按 appId 每秒超过 5 次(当前为 HTTP 200、JSON `code=429`,msg 形如 `Query limit 5 times in 1s`) | 降低频率,使用退避 | | HTTP 5xx、超时、非 JSON | 结果不确定 | 保存原单号并查单,禁止盲目重复付款 | 错误表为常见交易错误,不包含平台内部管理接口。不要依赖返回的英文文本完全一致。 ### 代付下单的拒绝与 msg 代付类接口(代付下单、代付查单、余额查询、收款人接口)为避免回显收款人资料,会改写错误 msg。下单被拒时按下表判断订单是否已创建: 自 平台代付参数校验新版本上线(上线时间以平台通知为准) 起,代付请求参数校验失败**同步返回、不建单**:HTTP 状态 仍为 200(与所有商户接口一致,结果看响应体),JSON `code=400`,msg 格式 `Invalid payout request: [; ...]`——`` 为本接口字段名,`` 为违反的规则(规则中的数字是平台或通道配置的上下限),**不回显字段值**。适用范围:所有币种的代付下单 `POST /transaction/payout/v1/apply`(商户后台代付相同);THB 代付另加 16.4 的付款通道字段规则。多个字段同时出错时全部列出,用 `; ` 分隔,通用字段按字段名字母序,THB 字段按 bizCode、branchCode、account、accountName、phone、email、amount 顺序;同一字段只报一条。 | 校验项 | msg(` ` 部分) | |---|---| | 必填字段缺失或为空(`merchantOrderNo`、`amount`、`currency`、`product`、`accountName`、`account`、`phone`、`email`、`payCode`) | ` required`(如 `email required`、`payCode required`);`amount` 不大于 0 时为 `amount must be greater than 0` | | 字段超长(`merchantOrderNo` > 255、`product` > 20、`bankName` > 255) | `merchantOrderNo too long (max 255)`、`product too long (max 20)`、`bankName too long (max 255)` | | `bizCode` / `branchCode` 不满足免填条件(第 7 节) | `bizCode required`、`branchCode required` | | THB:`bizCode` 不是 `BANK` | `bizCode must be BANK` | | THB:`branchCode` 不在 16.1 表 | `branchCode not supported (allowed: BAY, BBL, ...)`,allowed 按字母序列出当前可用银行 | | THB:`branchCode` 不在开通通知列出的可用银行内 | 同上:`branchCode not supported (allowed: ...)`,allowed 只含开通通知列出的可用银行(多个付款通道时取交集) | | THB:`account` 不是 6–20 位数字 | `account must be 6-20 digits` | | THB:`accountName` 含不允许的字符或超过开通通知的长度上限 | `accountName required`、`accountName too long (max 10)`(上限为通道配置,默认 10)、`accountName invalid characters (letters, spaces and . ' - only, starting with a letter)` | | THB:`phone` 格式不符 | `phone required`、`phone must be 6-15 digits with an optional leading +` | | THB:`email` 格式不符或超长 | `email required`、`email too long (max 64)`、`email invalid format`(部分付款通道要求,按交集执行,请始终按 16.4 填写) | | THB:`amount` 超出付款通道单笔范围 | `amount out of range (500.00-49999.00)`(范围为通道配置) | | 情况 | 返回 | 订单 | 处理 | |---|---|---|---| | 上表参数校验失败 | `code=400`,msg `Invalid payout request: ` | **确定**未创建,未冻结资金,`merchantOrderNo` 未被占用 | 按 msg 指出的字段修正后重新提交(可沿用原 `merchantOrderNo`,也可用新单号) | | 请求体不是 JSON 对象或字段类型错误(如 `amount` 不是数字) | `code=400`,msg `Invalid payout request: body must be a JSON object with correctly typed fields` | 未创建 | 修正请求格式后重提 | | THB:本次可能路由到的付款通道未开通或配置不完整 | 10005,msg `Payout request rejected` | 未创建 | 联系平台 | | 业务规则拒绝:10004、10005、10006、10008、10010、10011、10013、10020 等 | 对应业务码,msg 仍为 `Payout request rejected` | 未创建 | 按第 11 节错误表处理 | | 平台内部异常(数据库、提交阶段异常、超时等),以及缺少签名请求头或 `Content-Type` 不是 `application/json` | 业务码 500,msg `Payout request not confirmed; query original order` | **可能已建单** | 先补齐请求头;保留原单号查单,查到订单按查单结果处理,持续 30001 时联系平台确认后再处理,不要直接换单号重提 | | 超时、断线、HTTP 5xx、非 JSON | — | 不确定 | 同第 12 节,只查原单 | 说明:`code=400` 表示**本次请求未建单**;同号旧单可能已存在(重复单号仍按 10020),返回 500 时须先用原单号查单。THB 代付的金额精度与最低金额仍按原规则返回 10006(在 THB 字段规则之后检查)。已启用业务幂等的产品,同单号同参数重放直接返回原订单、不再校验;其他重复单号仍返回 10020。批量代付和平台人工改派不经过同步校验,仍可能以失败终态结束。以前参数错误的 msg 固定为 `Invalid payout request`,现在其后追加 `: <详情>`:按 `code` 或该前缀判断的客户端不受影响,按整串相等判断的请改为前缀匹配。上游付款通道的处理结果仍为异步,超时等未知结果按 16.4 「超时不等于失败」处理,不会变成 400。 ### 9999 自查清单 收到 9999 时: 1. **先用原 `merchantOrderNo` 查单**(第 8 节),不要换单号重付。查单返回 30001(持续查无)说明本次未建单;返回 `payStatus=0` 说明订单已创建。对下面清单中的参数问题,平台在提交支付通道之前就会取消该订单,不会生成支付链接(9999 响应中也没有 `payUrl`),付款人无法对该单付款;查单接口目前不区分“已取消”与“待支付”,两者都显示 `payStatus=0`。 2. 按下表逐项核对请求。下表只列通过通用校验后才会检查的项目;不满足通用校验的请求返回 400、10006、10012、10021 等原有业务码,不会是 9999(以 AUD-VKDPWMRT 为例,格式详见第 6 节「澳洲银行卡(AUD-VKDPWMRT)」)。 3. 确认属于清单中的参数问题并修正后,如需重新下单,使用**新的** `merchantOrderNo`:原单号已被占用,用同一单号、同样参数再次提交可能返回 `code=0` 但不含可用 `payUrl` 的原单受理结果,参数改动后再提交返回 409(第 4 节)。 4. 清单全部核对无误仍收到 9999,或无法确定原因时,按第 6 节「下单结果不确定时」处理:保留原单号、联系平台核对原单,不要自行换单号重付。 | 自查项 | 合规要求 | |---|---| | `cardExtra` 是否存在且为字符串 | 必填;值是序列化后的 JSON 文本,不是嵌套对象或数组(写成对象或数组可能返回 500) | | `cardExtra` 顶层键 | 恰好 `connection_info`、`account_info` 两个对象,无其他键 | | `cardExtra` 内部键与值 | 只用第 6 节表中的键,必填键齐全;`country`=`"AU"`、`state` 为 8 个州/领地缩写之一、`zip_code` 4 位数字、`area_code`=`"61"`、`role` 为 `GUEST` 或 `PRIVATE`;`create_time` 为 JSON 整数毫秒时间戳且不晚于当前时间;名/姓只含英文字母、空格、`'`、`-` | | `cardExtra` 内容限制 | 无空值、无控制字符、无卡号/CVV 字样或疑似卡号;总长不超过 8192 字节 | | `clientIp` | 键名精确为 `clientIp`;值不是空串或全空白,是真实付款人 IP 字面量(IPv4 直接写点分形式);放在请求体中 | | `payType` | 显式传 `CARD` | | `phone` | 5–15 位纯数字,不带 `+`、空格、横线、国家码 | | `email` | 形如 `payer@example.com`,不超过 254 个字符 | | `returnUrl` | 必填,`https://`,主机名含点且非 localhost,端口省略或 443,不超过 512 个字符 | | 金额 | 1–500 AUD,整数或 .99 结尾(如 `"20"`、`"20.00"`、`"19.99"`) | | 用户身份绑定 | 同一 `merchant_user_id` 始终对应同一 email 和 phone,email、phone 也未被其他 `merchant_user_id` 使用 | ### 16.5 THB 错误码速查 | 情况 | 返回 | 处理 | |---|---|---| | 缺少 `name`、`phone`、`email` 等必填字段,或传空字符串 | 400:代收 msg 形如「请求参数不正确:email invalid」;代付 msg 见第 11 节「代付下单的拒绝与 msg」 | 补齐字段后用新单号下单 | | 金额超出单笔范围或签约路由范围、超过两位小数 | 10006 | 按 16.2 修正金额;不要只依赖 msg | | THB 未开通、产品未签约 | 10002 / 10004 / 10011 | 核对开通通知与签约产品 | | 没有可用的 THB 路由 | 10005 | 联系平台 | | 代收传了 `payType` 空字符串或与币种不匹配的编码 | 10012 | 省略 `payType` | | 代收本节字段不合规(`cardExtra`、户名、手机号、邮箱、`returnUrl`、单笔范围) | 9999(订单未提交) | 按 16.3 自查清单修正,用新单号下单 | | 代收结果未知 | 9999(订单处理中)或 HTTP 5xx/超时 | 原单查单,不要重下 | | 代收付款通道明确未受理 | 10009 | 不要重试原单,联系平台 | | 代付可用余额不足(含手续费) | 10008 | 查询 THB 余额后再处理 | | 代付来源 IP 不在白名单 | 10010 | 提供真实服务器公网出口 IP | | 代付本节字段不合规 | 参数校验错误(见第 11 节「代付下单的拒绝与 msg」),本次请求未建单 | 按 msg 修正后重提(可沿用原单号) | | THB 付款通道未开通或配置不完整 | 10005 | 联系平台 | | 代付 500 | 结果未知,可能已建单 | 原单查单,不要换单号重付 | | 代付结果未知 | `payStatus=0` 持续 | 等待平台核对,不要重复代付 | 其他错误码含义见第 11 节。 ## 12. 重复请求、超时和对账 **代付超时是结果未知,不是付款失败。** 商户请求超时、HTTP 5xx、断线、非 JSON,或上游超时后平台仍返回处理中时,下游应保留待查状态。已受理订单在未确认终态前保留相应资金占用,不应因等待时间长就退款、解冻、重发或改走其他渠道。 1. 保存原 `merchantOrderNo`、已取得的 `orderNo`、请求金额和时间,使用原订单标识查单。每次查询生成新的签名头。 2. `payStatus=0` 表示仍待查;按退避间隔查询,等待验签通过的终态通知,超过内部跟踪时限转人工核对。 3. 一次“查无此单”(例如 30001)不能证明上游没有受理;先确认 appId 和订单号,持续核查或联系平台,禁止立即换号重发。 4. `payStatus=1` 确认成功,停止重发;仅在原单明确终态失败并完成资金核对后,才可按新的业务审批另建新单。不能对失败单或未知单使用原商户订单号重新提交。 5. 重复通知和查询结果共用同一幂等处理流程;后到的处理中或超时结果不能覆盖已经确认的成功状态。 ```text 创建本地业务订单 → 持久化商户单号 → 提交平台 ├─ 受理成功 → 保存平台单号 → 等回调 / 低频查单 ├─ 超时或未知 → 保留同一商户单号 → 查单 / 人工核对 └─ 明确参数拒绝 → 修正配置;确认原单未执行后再处理 回调验签 → 校验归属/金额 → 数据库事务幂等更新 → 返回 SUCCESS ``` 商户至少保存:appId、业务类型、商户单号、平台单号、币种、订单金额、实际金额、状态、请求/回调时间及验签结果。日志不要记录 appSecret、完整银行账户或包含密钥的待签名串。 每日核对本地订单与平台订单,分别核对代收实际到账、代付金额及签约费用。差异单、未决单、冻结金额单独跟踪。历史记录不能因停用或删除商户而丢失;资金未处理完成时不应直接清账删除。 ## 13. 联调与上线清单 1. 使用平台分配的应用和产品,先运行示例默认的余额查询,验证网络、时间戳及签名。 2. 校验必填手机号、邮箱等信息。使用受控的小额业务,金额仍需符合产品/接口最低要求。 3. 验证代收创建、付款、查单、成功回调、重复回调幂等,以及不正确签名被拒绝。 4. 单独验证代付来源 IP、余额及费用、受理/成功/失败/未知状态;生产代付测试须明确收款账户和资金用途。 5. 验证通知失败后的重试、服务重启后的订单恢复,以及对账差异处理。 6. 对需要 `cardExtra` 等扩展信息的产品,逐项核对产品目录「说明」和第 6 节;托管收银台还要验证付款后未跳回、链接过期和下单结果不确定时的处理。 7. USDT 代收开放后,验证 `paymentInfo` 展示、地址过期、4 位小数金额、含 `txHash` 的通知验签,以及按 `realAmount` 入账。 8. THB 代收或代付开通后,按第 16.6 节补充检查。 9. 只有完成以上验收,才能把“接口可以访问”提升为“业务链路已验证”。 [下载 OpenAPI 描述](openapi.json) · [Python 签名与验签示例](examples/client.py) · [Node.js 签名与验签示例](examples/client.mjs) ## 14. 查询状态扩展与沙箱自测 查询返回 `paymentStatus`、`isFinal` 和可选 `failureReason`(代收另有可选 `failureCode`,见第 8 节);原 `payStatus` 的值和含义不变。`failureReason` 仅供展示和排查,可能是大写代码(如 `PAYOUT_FAILED`)也可能是英文短句,不要据此做程序判断。客户端应忽略未知响应字段,金额用Decimal解析;这次不改变请求或回调签名规则。下单返回code=0只表示受理。 | paymentStatus | isFinal | 含义 | |---|---|---| | CREATED | false | 已创建,未确认支付结果 | | PROCESSING | false | 处理中 | | PENDING_CONFIRMATION | false | 代收金额等信息待核对,不自动入账 | | SUCCEEDED | true | 已确认成功,仍需核对订单、币种和realAmount | | CANCELLED | false | 平台侧已取消。一般情况下本地取消不能证明上游未扣款,failureReason=CANCELLATION_NOT_PAYMENT_PROOF,保留待核对;例外:下单时返回 9999、且订单在提交付款通道前就因参数不合规被取消(如 THB 代收,见 16.3),此时付款人无法付款,可修正后用新单号下单 | | FAILED | true | 明确失败:代付 failureReason=PAYOUT_FAILED;代收在通道明确确认失败时给出(沙箱为 SANDBOX_SIMULATED_FAILURE) | | UNKNOWN | false | 未知状态,保留待核对,不自动重付 | 代收旧payStatus成功为1,部分通道明确失败为2,其余为0(包括部分 `FAILED` 订单,请以 `paymentStatus`/`isFinal` 为准);代付成功1、失败2、其他0。不要把所有payStatus=0判为失败,也不要将所有取消判为支付最终失败。代收失败通知只在部分通道发送(第 10 节「代收失败通知」)。沙箱100.01失败场景可通过FAILED/isFinal=true与100.02持续处理区分;正式渠道未确认失败仍保留待核对状态。 生产与沙箱共用本开发文档和签名示例。[沙箱测试指南](sandbox-guide.html)、商户后台沙箱说明从同一份版本化契约生成。沙箱产品和余额独立,不代表正式产品开通;代付金额范围以指南的环境差异说明为准。 [下载可运行沙箱工具](examples/quickstart.zip):余额、服务器出口IP、下单、查单、回调验签、SQLite幂等入账演示、本地重复通知测试和结果报告。按指南配置独立沙箱凭据与自己的公网HTTPS通知地址;不使用生产密钥、不连接真实钱包。 ## 15. AUD Google Pay 与 Apple Pay 完整请求示例 本节用于已签约这两种 AUD 钱包产品的商户。沿用当前应用的 appId/appSecret;应用必须已绑定对应产品。产品编码中的字符属于完整编码,请原样传递。本文示例不会替商户开通产品,也不代表所有商户都已开通。 | 支付方式 | product | payType | |---|---|---| | Google Pay | `AUD-PAYPLUS-GOOGLE-M85` | `GOOGLE_PAY` | | Apple Pay | `AUD-PAYPLUS-APPLE-M85` | `APPLE_PAY` | 正式接口:`POST https://transaction.hisspay.com/transaction/payin/v1/apply`,Content-Type 为 `application/json`。请求头需 appId、timestamp、nonce、sign。timestamp 为当前13位毫秒时间戳,允许时间差30秒;nonce 每次重新生成且至少10字符。 金额为1–500 AUD,目前仅整数或 .99,例如1.00、1.99、19.99。示例金额使用字符串,避免签名时浮点格式变化。具体商户费率与结算周期以签约协议为准。 必须替换示例中的姓名、邮箱、电话、真实付款人公网IP、客户ID、注册时间、州、邮编、订单号及回调/跳转URL。clientIp不是商户服务器IP。澳大利亚资料country=AU、area_code=61,州为NSW/VIC/QLD/ACT/NT/SA/TAS/WA,邮编四位;电话仅数字。create_time为客户真实注册时间的13位毫秒整数,不晚于当前时间;注册用户role=PRIVATE,游客GUEST。 cardExtra 是 JSON **字符串**,不是嵌套对象;只序列化一次,签名后不重新格式化。不要传银行卡号、CVV或钱包密码。姓名、邮箱、电话通过顶层字段传递,cardExtra.account_info中不要重复添加email/phone。 ### Google Pay 完整请求体 [下载 Google Pay JSON](examples/google-pay-request.json) ```json { "merchantOrderNo": "WALLET_GOOGLE_REPLACE_UNIQUE", "amount": "1.00", "currency": "AUD", "product": "AUD-PAYPLUS-GOOGLE-M85", "payType": "GOOGLE_PAY", "name": "John Smith", "phone": "412345678", "email": "john@example.com", "clientIp": "", "notifyUrl": "https://your-domain.com/payment/notify", "returnUrl": "https://your-domain.com/payment/result", "cardExtra": "{\"connection_info\":{\"country\":\"AU\",\"state\":\"NSW\",\"zip_code\":\"2000\",\"media_source\":\"website\"},\"account_info\":{\"merchant_user_id\":\"customer_10001\",\"create_time\":1790812800000,\"role\":\"PRIVATE\",\"area_code\":\"61\",\"first_name\":\"John\",\"last_name\":\"Smith\"}}" } ``` ### Apple Pay 完整请求体 [下载 Apple Pay JSON](examples/apple-pay-request.json) ```json { "merchantOrderNo": "WALLET_APPLE_REPLACE_UNIQUE", "amount": "1.00", "currency": "AUD", "product": "AUD-PAYPLUS-APPLE-M85", "payType": "APPLE_PAY", "name": "John Smith", "phone": "412345678", "email": "john@example.com", "clientIp": "", "notifyUrl": "https://your-domain.com/payment/notify", "returnUrl": "https://your-domain.com/payment/result", "cardExtra": "{\"connection_info\":{\"country\":\"AU\",\"state\":\"NSW\",\"zip_code\":\"2000\",\"media_source\":\"website\"},\"account_info\":{\"merchant_user_id\":\"customer_10001\",\"create_time\":1790812800000,\"role\":\"PRIVATE\",\"area_code\":\"61\",\"first_name\":\"John\",\"last_name\":\"Smith\"}}" } ``` ### Python 3 请求示例(标准库,无额外依赖) [下载 Python 示例](examples/wallet_request.py)。先下载任一请求JSON并替换示例资料。密钥从服务端环境变量HISSPAY_APP_ID、HISSPAY_APP_SECRET读取,不填入前端代码或仓库。 默认仅打印请求体,不发送订单: ```bash python3 wallet_request.py google-pay-request.json python3 wallet_request.py apple-pay-request.json ``` 准备好真实资料与唯一订单号后,选择一种方式发送一次正式请求。`--send`会创建真实支付订单: ```bash python3 wallet_request.py google-pay-request.json --send # 或:python3 wallet_request.py apple-pay-request.json --send ``` ```python #!/usr/bin/env python3 """Python 3, standard library. Dry run by default; --send creates ONE real order.""" import hashlib import json import os import secrets import sys import time import urllib.request from pathlib import Path URL = "https://transaction.hisspay.com/transaction/payin/v1/apply" def sign(body, app_id, secret, timestamp, nonce): headers = {"appId": app_id, "timestamp": timestamp, "nonce": nonce} values = {**body, **headers} if any(not isinstance(v, str) for v in values.values()): raise ValueError("Use string top-level fields; cardExtra must be JSON text") canonical = "&".join(k + "=" + values[k] for k in sorted(values)) headers["sign"] = hashlib.sha256((canonical + secret).encode("utf-8")).hexdigest() return headers class NoRedirect(urllib.request.HTTPRedirectHandler): def redirect_request(self, *args, **kwargs): return None def main(): if len(sys.argv) not in (2, 3) or (len(sys.argv) == 3 and sys.argv[2] != "--send"): raise SystemExit("Usage: python3 wallet_request.py request.json [--send]") body = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) if len(sys.argv) == 2: print(json.dumps(body, ensure_ascii=False, indent=2)) return # Replace ALL example customer data, URLs and order number before --send. if body.get("clientIp") == "" or "REPLACE_UNIQUE" in body.get("merchantOrderNo", ""): raise SystemExit("Replace example payer IP and unique order number first") headers = sign(body, os.environ["HISSPAY_APP_ID"], os.environ["HISSPAY_APP_SECRET"], str(int(time.time() * 1000)), secrets.token_hex(16)) headers["Content-Type"] = "application/json" request = urllib.request.Request(URL, data=json.dumps(body, ensure_ascii=False, separators=(",", ":")).encode("utf-8"), headers=headers, method="POST") # No automatic application retry or redirects. On timeout, query the original order. with urllib.request.build_opener(NoRedirect).open(request, timeout=30) as response: print(response.read().decode("utf-8")) if __name__ == "__main__": main() ``` ### Java 17+ 请求示例(JDK标准库,无额外依赖) [下载 Java 示例](examples/WalletRequest.java) · [下载付款人参数模板](examples/wallet-payer.properties)。将文件放在同一目录,编辑模板中的全部示例数据;两种钱包分别使用不同的唯一订单号。环境变量HISSPAY_APP_ID、HISSPAY_APP_SECRET与Python一致。 ```bash javac WalletRequest.java # 以下仅打印JSON,不请求网络: java WalletRequest google wallet-payer.properties java WalletRequest apple wallet-payer.properties # 准备好真实资料后,选择一种方式发送一次正式请求: java WalletRequest google wallet-payer.properties --send # 或:java WalletRequest apple wallet-payer.properties --send ``` ```java import java.net.URI; import java.net.http.*; import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.security.MessageDigest; import java.time.Duration; import java.util.*; // Java 17+, no external dependencies. Default: print request only. public class WalletRequest { static final String URL = "https://transaction.hisspay.com/transaction/payin/v1/apply"; static String quote(String s) { StringBuilder b = new StringBuilder("\""); for (char c : s.toCharArray()) { switch (c) { case '"': b.append("\\\""); break; case '\\': b.append("\\\\"); break; case '\n': b.append("\\n"); break; case '\r': b.append("\\r"); break; case '\t': b.append("\\t"); break; default: if (c < 32) b.append(String.format("\\u%04x", (int)c)); else b.append(c); } } return b.append('"').toString(); } static String json(Map values) { StringJoiner j = new StringJoiner(",", "{", "}"); values.forEach((k, v) -> j.add(quote(k) + ":" + (v instanceof Map ? json((Map)v) : v instanceof Long ? v.toString() : quote((String)v)))); return j.toString(); } static String required(Properties p, String k) { String v = p.getProperty(k); if (v == null || v.isBlank()) throw new IllegalArgumentException("Missing " + k); return v; } static Map body(String wallet, Properties p) { if (!wallet.equals("google") && !wallet.equals("apple")) throw new IllegalArgumentException("Use google or apple"); Map connection = new LinkedHashMap<>(); connection.put("country", "AU"); connection.put("state", required(p, "state")); connection.put("zip_code", required(p, "zip")); connection.put("media_source", "website"); Map account = new LinkedHashMap<>(); account.put("merchant_user_id", required(p, "userId")); account.put("create_time", Long.parseLong(required(p, "created"))); account.put("role", required(p, "role")); account.put("area_code", "61"); account.put("first_name", required(p, "first")); account.put("last_name", required(p, "last")); Map context = new LinkedHashMap<>(); context.put("connection_info", connection); context.put("account_info", account); Map b = new LinkedHashMap<>(); for (String k : List.of("merchantOrderNo", "amount", "name", "phone", "email", "clientIp", "notifyUrl", "returnUrl")) b.put(k, required(p, k)); b.put("currency", "AUD"); b.put("product", wallet.equals("google") ? "AUD-PAYPLUS-GOOGLE-M85" : "AUD-PAYPLUS-APPLE-M85"); b.put("payType", wallet.equals("google") ? "GOOGLE_PAY" : "APPLE_PAY"); b.put("cardExtra", json(context)); // String, NOT a nested JSON object. return b; } static Map sign(Map body, String appId, String secret, String timestamp, String nonce) throws Exception { Map headers = new LinkedHashMap<>(); headers.put("appId", appId); headers.put("timestamp", timestamp); headers.put("nonce", nonce); TreeMap values = new TreeMap<>(body); values.putAll(headers); StringJoiner canonical = new StringJoiner("&"); values.forEach((k, v) -> canonical.add(k + "=" + v)); byte[] digest = MessageDigest.getInstance("SHA-256").digest((canonical.toString() + secret).getBytes(StandardCharsets.UTF_8)); headers.put("sign", HexFormat.of().formatHex(digest)); return headers; } public static void main(String[] args) throws Exception { if (args.length < 2 || args.length > 3 || (args.length == 3 && !args[2].equals("--send"))) throw new IllegalArgumentException("Usage: java WalletRequest google|apple payer.properties [--send]"); Properties p = new Properties(); try (var reader = Files.newBufferedReader(Path.of(args[1]), StandardCharsets.UTF_8)) { p.load(reader); } Map b = body(args[0], p); String payload = json(b); if (args.length == 2) { System.out.println(payload); return; } if (b.get("clientIp").equals("") || b.get("merchantOrderNo").contains("REPLACE_UNIQUE")) throw new IllegalArgumentException("Replace example payer IP and unique order number first"); String appId = Objects.requireNonNull(System.getenv("HISSPAY_APP_ID"), "HISSPAY_APP_ID"); String secret = Objects.requireNonNull(System.getenv("HISSPAY_APP_SECRET"), "HISSPAY_APP_SECRET"); Map headers = sign(b, appId, secret, Long.toString(System.currentTimeMillis()), UUID.randomUUID().toString().replace("-", "")); HttpRequest.Builder req = HttpRequest.newBuilder(URI.create(URL)).timeout(Duration.ofSeconds(30)) .header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(payload, StandardCharsets.UTF_8)); headers.forEach(req::header); HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).followRedirects(HttpClient.Redirect.NEVER).build(); // Send once. On timeout/error, query the original order; do not automatically resubmit. HttpResponse response = client.send(req.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); System.out.println("HTTP " + response.statusCode()); System.out.println(response.body()); } } ``` ### 签名与结果处理 两个示例都将请求体字段与appId、timestamp、nonce合并,按字段名升序拼接key=value,以&连接,末尾直接追加appSecret,不加分隔符、不做URL编码;UTF-8 SHA-256输出小写十六进制。cardExtra使用原字符串值参与签名,而不是外层JSON转义后的文本。 下单响应code=0且data.payUrl有效时,打开data.payUrl托管收银台,不使用iframe。下单受理不等于付款成功,returnUrl仅用于页面跳转;最终按服务端通知或查单确认。按第10节验证下游通知签名、订单号、金额和币种,再幂等入账。 超时、网络断开或结果不确定时保留原merchantOrderNo,先查询原单,不换单号重复下单。查询接口为POST /transaction/payin/v1/query,请求体例如 {"merchantOrderNo":"原商户订单号"},使用同样签名规则和新的timestamp、nonce。成功创建订单也不能证明真实钱包付款与入账验收完成。 ## 16. 泰国(THB)代收与代付 **开通状态(2026-10-27)**:THB 代收(PromptPay 扫码)已按商户逐个开通,收到开通通知(含产品编码)后即可接入,产品编码见第 1 节目录;**THB 代付需单独开通,以开通通知为准,未收到开通通知前不要调用 THB 代付**(未开通时下单会被拒绝)。 本节是 THB 代收、代付在**本接口**上的统一接入要求:平台为您开通的所有 THB 代收产品使用同一套字段规则,所有 THB 代付产品也一样,平台调整或增加付款通道时您不需要改代码。本节**不表示该产品已向您的应用开放**:产品编码、单笔金额范围、费率、结算周期以及可用银行以平台开通通知为准,未开通前不要下单。签名、请求头、nonce、通用字段、查单、余额和通知验签规则不变,见第 3–5 节、第 8–10 节和第 14 节。下文示例中的姓名、账号、手机号和域名均为虚构值。 ### 16.1 银行编码 THB 代收的 `cardExtra.payerBankCode` 与 THB 代付的 `branchCode` 使用同一套编码,只能取下表的值(区分大小写,不加前缀或后缀)。表外的值或其他写法都会被拒绝。 | 银行编码 | 银行 | 代收 | 代付 | |---|---|---|---| | `SCB` | Siam Commercial Bank(汇商银行) | 支持 | 支持 | | `KBANK` | Kasikornbank(开泰银行) | 支持 | 支持 | | `KTB` | Krungthai Bank(泰京银行) | 支持 | 支持 | | `BAY` | Bank of Ayudhya / Krungsri(大城银行) | 支持 | 支持 | | `GSB` | Government Savings Bank(政府储蓄银行) | 支持 | 支持 | | `TTB` | TMBThanachart Bank(TTB 银行) | 支持 | 支持 | | `BBL` | Bangkok Bank(盘谷银行) | 不支持 | 支持 | 代收付款银行只能是表中「代收」为「支持」的 6 家。代付实际可用的收款银行**按付款通道配置,以开通通知列出的为准**:平台只对已与付款通道逐家确认的银行出款,可能少于 7 家;通道未确认任何银行时代付不可用。使用未列出的银行编码时不会付款(返回形式见 16.4)。 ### 16.2 金额、范围与费用 - `currency` 固定为 `THB`,订单币种必须与签约的 THB 产品一致。 - `amount` 为泰铢主币种金额,必须大于 0,最多两位小数,如 `"1500"`、`"1500.00"`、`"1500.50"`。超过两位小数返回 10006。 - **单笔金额范围**:代收与代付默认均为 **500.00–49999.00 THB(含两端)**。开通通知给出更窄的范围时以通知为准。超出签约路由范围的请求同步返回 10006:代收 msg 形如 `The minimum payable amount is 500.00.`(即使是超过上限也可能显示这条最低金额文案,不要依赖 msg 判断上下限);代付 msg 统一为 `Payout request rejected`(第 11 节)。平台在提交付款通道前还会再核对一次通道的单笔范围:代收不满足时返回 9999 且订单不会提交(见 16.3),代付不满足时的返回形式见 16.4。 - 平台按两位小数精确核对金额:付款金额或付款通道确认金额与订单金额不一致的订单,不会自动入账或出款,保留待人工核对(查单 `paymentStatus` 为 `PENDING_CONFIRMATION` 或保持处理中)。THB 代收成功时 `realAmount` 等于 `amount`(沙箱尾数 .06 场景故意返回不同的 `realAmount`,用于演练第 4 节的差额处理,生产 THB 不会出现)。 - **代收费用**:`realAmount` 是扣费前的实际收款金额;商户 THB 账户的结算净额按您签约的 THB 代收费率与固定费用计算(第 4 节「实际到账金额规则」)。 - **代付费用**:收款人应收金额就是请求中的 `amount`,手续费不从 `amount` 中扣减。受理时平台从您的 THB 可用余额中占用 `amount + amount × 代付费率(向上保留 4 位小数)+ 单笔代付费用`,费率与单笔费用以您的 THB 代付签约为准;成功后按此扣款,失败按账务规则释放。可用余额不足时返回 10008,下单前请用第 9 节查询 THB 可用余额并预留手续费。 ### 16.6 THB 余额、联调与上线检查 - 余额查询使用第 9 节接口,传 `{"currency":"THB"}`;返回的是商户在平台的 THB 账户余额。 - 沙箱提供 THB 代收与代付模拟产品,字段校验按本节统一契约(代付户名上限、可用银行及不合规时的返回形式与生产当前默认一致,见沙箱指南),按金额尾数模拟成功、失败、处理中、超时、重复通知等场景,详见[沙箱测试指南](sandbox-guide.html)「泰国 THB 模拟」。沙箱模拟不连接银行,不产生真实付款。 - 上线前按第 13 节清单验证,并额外确认:代收 `cardExtra` 字符串与签名一致、户名与付款账号一致、16.1 编码映射正确、不传 `payType`;代付金额、手续费与可用余额核对正确,使用真实收款账户的小额测试并确认成功与失败两种通知的处理;代付超时或长期处理中时不重复出款。