# 商户 OpenAPI 接入文档

- Base URL：请登录商户后台「安全中心 · OpenAPI」获取你的实际接入地址（本文示例中以 `<BASE_URL>` 占位）。
- 接口版本：`v1`（服务端版本 1.2.0；每次响应都带 `X-Api-Version` 头）
- 导出日期：20260620

> 注意：本文档不包含商户真实凭据与白名单信息，请使用占位符替换后对接。

## 版本与兼容策略

- 大版本体现在路径前缀：当前为 `/api/open/v1`。
- v1 仅做**向后兼容**的演进（新增可选字段、修复缺陷），不会改变已有字段含义或收紧已有校验。
- 若有破坏性变更，会新增 `/api/open/v2` 并保留 v1 一段时间，商户可平滑迁移。
- 可调用 `GET <BASE_URL>/version` 查询当前服务端版本（无需鉴权）。

**本次变更（1.2.0）**：
- 代收对外 `status` 不再返回 `expired`：超时未支付统一归一为 `pending`（处理中），请以查单结果为准。
- 代收仅在订单成功（`success`）时回调商户；失败/超时等非成功状态不回调，商户应通过查单接口获知最终结果（代付不受此限制）。

## 商户信息

- 商户名称：<YOUR_MERCHANT_NAME>
- 商户号：<YOUR_MERCHANT_NO>

## 代收：创建订单（pay/create）

- 方法：`POST`
- 接口：`<BASE_URL>/merchant/pay/create`
- 用途：创建代收订单，返回平台订单号与支付链接/二维码内容。

### 请求说明

- Content-Type：`application/json`
- 必填字段：merchant_no、api_key、timestamp、sign、out_order_no、amount、currency、pay_method、notify_url
- 可选字段：nonce、country、return_url、subject、remark、client_ip、extra

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| merchant_no | string | 是 | 商户号（从商户后台获取） |
| api_key | string | 是 | API Key（从商户后台获取） |
| timestamp | int | 是 | Unix 时间戳（秒），用于过期校验（建议 ±300 秒窗口） |
| nonce | string\|null | 否 | 防重放随机串；不使用时建议省略或传 null（不要传空字符串） |
| out_order_no | string | 是 | 商户订单号（幂等键；同一商户不可重复） |
| amount | int | 是 | 订单金额（最小单位整数，精度 0.0001；需要放大10000倍，例如 1.2345 传 12345）。必须是 JSON 数字正整数，不接受字符串（如 "12345"）或小数；非法返回 HTTP 400（code=400）。 |
| currency | string | 是 | 币种（如 PHP/USDT；用于校验与路由） |
| pay_method | string | 是 | 支付方式（由平台提供，如 gcash/maya；加密货币用链名如 trc20/erc20） |
| country | string\|null | 否 | 国家 ISO 码（如 PH；法币必填，加密货币可省略） |
| notify_url | string | 是 | 回调地址（订单终态时平台会向该地址推送结果） |
| return_url | string\|null | 否 | 前端回跳地址（支付完成后跳回商户页面；是否跳转取决于支付方式/场景） |
| subject | string\|null | 否 | 订单标题（可用于展示或对账说明） |
| remark | string\|null | 否 | 备注（可用于商户自定义说明/对账） |
| client_ip | string\|null | 否 | 终端用户 IP（可用于风控；不传则平台无法感知终端来源） |
| extra | object\|null | 否 | 扩展字段（顶层字段，参与签名；object 会按稳定 JSON 序列化）。⚠️ 强烈建议携带下单用户个人信息 extra.customer（见下方"扩展信息与渠道要求"说明） |
| extra.customer | object | 否 | 下单用户信息（选填）。平台侧不再因缺失而拒单（不再返回 300405）；但 gcash/maya 等主流支付方式的上游渠道实际强制要求 name/phone，缺失会被上游拒单导致订单失败，故强烈建议携带。提供时平台做格式校验并自动映射到各渠道格式（整名 name 自动拆为 first/last）：可填 name 整名（或 first_name+last_name）、email、phone |
| sign | string | 是 | 签名（按文档规则计算；pay 接口使用 api_secret_pay） |

#### 请求示例
```json
{
  "merchant_no": "<YOUR_MERCHANT_NO>",
  "api_key": "<YOUR_API_KEY>",
  "timestamp": 1736073600,
  "nonce": "random-xyz",
  "sign": "<SIGN>",
  "out_order_no": "202501010001",
  "amount": 10000,
  "currency": "PHP",
  "pay_method": "gcash",
  "country": "PH",
  "notify_url": "https://merchant.example.com/api/notify/pay",
  "return_url": "https://merchant.example.com/pay/result",
  "subject": "订单标题（可选）",
  "remark": "备注（可选）",
  "client_ip": "1.2.3.4",
  "extra": {
    "user_id": "u123456",
    "customer": {
      "first_name": "San",
      "last_name": "Zhang",
      "email": "user@example.com",
      "phone": "13800000000"
    }
  }
}
```

> 签名说明：sign 需对"请求 JSON 顶层除 sign 以外的所有字段"参与计算；object/array 字段会按稳定 JSON 序列化。

### 返回字段与示例

- 统一返回结构：`{ code, message, data }`；业务失败通常仍为 HTTP 200。

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| order_no | string | 是 | 平台订单号（用于后续查询与对账） |
| out_order_no | string | 是 | 商户订单号（原样回传，便于商户侧关联业务订单） |
| amount | int | 是 | 订单金额（最小单位整数，精度 0.0001；需要放大10000倍，例如 1.2345 传 12345） |
| currency | string | 是 | 币种（原样回传请求币种，如 PHP/USDT） |
| pay_url | string\|null | 否 | 支付链接（下单成功 code=0 时，pay_url/qrcode_content/pay_params 至少一项非空；本字段可能为空，此时用其余字段唤起支付） |
| qrcode_content | string\|null | 否 | 二维码内容（下单成功 code=0 时，pay_url/qrcode_content/pay_params 至少一项非空；本字段可能为空） |
| pay_params | string\|null | 否 | 支付原生串（可选）：部分支付方式返回 App 原生唤端参数；pay_url 为空而本字段非空时，请用它在客户端唤起支付 |
| expire_at | string\|null | 否 | 过期时间（ISO8601；可空）。超时未支付不再单独表示为 expired 状态，对外统一归一为 pending（处理中），请以查单结果为准。 |
| status | string | 是 | 订单状态：pending（处理中）/success（成功）/failed（失败）。仅 success/failed 为终态；超时未支付不再单独返回 expired，统一归一为 pending。 |

#### 返回示例
```json
{
  "code": 0,
  "message": "ok",
  "data": {
    "order_no": "P20250101000001",
    "out_order_no": "202501010001",
    "amount": 10000,
    "currency": "PHP",
    "pay_url": "https://pay.example.com/h5/P20250101000001",
    "qrcode_content": "https://pay.example.com/h5/P20250101000001",
    "pay_params": null,
    "expire_at": "2025-01-01T12:30:00Z",
    "status": "pending"
  }
}
```

### 补充说明

- 金额为最小单位整数，精度 0.0001；需要放大10000倍，例如 1.2345 传 12345，避免浮点误差。
- 幂等键：同一商户的 out_order_no 全局唯一；重复下单参数一致视为幂等成功，参数不一致返回幂等冲突。
- 上游未受理时接口实时返回 code=100000 且订单置 failed 终态；此时不可用相同 out_order_no 重试（幂等只会返回该 failed 单），需换新 out_order_no 重新下单。
- 【用户信息（强烈建议必传）】gcash/maya 等主流支付方式的上游渠道实际强制要求下单用户的姓名/手机（email 部分渠道可选），缺失会被上游拒单导致订单失败；平台侧不再以 300405 拦截（customer 选填，提供时做格式校验+整名自动拆分）。请用通用字段提供，平台会自动映射到各渠道所需格式（例如把整名拆成 first/last_name）：姓名——extra.customer.name 整名（也可显式 extra.customer.first_name+last_name）；邮箱——extra.customer.email（或 extra.email）；手机——extra.customer.phone（或 extra.phone）。这样商户只需接一套通用字段，无需为每个渠道单独适配。
- 【支付原生串】pay_url 为空而 pay_params 非空时（部分支付方式仅返回原生唤端参数），请在客户端用 pay_params 唤起支付。

### 常见错误

- 100101 请求过期
- 100102 IP 不在白名单
- 100104 签名错误
- 100105 IP 在黑名单
- 100000 创建订单失败（上游未受理，订单已实时置 failed）
- 300404 无可用支付方式渠道（country/currency/pay_method 未命中可用上游）
- 100001 参数校验失败（如 notify_url 指向内网/非法协议）
- 300402 上游配置不可用
- 300403 费率未配置

## 代收：查询订单（pay/query）

- 方法：`POST`
- 接口：`<BASE_URL>/merchant/pay/query`
- 用途：按平台订单号或商户订单号查询代收订单状态。

### 请求说明

- Content-Type：`application/json`
- 必填字段：merchant_no、api_key、timestamp、sign、order_no/out_order_no（二选一）
- 可选字段：nonce

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| merchant_no | string | 是 | 商户号（从商户后台获取） |
| api_key | string | 是 | API Key（从商户后台获取） |
| timestamp | int | 是 | Unix 时间戳（秒），用于过期校验（建议 ±300 秒窗口） |
| nonce | string\|null | 否 | 防重放随机串；不使用时建议省略或传 null（不要传空字符串） |
| order_no | string\|null | 否 | 平台订单号（与 out_order_no 二选一，至少提供一个） |
| out_order_no | string\|null | 否 | 商户订单号（与 order_no 二选一，至少提供一个） |
| sign | string | 是 | 签名（按文档规则计算；pay 接口使用 api_secret_pay） |

#### 请求示例
```json
{
  "merchant_no": "<YOUR_MERCHANT_NO>",
  "api_key": "<YOUR_API_KEY>",
  "timestamp": 1736073600,
  "nonce": "random-xyz",
  "sign": "<SIGN>",
  "out_order_no": "202501010001",
  "order_no": null
}
```

> 签名说明：sign 需对"请求 JSON 顶层除 sign 以外的所有字段"参与计算；object/array 字段会按稳定 JSON 序列化。

### 返回字段与示例

- 统一返回结构：`{ code, message, data }`；业务失败通常仍为 HTTP 200。

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| order_no | string | 是 | 平台订单号 |
| out_order_no | string | 是 | 商户订单号 |
| amount | int | 是 | 订单金额（最小单位整数，精度 0.0001；需要放大10000倍，例如 1.2345 传 12345） |
| currency | string | 是 | 币种 |
| status | string | 是 | 订单状态：pending（处理中）/success（成功）/failed（失败）。仅 success/failed 为终态；超时未支付不再单独返回 expired，统一归一为 pending。 |
| channel_order_no | string\|null | 否 | 渠道侧订单号（可空） |
| paid_at | string\|null | 否 | 支付成功时间（ISO8601；未成功时可能为空） |
| notify_status | string | 是 | 平台对商户回调推送状态：pending/success/failed（不等同于支付结果） |

#### 返回示例
```json
{
  "code": 0,
  "message": "ok",
  "data": {
    "order_no": "P20250101000001",
    "out_order_no": "202501010001",
    "amount": 10000,
    "currency": "USDT",
    "status": "success",
    "channel_order_no": "CH20250101XXX",
    "paid_at": "2025-01-01T12:15:00Z",
    "notify_status": "success"
  }
}
```

### 补充说明

- order_no/out_order_no 至少提供一个。
- status 终态为 success/failed；pending 表示处理中（含超时未支付，已统一归一为 pending）。
- 代收仅在订单成功（success）时回调商户；失败/超时等非成功状态不回调，商户应通过本查单接口获知最终结果。

### 常见错误

- 100104 签名错误
- 300301 订单不存在

## 代付：创建订单（payout/create）

- 方法：`POST`
- 接口：`<BASE_URL>/merchant/payout/create`
- 用途：创建代付订单并冻结余额（创建成功不代表最终出款成功）。

### 请求说明

- Content-Type：`application/json`
- 必填字段：merchant_no、api_key、timestamp、sign、out_payout_no、amount、currency、pay_method、notify_url、account_no
- 可选字段：nonce、country、account_name、bank_name、bank_code、remark、client_ip、extra

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| merchant_no | string | 是 | 商户号（从商户后台获取） |
| api_key | string | 是 | API Key（从商户后台获取） |
| timestamp | int | 是 | Unix 时间戳（秒），用于过期校验（建议 ±300 秒窗口） |
| nonce | string\|null | 否 | 防重放随机串；不使用时建议省略或传 null（不要传空字符串） |
| out_payout_no | string | 是 | 商户代付单号（幂等键；同一商户不可重复） |
| amount | int | 是 | 代付金额（最小单位整数，精度 0.0001；需要放大10000倍，例如 1.2345 传 12345）。必须是 JSON 数字正整数，不接受字符串（如 "12345"）或小数；非法返回 HTTP 400（code=400）。 |
| currency | string | 是 | 币种（如 PHP/USDT；用于校验与路由） |
| pay_method | string | 是 | 支付方式（由平台提供，如 gcash/maya；加密货币用链名如 trc20/erc20） |
| country | string\|null | 否 | 国家 ISO 码（如 PH；法币必填，加密货币可省略） |
| notify_url | string | 是 | 回调地址（代付终态时平台会向该地址推送结果） |
| account_name | string\|null | 否 | 收款人姓名/账户名（按通道类型解释；链上地址类可空） |
| account_no | string | 是 | 收款账号/地址（按通道类型解释；例如银行卡号/链上地址） |
| bank_name | string\|null | 否 | 银行/链名称（按通道类型解释；可空） |
| bank_code | string\|null | 否 | 银行编码（对应「查询可用银行」接口返回的 code，平台转译为上游编码并回填 bank_name；无银行选择时可省略；如同时传 bank_name，以 bank_code 解析结果优先） |
| remark | string\|null | 否 | 备注（可用于商户自定义说明/对账） |
| client_ip | string\|null | 否 | 终端用户 IP（可用于风控） |
| extra | object\|null | 否 | 扩展字段（参与签名；object 会按稳定 JSON 序列化）。⚠️ 强烈建议携带下单用户个人信息 extra.customer（见下方"扩展信息与渠道要求"说明） |
| extra.customer | object | 否 | 下单用户信息（选填）。平台侧不再因缺失而拒单（不再返回 300405）；但 gcash/maya 等主流支付方式的上游渠道实际强制要求 name/phone，缺失会被上游拒单导致订单失败，故强烈建议携带。提供时平台做格式校验并自动映射到各渠道格式（整名 name 自动拆为 first/last）：可填 name 整名（或 first_name+last_name）、email、phone |
| sign | string | 是 | 签名（按文档规则计算；payout 接口使用 api_secret_payout） |

#### 请求示例
```json
{
  "merchant_no": "<YOUR_MERCHANT_NO>",
  "api_key": "<YOUR_API_KEY>",
  "timestamp": 1736073600,
  "nonce": "random-xyz",
  "sign": "<SIGN>",
  "out_payout_no": "WD202501010001",
  "amount": 5000,
  "currency": "PHP",
  "pay_method": "gcash",
  "country": "PH",
  "notify_url": "https://merchant.example.com/api/notify/payout",
  "account_name": "张三（可选）",
  "account_no": "09171234567",
  "bank_name": "可选：银行卡代付时填银行名（gcash 等钱包类可省略）",
  "client_ip": "1.2.3.4",
  "remark": "商户提现（可选）",
  "extra": {
    "user_id": "u123456",
    "customer": {
      "first_name": "San",
      "last_name": "Zhang",
      "email": "user@example.com",
      "phone": "13800000000"
    }
  }
}
```

> 签名说明：sign 需对"请求 JSON 顶层除 sign 以外的所有字段"参与计算；object/array 字段会按稳定 JSON 序列化。

### 返回字段与示例

- 统一返回结构：`{ code, message, data }`；业务失败通常仍为 HTTP 200。

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| payout_no | string | 是 | 平台代付单号（用于后续查询与对账） |
| out_payout_no | string | 是 | 商户代付单号（原样回传，便于商户侧关联业务） |
| amount | int | 是 | 代付金额（最小单位整数，精度 0.0001；需要放大10000倍，例如 1.2345 传 12345） |
| currency | string | 是 | 币种（原样回传请求币种，如 PHP/USDT） |
| status | string | 是 | 订单状态：创建时为 pending（处理中，代付一律先进入处理流程）；success（成功）/failed（失败）为终态。 |
| review_status | string\|null | 否 | 出款审批状态（启用商户出款审批时）：pending（审批中）/approved（通过）/rejected（驳回）；无需审批时为 null。 |
| fee_amount | int\|null | 否 | 手续费（可空） |
| freeze_amount | int\|null | 否 | 冻结金额（可空；建议口径 amount + fee_amount） |

#### 返回示例
```json
{
  "code": 0,
  "message": "ok",
  "data": {
    "payout_no": "W20250101000001",
    "out_payout_no": "WD202501010001",
    "amount": 5000,
    "currency": "PHP",
    "status": "pending",
    "review_status": "pending",
    "fee_amount": 20,
    "freeze_amount": 5020
  }
}
```

### 补充说明

- 创建成功表示"已受理并冻结余额"。代付通常需要审核/下发等流程，最终结果以异步通知与查询为准。
- 幂等键：同一商户的 out_payout_no 全局唯一；重复下单参数一致视为幂等成功。
- 【用户信息（强烈建议必传）】gcash/maya 等主流支付方式的上游渠道实际强制要求下单用户的姓名/手机（email 部分渠道可选），缺失会被上游拒单导致订单失败；平台侧不再以 300405 拦截（customer 选填，提供时做格式校验+整名自动拆分）。请用通用字段提供，平台会自动映射到各渠道所需格式（例如把整名拆成 first/last_name）：姓名——代付可用 account_name（收款人/账户名，自动映射为 last_name），或 extra.customer.name 整名（也可显式 extra.customer.first_name+last_name）；邮箱——extra.customer.email（或 extra.email）；手机——extra.customer.phone（或 extra.phone）。这样商户只需接一套通用字段，无需为每个渠道单独适配。
- 【银行类代付】pay_method 为银行类时 bank_code 必传，取值用「查询可用银行（payout/banks/query）」接口返回的 code；编码非法返回 300407。

### 常见错误

- 100104 签名错误
- 300201 代付下单幂等冲突
- 300501 余额不足
- 300404 无可用支付方式渠道
- 300403 费率未配置
- 300401 渠道不可用/不存在
- 100001 参数校验失败（如 notify_url 指向内网/非法协议）
- 300402 上游配置不可用
- 300406 缺少必要下单参数（如 bank_code）
- 300407 银行编码非法

## 代付：查询订单（payout/query）

- 方法：`POST`
- 接口：`<BASE_URL>/merchant/payout/query`
- 用途：按平台代付单号或商户代付单号查询代付订单状态。

### 请求说明

- Content-Type：`application/json`
- 必填字段：merchant_no、api_key、timestamp、sign、payout_no/out_payout_no（二选一）
- 可选字段：nonce

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| merchant_no | string | 是 | 商户号（从商户后台获取） |
| api_key | string | 是 | API Key（从商户后台获取） |
| timestamp | int | 是 | Unix 时间戳（秒），用于过期校验（建议 ±300 秒窗口） |
| nonce | string\|null | 否 | 防重放随机串；不使用时建议省略或传 null（不要传空字符串） |
| payout_no | string\|null | 否 | 平台代付单号（与 out_payout_no 二选一，至少提供一个） |
| out_payout_no | string\|null | 否 | 商户代付单号（与 payout_no 二选一，至少提供一个） |
| sign | string | 是 | 签名（按文档规则计算；payout 接口使用 api_secret_payout） |

#### 请求示例
```json
{
  "merchant_no": "<YOUR_MERCHANT_NO>",
  "api_key": "<YOUR_API_KEY>",
  "timestamp": 1736073600,
  "nonce": "random-xyz",
  "sign": "<SIGN>",
  "payout_no": null,
  "out_payout_no": "WD202501010001"
}
```

> 签名说明：sign 需对"请求 JSON 顶层除 sign 以外的所有字段"参与计算；object/array 字段会按稳定 JSON 序列化。

### 返回字段与示例

- 统一返回结构：`{ code, message, data }`；业务失败通常仍为 HTTP 200。

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| payout_no | string | 是 | 平台代付单号 |
| out_payout_no | string | 是 | 商户代付单号 |
| amount | int | 是 | 代付金额（最小单位整数，精度 0.0001；需要放大10000倍，例如 1.2345 传 12345） |
| currency | string | 是 | 币种 |
| status | string | 是 | 订单状态：pending（处理中）/success（成功）/failed（失败），仅 success/failed 为终态。 |
| sub_state | string\|null | 否 | 处理中子态（归一化）：accepted 已受理 / reviewing 审核中 / processing 出款处理中 / verifying 出款结果核实中（非终态，请继续等待回调或轮询）；终态时为 null |
| channel_order_no | string\|null | 否 | 渠道侧订单号（可空） |
| finished_at | string\|null | 否 | 完成时间（ISO8601；未终态时可能为空） |
| failed_reason | string\|null | 否 | 失败原因摘要（仅失败时可能有值） |
| notify_status | string | 是 | 平台对商户回调推送状态：pending/success/failed（不等同于代付结果） |

#### 返回示例
```json
{
  "code": 0,
  "message": "ok",
  "data": {
    "payout_no": "W20250101000001",
    "out_payout_no": "WD202501010001",
    "amount": 5000,
    "currency": "USDT",
    "status": "success",
    "sub_state": null,
    "channel_order_no": "CHW20250101XXX",
    "finished_at": "2025-01-01T13:00:00Z",
    "failed_reason": null,
    "notify_status": "success"
  }
}
```

### 补充说明

- payout_no/out_payout_no 至少提供一个。
- status 终态为 success/failed；pending 表示处理中（请继续等待回调或轮询查单）。
- sub_state 是处理中状态的归一化细分（verifying 表示出款结果核实中，仍是非终态）；sub_state 仅用于展示/排查，业务判断以 status 终态（success/failed）为准。

### 常见错误

- 100104 签名错误
- 300301 订单不存在

## 代付：查询可用银行（payout/banks/query）

- 方法：`POST`
- 接口：`<BASE_URL>/merchant/payout/banks/query`
- 用途：按国家 + 支付方式查询当前可用的银行列表（银行类代付下单 bank_code 的合法取值）。

### 请求说明

- Content-Type：`application/json`
- 必填字段：merchant_no、api_key、timestamp、sign、pay_method
- 可选字段：nonce、country、currency

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| merchant_no | string | 是 | 商户号（从商户后台获取） |
| api_key | string | 是 | API Key（从商户后台获取） |
| timestamp | int | 是 | Unix 时间戳（秒），用于过期校验（建议 ±300 秒窗口） |
| nonce | string\|null | 否 | 防重放随机串；不使用时建议省略或传 null（不要传空字符串） |
| pay_method | string | 是 | 支付方式（银行类如 bank；钱包类如 gcash 通常返回空列表） |
| country | string\|null | 否 | 国家 ISO 码（如 PH） |
| currency | string\|null | 否 | 币种（如 PHP） |
| sign | string | 是 | 签名（按文档规则计算；payout 接口使用 api_secret_payout） |

#### 请求示例
```json
{
  "merchant_no": "<YOUR_MERCHANT_NO>",
  "api_key": "<YOUR_API_KEY>",
  "timestamp": 1736073600,
  "nonce": "random-xyz",
  "sign": "<SIGN>",
  "pay_method": "bank",
  "country": "PH",
  "currency": "PHP"
}
```

> 签名说明：sign 需对"请求 JSON 顶层除 sign 以外的所有字段"参与计算；object/array 字段会按稳定 JSON 序列化。

### 返回字段与示例

- 统一返回结构：`{ code, message, data }`；业务失败通常仍为 HTTP 200。

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| banks | array | 是 | 可用银行列表 [{code,name}]；code 即代付下单 bank_code 的合法取值（跨渠道统一编码，平台自动转译上游） |

#### 返回示例
```json
{
  "code": 0,
  "message": "ok",
  "data": {
    "banks": [
      {
        "code": "BDO",
        "name": "BDO Unibank"
      },
      {
        "code": "BPI",
        "name": "Bank of the Philippine Islands"
      },
      {
        "code": "GCASH",
        "name": "GCash"
      },
      {
        "code": "MAYA",
        "name": "Maya"
      }
    ]
  }
}
```

### 补充说明

- 银行列表随上游渠道可用性动态变化，建议下单前实时查询（或做短期缓存）。
- 纯钱包类支付方式（pay_method=gcash/maya 本身）无需选目的地，返回空 banks；银行类（pay_method=bank）的 banks 可能含电子钱包（如 GCASH/MAYA）作为代付目的地，bank_code 可传其 code。

### 常见错误

- 100104 签名错误

## 代付：查询付款凭证（payout/proof/query）

- 方法：`POST`
- 接口：`<BASE_URL>/merchant/payout/proof/query`
- 用途：查询已成功代付订单的上游付款凭证地址（并非所有渠道/币种都提供凭证）。

### 请求说明

- Content-Type：`application/json`
- 必填字段：merchant_no、api_key、timestamp、sign、payout_no/out_payout_no（二选一）
- 可选字段：nonce

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| merchant_no | string | 是 | 商户号（从商户后台获取） |
| api_key | string | 是 | API Key（从商户后台获取） |
| timestamp | int | 是 | Unix 时间戳（秒），用于过期校验（建议 ±300 秒窗口） |
| nonce | string\|null | 否 | 防重放随机串；不使用时建议省略或传 null（不要传空字符串） |
| payout_no | string\|null | 否 | 平台代付单号（与 out_payout_no 二选一，至少提供一个） |
| out_payout_no | string\|null | 否 | 商户代付单号（与 payout_no 二选一，至少提供一个） |
| sign | string | 是 | 签名（按文档规则计算；payout 接口使用 api_secret_payout） |

#### 请求示例
```json
{
  "merchant_no": "<YOUR_MERCHANT_NO>",
  "api_key": "<YOUR_API_KEY>",
  "timestamp": 1736073600,
  "nonce": "random-xyz",
  "sign": "<SIGN>",
  "payout_no": null,
  "out_payout_no": "WD202501010001"
}
```

> 签名说明：sign 需对"请求 JSON 顶层除 sign 以外的所有字段"参与计算；object/array 字段会按稳定 JSON 序列化。

### 返回字段与示例

- 统一返回结构：`{ code, message, data }`；业务失败通常仍为 HTTP 200。

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| payout_no | string | 是 | 平台代付单号 |
| out_payout_no | string | 是 | 商户代付单号 |
| proof_url | string | 是 | 付款凭证地址（有访问时效，见 expires_in；请即取即用，勿持久化该 URL） |
| expires_in | int\|null | 否 | 凭证地址有效期（秒，由渠道决定，如 1800=30 分钟；null 表示渠道未声明） |
| queried_at | string\|null | 否 | 渠道返回的查询时间（可空） |

#### 返回示例
```json
{
  "code": 0,
  "message": "ok",
  "data": {
    "payout_no": "W20250101000001",
    "out_payout_no": "WD202501010001",
    "proof_url": "https://proof.example.com/xxx.pdf",
    "expires_in": 1800,
    "queried_at": "2025-01-01 13:00:00"
  }
}
```

### 补充说明

- 仅出款成功（status=success）的订单可查凭证；处理中/失败订单返回 300409。
- 并非所有渠道/币种都提供凭证；部分渠道仅支持查询近几天内的订单（超窗返回 300409）。
- 凭证 URL 有访问时效（expires_in 秒），请下载转存而非保存 URL。

### 常见错误

- 100104 签名错误
- 300301 订单不存在
- 300408 渠道不支持凭证查询
- 300409 凭证暂不可用

## 代付：查询付款收据图片（payout/receipt/query）

- 方法：`POST`
- 接口：`<BASE_URL>/merchant/payout/receipt/query`
- 用途：查询/生成代付订单的付款收据图片（PNG）。仅 status=success 的代付单可生成；render-once 落盘复用。

### 请求说明

- Content-Type：`application/json`
- 必填字段：merchant_no、api_key、timestamp、sign、payout_no/out_payout_no（二选一）
- 可选字段：nonce、lang、inline

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| merchant_no | string | 是 | 商户号（从商户后台获取） |
| api_key | string | 是 | API Key（从商户后台获取） |
| timestamp | int | 是 | Unix 时间戳（秒），用于过期校验（建议 ±300 秒窗口） |
| nonce | string\|null | 否 | 防重放随机串；不使用时建议省略或传 null（不要传空字符串） |
| payout_no | string\|null | 否 | 平台代付单号（与 out_payout_no 二选一，至少提供一个） |
| out_payout_no | string\|null | 否 | 商户代付单号（与 payout_no 二选一，至少提供一个） |
| lang | string\|null | 否 | 收据语言（可选，枚举：en / zh-CN / zh-TW；不传时按订单国家自动派生） |
| inline | int\|null | 否 | 传 1 时直接返回 base64 图片数据（image_base64 + mime）；不传或传 0 时返回带时效签名的下载链接（receipt_url） |
| sign | string | 是 | 签名（按文档规则计算；payout 接口使用 api_secret_payout） |

#### 请求示例
```json
{
  "merchant_no": "<YOUR_MERCHANT_NO>",
  "api_key": "<YOUR_API_KEY>",
  "timestamp": 1736073600,
  "nonce": "random-xyz",
  "sign": "<SIGN>",
  "payout_no": null,
  "out_payout_no": "WD202501010001",
  "lang": "zh-CN",
  "inline": 0
}
```

> 签名说明：sign 需对"请求 JSON 顶层除 sign 以外的所有字段"参与计算；object/array 字段会按稳定 JSON 序列化。

### 返回字段与示例

- 统一返回结构：`{ code, message, data }`；业务失败通常仍为 HTTP 200。

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| payout_no | string | 是 | 平台代付单号 |
| out_payout_no | string | 是 | 商户代付单号 |
| lang | string | 是 | 收据实际使用的语言（en / zh-CN / zh-TW） |
| receipt_url | string\|null | 否 | 收据图片下载地址（相对路径，如 /api/open/v1/payout/receipt/file?token=…；商户需拼接自己的 openapi_base 即上级代理专有域名得到完整下载地址；GET 即可下载；仅 inline=0 或未传时返回；链接带时效，有效期见 expires_in；请即取即用，勿持久化该 URL） |
| expires_in | int\|null | 否 | 下载链接有效期（秒，如 3600=1 小时；null 表示未声明有效期）；仅 inline=0 或未传时有意义 |
| mime | string\|null | 否 | 图片 MIME 类型（如 image/png）；仅 inline=1 时返回 |
| image_base64 | string\|null | 否 | 收据图片 Base64 编码数据（不含 data URI 前缀）；仅 inline=1 时返回 |

#### 返回示例
```json
{
  "code": 0,
  "message": "ok",
  "data": {
    "payout_no": "W20250101000001",
    "out_payout_no": "WD202501010001",
    "lang": "zh-CN",
    "receipt_url": "/api/open/v1/payout/receipt/file?token=eyJhbGciOiJIUzI1NiJ9...",
    "expires_in": 3600
  }
}
```

### 补充说明

- 仅出款成功（status=success）的代付单可生成收据；非 success 状态返回 300410。
- render-once：首次生成后图片落盘复用，相同参数重复查询直接返回缓存结果。
- lang 不传时平台按订单国家自动派生语言（如 PH → en）；显式指定可覆盖。
- inline=1 时响应体直接含 Base64 图片（image_base64 + mime），适合服务端即时处理；inline=0（默认）时返回带时效签名的 receipt_url 下载链接，适合前端跳转/嵌入。
- 下载链接有时效（expires_in 秒），请勿持久化 URL；需长期保存请下载图片转存至自有存储。

### 常见错误

- 100104 签名错误
- 300301 订单不存在
- 300410 代付收据暂不可用（订单非 success）
- 300411 收据生成失败

## 通用：查询可用支付方式（pay-methods/query）

- 方法：`POST`
- 接口：`<BASE_URL>/merchant/pay-methods/query`
- 用途：查询平台启用的支付方式字典（下单 pay_method/country 的合法取值）。

### 请求说明

- Content-Type：`application/json`
- 必填字段：merchant_no、api_key、timestamp、sign
- 可选字段：nonce、country

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| merchant_no | string | 是 | 商户号（从商户后台获取） |
| api_key | string | 是 | API Key（从商户后台获取） |
| timestamp | int | 是 | Unix 时间戳（秒），用于过期校验（建议 ±300 秒窗口） |
| nonce | string\|null | 否 | 防重放随机串；不使用时建议省略或传 null（不要传空字符串） |
| country | string\|null | 否 | 国家 ISO 码（如 PH；不传返回全部） |
| sign | string | 是 | 签名（按文档规则计算；本接口使用 api_secret_pay） |

#### 请求示例
```json
{
  "merchant_no": "<YOUR_MERCHANT_NO>",
  "api_key": "<YOUR_API_KEY>",
  "timestamp": 1736073600,
  "nonce": "random-xyz",
  "sign": "<SIGN>",
  "country": "PH"
}
```

> 签名说明：sign 需对"请求 JSON 顶层除 sign 以外的所有字段"参与计算；object/array 字段会按稳定 JSON 序列化。

### 返回字段与示例

- 统一返回结构：`{ code, message, data }`；业务失败通常仍为 HTTP 200。

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| methods | array | 是 | 支付方式列表 [{pay_method,name,country,currency}]；pay_method 即下单接口的合法取值 |

#### 返回示例
```json
{
  "code": 0,
  "message": "ok",
  "data": {
    "methods": [
      {
        "pay_method": "gcash",
        "name": "GCash",
        "country": "PH",
        "currency": "PHP"
      },
      {
        "pay_method": "maya",
        "name": "Maya",
        "country": "PH",
        "currency": "PHP"
      }
    ]
  }
}
```

### 补充说明

- 返回的是平台启用的支付方式字典；某支付方式当下是否有可用上游以下单接口实际返回为准（无可用渠道时报 300404）。

### 常见错误

- 100104 签名错误

## 通用：查询账户余额（balance/query）

- 方法：`POST`
- 接口：`<BASE_URL>/merchant/balance/query`
- 用途：查询商户交易账户各币种的可用/冻结余额。

### 请求说明

- Content-Type：`application/json`
- 必填字段：merchant_no、api_key、timestamp、sign
- 可选字段：nonce、currency

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| merchant_no | string | 是 | 商户号（从商户后台获取） |
| api_key | string | 是 | API Key（从商户后台获取） |
| timestamp | int | 是 | Unix 时间戳（秒），用于过期校验（建议 ±300 秒窗口） |
| nonce | string\|null | 否 | 防重放随机串；不使用时建议省略或传 null（不要传空字符串） |
| currency | string\|null | 否 | 币种（如 PHP；不传返回全部币种） |
| sign | string | 是 | 签名（按文档规则计算；本接口使用 api_secret_pay） |

#### 请求示例
```json
{
  "merchant_no": "<YOUR_MERCHANT_NO>",
  "api_key": "<YOUR_API_KEY>",
  "timestamp": 1736073600,
  "nonce": "random-xyz",
  "sign": "<SIGN>",
  "currency": "PHP"
}
```

> 签名说明：sign 需对"请求 JSON 顶层除 sign 以外的所有字段"参与计算；object/array 字段会按稳定 JSON 序列化。

### 返回字段与示例

- 统一返回结构：`{ code, message, data }`；业务失败通常仍为 HTTP 200。

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| balances | array | 是 | 余额列表 [{currency,available,frozen}]；金额为最小单位整数（10000=1 元） |

#### 返回示例
```json
{
  "code": 0,
  "message": "ok",
  "data": {
    "balances": [
      {
        "currency": "PHP",
        "available": 12345600,
        "frozen": 50000
      }
    ]
  }
}
```

### 补充说明

- available 为可用余额，frozen 为冻结金额（含在途代付冻结）；均为最小单位整数。

### 常见错误

- 100104 签名错误

## 通用：服务版本（GET /version）

- 方法：`GET`
- 接口：`<BASE_URL>/version`
- 用途：查询服务端 OpenAPI 版本（无需鉴权）；所有 /api/open/ 响应均带 X-Api-Version 头。

### 请求说明

- Content-Type：`application/json`
- 必填字段：无
- 可选字段：无

_无_

#### 请求示例
```json
{}
```

> 签名说明：sign 需对"请求 JSON 顶层除 sign 以外的所有字段"参与计算；object/array 字段会按稳定 JSON 序列化。

### 返回字段与示例

- 统一返回结构：`{ code, message, data }`；业务失败通常仍为 HTTP 200。

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| major | string | 是 | 大版本（路径前缀，如 v1） |
| version | string | 是 | 语义化版本（major.minor.patch） |
| base_path | string | 是 | OpenAPI 路径前缀 |

#### 返回示例
```json
{
  "code": 0,
  "message": "ok",
  "data": {
    "major": "v1",
    "version": "1.2.0",
    "base_path": "/api/open/v1"
  }
}
```

### 补充说明

- 无需鉴权；用于探活与版本灰度排查。

## 签名与通用字段

通用字段（建议放在 JSON 请求体中）：`merchant_no`、`api_key`、`timestamp`、`nonce`（可选）、`sign`。
建议算法：HMAC-SHA256。把所有参与签名字段（不含 sign）按字段名 ASCII 升序排序，拼成 `key=value` 并用 `&` 连接，末尾追加 `&secret=...`，对 raw string 做 HMAC-SHA256，结果转 16 进制小写作为 sign。
时间戳窗口建议 ±300 秒；若使用 nonce，服务端会在短时间内做防重放校验。

## 多语言示例（pay/create）

> 说明：示例已对 extra 等嵌套字段做稳定 JSON 序列化参与签名，可直接照抄（替换占位符与凭据后即可用）。

### JavaScript（Node.js）
```js
// 本 Demo 演示签名与下单。sign = HMAC-SHA256(参数按字段名升序拼成 k=v 用 & 连接、末尾追加 "&secret=密钥") 的 hex 值。嵌套对象（extra 等）按 key 升序紧凑 JSON 序列化后参与签名。请把占位符替换为商户实际凭据。
const crypto = require('crypto');

const baseUrl = '<BASE_URL>';
const merchantNo = '<YOUR_MERCHANT_NO>';
const apiKey = '<YOUR_API_KEY>';
const apiSecret = '<API_SECRET>';

const payload = {
  merchant_no: merchantNo,
  api_key: apiKey,
  timestamp: Math.floor(Date.now() / 1000),
  nonce: 'random-xyz',
  out_order_no: '202501010001',
  amount: 10000,
  currency: 'PHP',
  pay_method: 'gcash',
  country: 'PH',
  notify_url: 'https://merchant.example.com/api/notify/pay',
  extra: {
    customer: {
      first_name: '<FIRST_NAME>',
      last_name: '<LAST_NAME>',
      email: '<EMAIL>',
      phone: '<PHONE>',
    },
  },
};

const stableStringify = (v) => {
  if (v === null) return 'null';
  if (typeof v !== 'object') return JSON.stringify(v);
  if (Array.isArray(v)) return '[' + v.map(stableStringify).join(',') + ']';
  return '{' + Object.keys(v).sort().map((k) => JSON.stringify(k) + ':' + stableStringify(v[k])).join(',') + '}';
};
const signPayload = (data, secret) => {
  const keys = Object.keys(data).filter((k) => k !== 'sign' && data[k] != null).sort();
  const raw = keys.map((k) => `${k}=${typeof data[k] === 'object' && data[k] !== null ? stableStringify(data[k]) : data[k]}`).join('&') + `&secret=${secret}`;
  return crypto.createHmac('sha256', secret).update(raw).digest('hex');
};

const sign = signPayload(payload, apiSecret);
const body = { ...payload, sign };

fetch(`${baseUrl}/merchant/pay/create`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(body),
})
  .then((r) => r.json())
  .then((d) => console.log(d));
```

### PHP
```php
<?php
// 本 Demo 演示签名与下单。sign = HMAC-SHA256(参数按字段名升序拼成 k=v 用 & 连接、末尾追加 "&secret=密钥") 的 hex 值。嵌套对象（extra 等）按 key 升序紧凑 JSON 序列化后参与签名。请把占位符替换为商户实际凭据。
$baseUrl = "<BASE_URL>";
$merchantNo = "<YOUR_MERCHANT_NO>";
$apiKey = "<YOUR_API_KEY>";
$apiSecret = "<API_SECRET>";

$payload = [
  'merchant_no' => $merchantNo,
  'api_key' => $apiKey,
  'timestamp' => time(),
  'nonce' => 'random-xyz',
  'out_order_no' => '202501010001',
  'amount' => 10000,
  'currency' => 'PHP',
  'pay_method' => 'gcash',
  'country' => 'PH',
  'notify_url' => 'https://merchant.example.com/api/notify/pay',
  'extra' => [
    'customer' => [
      'first_name' => '<FIRST_NAME>',
      'last_name' => '<LAST_NAME>',
      'email' => '<EMAIL>',
      'phone' => '<PHONE>',
    ],
  ],
];

function ksort_recursive(&$a) {
  if (is_array($a)) { ksort($a); foreach ($a as &$v) { ksort_recursive($v); } }
}
function stable_value($v) {
  if (is_array($v)) {
    ksort_recursive($v);
    return json_encode($v, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
  }
  return (string)$v;
}
ksort($payload);
$parts = [];
foreach ($payload as $k => $v) {
  if ($k === 'sign' || $v === null) { continue; }
  $parts[] = $k . "=" . stable_value($v);
}
$raw = implode("&", $parts) . "&secret=" . $apiSecret;
$sign = hash_hmac("sha256", $raw, $apiSecret);
$payload["sign"] = $sign;

$ch = curl_init($baseUrl . "/merchant/pay/create");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json"]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$resp = curl_exec($ch);
curl_close($ch);
echo $resp;
```

### Java
```java
// 本 Demo 演示签名与下单。sign = HMAC-SHA256(参数按字段名升序拼成 k=v 用 & 连接、末尾追加 "&secret=密钥") 的 hex 值。嵌套对象（extra 等）按 key 升序紧凑 JSON 序列化后参与签名。请把占位符替换为商户实际凭据。
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.TreeMap;
import java.util.HexFormat;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;

String baseUrl = "<BASE_URL>";
String merchantNo = "<YOUR_MERCHANT_NO>";
String apiKey = "<YOUR_API_KEY>";
String apiSecret = "<API_SECRET>";

Map<String, Object> payload = new TreeMap<>();
payload.put("merchant_no", merchantNo);
payload.put("api_key", apiKey);
payload.put("timestamp", System.currentTimeMillis() / 1000);
payload.put("nonce", "random-xyz");
payload.put("out_order_no", "202501010001");
payload.put("amount", 10000);
payload.put("currency", "PHP");
payload.put("pay_method", "gcash");
payload.put("country", "PH");
payload.put("notify_url", "https://merchant.example.com/api/notify/pay");
Map<String, Object> extra = new TreeMap<>();
Map<String, Object> customer = new TreeMap<>();
customer.put("first_name", "<FIRST_NAME>");
customer.put("last_name", "<LAST_NAME>");
customer.put("email", "<EMAIL>");
customer.put("phone", "<PHONE>");
extra.put("customer", customer);
payload.put("extra", extra);

// signMapper：key 升序序列化嵌套对象，与服务端稳定签名口径一致
ObjectMapper signMapper = new ObjectMapper();
signMapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true);
StringBuilder raw = new StringBuilder();
for (Map.Entry<String, Object> e : payload.entrySet()) {
  if ("sign".equals(e.getKey()) || e.getValue() == null) { continue; }
  Object val = e.getValue();
  String strVal = (val instanceof Map || val instanceof java.util.Collection)
      ? signMapper.writeValueAsString(val) : String.valueOf(val);
  raw.append(e.getKey()).append("=").append(strVal).append("&");
}
raw.append("secret=").append(apiSecret);

Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(apiSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
String sign = HexFormat.of().formatHex(mac.doFinal(raw.toString().getBytes(StandardCharsets.UTF_8)));
payload.put("sign", sign);

ObjectMapper mapper = new ObjectMapper();
String body = mapper.writeValueAsString(payload);

HttpRequest req = HttpRequest.newBuilder()
  .uri(URI.create(baseUrl + "/merchant/pay/create"))
  .header("Content-Type", "application/json")
  .POST(HttpRequest.BodyPublishers.ofString(body))
  .build();
HttpResponse<String> resp = HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(resp.body());
```

### Go
```go
// 本 Demo 演示签名与下单。sign = HMAC-SHA256(参数按字段名升序拼成 k=v 用 & 连接、末尾追加 "&secret=密钥") 的 hex 值。嵌套对象（extra 等）按 key 升序紧凑 JSON 序列化后参与签名。请把占位符替换为商户实际凭据。
package main

import (
  "bytes"
  "crypto/hmac"
  "crypto/sha256"
  "encoding/hex"
  "encoding/json"
  "fmt"
  "net/http"
  "sort"
  "strings"
  "time"
)

func main() {
  baseUrl := "<BASE_URL>"
  merchantNo := "<YOUR_MERCHANT_NO>"
  apiKey := "<YOUR_API_KEY>"
  apiSecret := "<API_SECRET>"

  payload := map[string]any{
    "merchant_no": merchantNo,
    "api_key": apiKey,
    "timestamp": time.Now().Unix(),
    "nonce": "random-xyz",
    "out_order_no": "202501010001",
    "amount": 10000,
    "currency": "PHP",
    "pay_method": "gcash",
    "country": "PH",
    "notify_url": "https://merchant.example.com/api/notify/pay",
    "extra": map[string]any{
      "customer": map[string]any{
        "first_name": "<FIRST_NAME>",
        "last_name": "<LAST_NAME>",
        "email": "<EMAIL>",
        "phone": "<PHONE>",
      },
    },
  }

  // stableJSON：关闭 HTML 转义（Go 默认转义 <>&），与 JS JSON.stringify 语义一致
  stableJSON := func(v any) string {
    var buf bytes.Buffer
    enc := json.NewEncoder(&buf)
    enc.SetEscapeHTML(false)
    enc.Encode(v)
    return strings.TrimRight(buf.String(), "\n")
  }

  keys := make([]string, 0, len(payload));
  for k := range payload {
    if k == "sign" || payload[k] == nil { continue }
    keys = append(keys, k)
  }
  sort.Strings(keys)
  var b strings.Builder
  for i, k := range keys {
    if i > 0 { b.WriteString("&") }
    b.WriteString(k)
    b.WriteString("=")
    switch payload[k].(type) {
    case map[string]any, []any:
      b.WriteString(stableJSON(payload[k]))
    default:
      b.WriteString(fmt.Sprint(payload[k]))
    }
  }
  b.WriteString("&secret=")
  b.WriteString(apiSecret)

  mac := hmac.New(sha256.New, []byte(apiSecret))
  mac.Write([]byte(b.String()))
  sign := hex.EncodeToString(mac.Sum(nil))
  payload["sign"] = sign

  body, _ := json.Marshal(payload)
  req, _ := http.NewRequest("POST", baseUrl+"/merchant/pay/create", bytes.NewBuffer(body))
  req.Header.Set("Content-Type", "application/json")
  resp, _ := http.DefaultClient.Do(req)
  defer resp.Body.Close()
}
```

### Python
```python
# 本 Demo 演示签名与下单。sign = HMAC-SHA256(参数按字段名升序拼成 k=v 用 & 连接、末尾追加 "&secret=密钥") 的 hex 值。嵌套对象（extra 等）按 key 升序紧凑 JSON 序列化后参与签名。请把占位符替换为商户实际凭据。
import time
import hmac
import hashlib
import json
import requests

base_url = "<BASE_URL>"
merchant_no = "<YOUR_MERCHANT_NO>"
api_key = "<YOUR_API_KEY>"
api_secret = "<API_SECRET>"

payload = {
    "merchant_no": merchant_no,
    "api_key": api_key,
    "timestamp": int(time.time()),
    "nonce": "random-xyz",
    "out_order_no": "202501010001",
    "amount": 10000,
    "currency": "PHP",
    "pay_method": "gcash",
    "country": "PH",
    "notify_url": "https://merchant.example.com/api/notify/pay",
    "extra": {
        "customer": {
            "first_name": "<FIRST_NAME>",
            "last_name": "<LAST_NAME>",
            "email": "<EMAIL>",
            "phone": "<PHONE>",
        },
    },
}

def _sv(v):
    """稳定序列化：嵌套对象/数组 → key 升序紧凑 JSON；标量 → str(v)。"""
    if isinstance(v, (dict, list)):
        return json.dumps(v, sort_keys=True, separators=(',', ':'), ensure_ascii=False)
    return str(v)

keys = sorted(k for k in payload if k != 'sign' and payload[k] is not None)
raw = "&".join(f"{k}={_sv(payload[k])}" for k in keys) + f"&secret={api_secret}"
sign = hmac.new(api_secret.encode(), raw.encode(), hashlib.sha256).hexdigest()
payload["sign"] = sign

resp = requests.post(f"{base_url}/merchant/pay/create", json=payload)
print(resp.text)
```

## 金额口径

所有金额统一使用整数表示"最小记账单位"，当前精度约定为 `0.0001`；需要放大 10000 倍传值，例如 `1.2345` 传 `12345`，避免浮点误差。

## 回调说明

平台会按订单创建时的 `notify_url` 以 `POST application/json` 方式回调支付/代付结果。
回调同样携带签名字段，请商户侧按对应业务（pay/payout）使用各自密钥验签。

**代收回调时机**：代收仅在订单成功（`status=success`）时回调商户；失败/超时等非成功状态不回调，商户应通过查单接口（`pay/query`）获知最终结果。
（代付不受此限制：代付在终态 `success/failed` 均会回调。）

**成功应答规则**：商户处理成功后，必须返回 HTTP 200 且响应体为 `success` 或 `ok`（大小写不敏感）；
也接受 JSON 格式：`{"success":true}` / `{"code":0}` / `{"message":"success"}` / `{"message":"ok"}`。
未按此返回（即使 HTTP 200 但 body 为其他内容，如空响应、HTML 错误页等）平台视为回调失败，
将按策略重试，连续失败达阈值会触发告警。

## IP 黑白名单说明

- 默认不启用白名单（允许所有来源 IP 访问 OpenAPI）。
- 当启用策略后，命中黑名单直接拒绝；存在白名单时必须命中白名单才允许访问。
- 导出文档不包含商户真实白名单配置，请在商户后台自行查看。

## 通用鉴权错误码

以下错误码对所有需鉴权端点通用；各端点「常见错误」仅列出高频或业务相关项，不代表其他鉴权码不会返回：

- `100001` 参数校验失败（HTTP 200，message 为具体字段错误 Joi 原文，如 `"amount" must be a number`）
- `100101` 请求过期（timestamp 超出允许窗口）
- `100102` IP 不在白名单
- `100103` 重放（nonce/签名指纹重复）
- `100104` 签名错误
- `100105` IP 在黑名单
- `100106` 鉴权失败次数过多（限流）
