Copy the whole prompt below into an AI assistant (ChatGPT / Claude / Cursor, etc.) and it will write integration code with correct signing per this API spec. The full API doc is embedded in the prompt; credentials are placeholders—replace them with your real credentials after generating code.
You are a senior payment-system integration engineer. Please help me integrate the following "Merchant OpenAPI".
Integration requirements:
1. Write directly runnable integration code in the programming language I specify; if I have not specified a language, ask me first (commonly Node.js / PHP / Python / Go / Java).
2. Implement the signature `sign` strictly (critical to fund security, follow every rule precisely):
- Algorithm HMAC-SHA256, output as lowercase hexadecimal;
- The fields that participate in the signature = all top-level fields of the request body except `sign` whose value is not null;
- Sort by field name in ASCII ascending order, join as `key=value` with `&`, then append `&secret=<corresponding business secret>` at the end;
- For fields whose value is an object/array (such as `extra`), first apply stable serialization as "compact JSON with keys in ascending order", then use that as the value of the field;
- Use api_secret_pay to sign for Collection, and api_secret_payout to sign for Payout.
3. Cover Collection create order `pay/create` and query `pay/query`; if I need Payout, also add `payout/create` and `payout/query`.
4. The amount is an integer scaled by 10000 (precision 0.0001, e.g. 1.2345 is sent as 12345); the unified response structure is `{ code, message, data }`, please handle the common error codes listed in the documentation.
5. Represent the endpoint address and credentials with placeholders throughout, I will replace them myself (please obtain the Base URL from "Security Center · OpenAPI" in my merchant admin): `<BASE_URL>`, `<YOUR_MERCHANT_NO>`, `<YOUR_API_KEY>`, `<YOUR_API_SECRET_PAY>`, `<YOUR_API_SECRET_PAYOUT>`.
Output requirements: first list the integration steps as bullet points, then give the complete runnable code, and attach a minimal runnable example of "signature computation + create order".
The following is the complete API documentation (endpoints, fields, signature rules and the Official SDK entry point are all included, please treat it as authoritative):
=== API documentation begins ===
# Merchant OpenAPI Integration Guide
- Base URL: Log in to the merchant console "Security Center · OpenAPI" to obtain your actual integration address (this guide uses `<BASE_URL>` as a placeholder).
- API version: `v1` (server version 1.2.0; every response carries the `X-Api-Version` header).
- Export date: 20260710
> Note: This guide does not contain the merchant real credentials or allowlist information; replace the placeholders before integrating.
## Versioning and Compatibility Policy
- The major version is reflected in the path prefix: currently `/api/open/v1`.
- v1 evolves only in a **backward-compatible** way (adding optional fields, fixing defects); it will not change the meaning of existing fields or tighten existing validation.
- If a breaking change is required, a new `/api/open/v2` will be added while v1 is retained for a period, allowing merchants to migrate smoothly.
- Call `GET <BASE_URL>/version` to query the current server version (no authentication required).
**Changes in this version (1.2.0)**:
- Collection external `status` no longer returns `expired`: unpaid timeouts are normalized to `pending` (processing); rely on the order query result.
- Collection only sends a callback to the merchant when the order succeeds (`success`); non-success states such as failed/timeout trigger no callback, and the merchant should obtain the final result via the order query API (Payout is not subject to this restriction).
## Merchant Information
- Merchant name: <YOUR_MERCHANT_NAME>
- Merchant no.: <YOUR_MERCHANT_NO>
## Official SDKs
Official SDKs for 5 languages: zero third-party dependencies (standard library only), with built-in HMAC-SHA256 request signing / callback verification, dual environments (SANDBOX local / PRODUCTION with your dedicated-domain `baseUrl`), full endpoint coverage, and byte-for-byte consistent signing across languages. **Use the SDK directly to integrate — no need to implement signing or stable serialization yourself.**
| Language | Install | Package / Source |
|------|------|------|
| Node.js | `npm i @bebebus/merchant-openapi-sdk` | [npm](https://www.npmjs.com/package/@bebebus/merchant-openapi-sdk) |
| Python | `pip install bebebus-merchant-openapi-sdk` | [PyPI](https://pypi.org/project/bebebus-merchant-openapi-sdk/) |
| PHP | `composer require bebebus/merchant-openapi-sdk` | [Packagist](https://packagist.org/packages/bebebus/merchant-openapi-sdk) |
| Go | `go get github.com/bebebus/SDK/go` | [pkg.go.dev](https://pkg.go.dev/github.com/bebebus/SDK/go) |
| Java | From source (package `cloud.cniia.openapi.sdk`, not on Maven) | [GitHub](https://github.com/bebebus/SDK/tree/main/java) |
- Repository and releases: https://github.com/bebebus/SDK (see `SIGNING.md` for the authoritative signing algorithm and `INTERFACES.md` for field-level request/response).
## Collection: Create Order (pay/create)
- Method: `POST`
- Endpoint: `<BASE_URL>/merchant/pay/create`
- Purpose: Create a collection order; returns the API order no. and a payment URL / QR code content.
### Request Details
- Content-Type: `application/json`
- Required fields: merchant_no, api_key, timestamp, sign, out_order_no, amount, currency, pay_method, notify_url
- Optional fields: nonce, country, return_url, subject, remark, client_ip, extra
| Field | Type | Required | Description |
|---|---|---|---|
| merchant_no | string | Yes | Merchant no. (obtained from the merchant dashboard) |
| api_key | string | Yes | API Key (obtained from the merchant dashboard) |
| timestamp | int | Yes | Unix timestamp (seconds), used for expiry check (recommended ±300s window) |
| nonce | string\|null | No | Random string for replay protection; when unused, omit it or pass null (do not pass an empty string) |
| out_order_no | string | Yes | Merchant order no. (idempotency key; must be unique per merchant) |
| amount | int | Yes | Order amount (= actual value × 10000 as an integer, e.g. pass 12345 for 1.2345). Must be a positive JSON integer; strings (e.g. "12345") or decimals are not accepted; invalid input returns code=100001 (HTTP 200, with the specific field validation error in message). |
| currency | string | Yes | Currency (e.g. PHP/USDT; used for validation and routing) |
| pay_method | string | Yes | Payment method (choose from the payment-method dictionary, e.g. gcash/maya; for crypto use the chain name such as trc20/erc20) |
| country | string\|null | No | Country ISO code (e.g. PH; required for fiat, optional for crypto) |
| notify_url | string | Yes | Callback URL (the service sends the result to this URL when the order reaches a final state) |
| return_url | string\|null | No | Frontend return URL (redirect back to the merchant page after payment; whether it redirects depends on the payment method/scenario) |
| subject | string\|null | No | Order subject (can be used for display or reconciliation notes) |
| remark | string\|null | No | Remark (can be used for merchant custom notes / reconciliation) |
| client_ip | string\|null | No | End-user IP (can be used for risk control; if omitted, the service cannot obtain this information) |
| extra | object\|null | No | Extension field (top-level field, included in the signature; the object is serialized with stable JSON). ⚠️ It is strongly recommended to include the end-user info extra.customer (see the "Extension info and channel requirements" section below) |
| extra.customer | object | No | End-user info (optional). If omitted, the request is not rejected solely because this field is missing; however, some payment methods require name/phone and the order may fail without them, so including them is recommended. The service validates the format and maps the fields (a full name is split into first/last): provide the full name (or first_name+last_name), email, and phone |
| sign | string | Yes | Signature (computed per the documented rules; the pay endpoint uses api_secret_pay) |
#### Request Example
```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": "Order subject (optional)",
"remark": "Remark (optional)",
"client_ip": "1.2.3.4",
"extra": {
"user_id": "u123456",
"customer": {
"first_name": "San",
"last_name": "Zhang",
"email": "user@example.com",
"phone": "13800000000"
}
}
}
```
> Signature note: sign is computed over "all top-level fields of the request JSON except sign"; object/array fields are serialized with stable JSON.
### Response Fields and Example
- Unified response structure: `{ code, message, data }`; business failures usually still return HTTP 200.
| Field | Type | Required | Description |
|---|---|---|---|
| order_no | string | Yes | API order no. (used for subsequent query and reconciliation) |
| out_order_no | string | Yes | Merchant order no. (echoed as-is, to help the merchant link its business order) |
| amount | int | Yes | Order amount (= actual value × 10000 as an integer, e.g. 12345 for 1.2345) |
| currency | string | Yes | Currency (echoes the requested currency as-is, e.g. PHP/USDT) |
| pay_url | string\|null | No | Payment URL (on a successful order with code=0, at least one of pay_url/qrcode_content/pay_params is non-empty; this field may be empty, in which case use the other fields to launch payment) |
| qrcode_content | string\|null | No | QR code content (on a successful order with code=0, at least one of pay_url/qrcode_content/pay_params is non-empty; this field may be empty) |
| pay_params | string\|null | No | Native payment string (optional): some payment methods return native app launch parameters; when pay_url is empty and this field is non-empty, use it to launch payment on the client |
| expire_at | string\|null | No | Expiry time (ISO8601; nullable). Unpaid-and-timed-out is no longer represented separately as an expired state; externally it is normalized to pending (processing) — rely on the order query result. |
| status | string | Yes | Order status: pending (processing) / success / failed. Only success/failed are final states; unpaid-and-timed-out is no longer returned separately as expired and is normalized to pending. |
#### Response Example
```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"
}
}
```
### Additional Notes
- Amount = actual value × 10000 as an integer, e.g. pass 12345 for 1.2345, transmitted as an integer to avoid floating-point error.
- Idempotency key: out_order_no is globally unique per merchant; a repeated order with identical parameters is treated as an idempotent success, while mismatched parameters return an idempotency conflict.
- If the order is not accepted, the endpoint returns code=100000 in real time and sets the order to the failed final state; you cannot retry with the same out_order_no (idempotency will only return that failed order), so use a new out_order_no to place a new order.
- [End-user info (strongly recommended)] Some payment methods require the end-user name and phone (email may be optional); missing them may cause the order to fail. Use these generic fields: name — full name in extra.customer.name (or explicit extra.customer.first_name+last_name); email — extra.customer.email (or extra.email); phone — extra.customer.phone (or extra.phone). The service validates and maps these fields, so the merchant does not need separate integrations for different payment methods.
- [Native payment string] When pay_url is empty and pay_params is non-empty (some payment methods only return native launch parameters), use pay_params to launch payment on the client.
### Common Errors
- 100101 Request expired
- 100102 IP not in allowlist
- 100104 Invalid signature
- 100105 IP in blocklist
- 100000 Failed to create order (the order was not accepted and was set to failed in real time)
- 300404 No payment method is available for the request
- 100001 Parameter validation failed (e.g. notify_url points to an internal network / illegal protocol)
- 300402 Payment method configuration unavailable
- 300403 Fee rate not configured
## Collection: Query Order (pay/query)
- Method: `POST`
- Endpoint: `<BASE_URL>/merchant/pay/query`
- Purpose: Query a collection order status by API order no. or merchant order no.
### Request Details
- Content-Type: `application/json`
- Required fields: merchant_no, api_key, timestamp, sign, order_no/out_order_no (either one)
- Optional fields: nonce
| Field | Type | Required | Description |
|---|---|---|---|
| merchant_no | string | Yes | Merchant no. (obtained from the merchant dashboard) |
| api_key | string | Yes | API Key (obtained from the merchant dashboard) |
| timestamp | int | Yes | Unix timestamp (seconds), used for expiry check (recommended ±300s window) |
| nonce | string\|null | No | Random string for replay protection; when unused, omit it or pass null (do not pass an empty string) |
| order_no | string\|null | No | API order no. (provide either this or out_order_no; at least one is required) |
| out_order_no | string\|null | No | Merchant order no. (provide either this or order_no; at least one is required) |
| sign | string | Yes | Signature (computed per the documented rules; the pay endpoint uses api_secret_pay) |
#### Request Example
```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
}
```
> Signature note: sign is computed over "all top-level fields of the request JSON except sign"; object/array fields are serialized with stable JSON.
### Response Fields and Example
- Unified response structure: `{ code, message, data }`; business failures usually still return HTTP 200.
| Field | Type | Required | Description |
|---|---|---|---|
| order_no | string | Yes | API order no. |
| out_order_no | string | Yes | Merchant order no. |
| amount | int | Yes | Reference order amount (= actual value × 10000 as an integer); reference only — credit by actual_amount |
| actual_amount | int\|null | No | End-user's actual paid amount (the primary basis for collection crediting and reconciliation; use it first; null if unknown) |
| fee_amount | int\|null | No | Fee (recomputed from the actual paid amount; null if unknown) |
| net_amount | int\|null | No | Net credited amount = actual paid − fee (null if unknown) |
| currency | string | Yes | Currency |
| status | string | Yes | Order status: pending (processing) / success / failed. Only success/failed are final states; unpaid-and-timed-out is no longer returned separately as expired and is normalized to pending. |
| channel_order_no | null | No | Always returns null. Use order_no or out_order_no for order queries and business correlation. |
| paid_at | string\|null | No | Payment success time (ISO8601; may be empty when not yet successful) |
| notify_status | string | Yes | Callback delivery status: pending/success/failed (not the same as the payment result) |
#### Response Example
```json
{
"code": 0,
"message": "ok",
"data": {
"order_no": "P20250101000001",
"out_order_no": "202501010001",
"amount": 10000,
"actual_amount": 9500,
"fee_amount": 500,
"net_amount": 9000,
"currency": "USDT",
"status": "success",
"channel_order_no": null,
"paid_at": "2025-01-01T12:15:00Z",
"notify_status": "success"
}
}
```
### Additional Notes
- Provide at least one of order_no/out_order_no.
- The final states for status are success/failed; pending means processing (including unpaid-and-timed-out, which is normalized to pending).
- Collection callbacks are sent to the merchant only when the order succeeds (success); non-success states such as failure/timeout are not called back, so the merchant should learn the final result via this order query endpoint.
### Common Errors
- 100104 Invalid signature
- 300301 Order does not exist
## Payout: Create Order (payout/create)
- Method: `POST`
- Endpoint: `<BASE_URL>/merchant/payout/create`
- Purpose: Create a payout order and freeze the balance (a successful creation does not mean the payout will ultimately succeed).
### Request Details
- Content-Type: `application/json`
- Required fields: merchant_no, api_key, timestamp, sign, out_payout_no, amount, currency, pay_method, notify_url, account_no
- Optional fields: nonce, country, account_name, bank_name, bank_code, remark, client_ip, extra
| Field | Type | Required | Description |
|---|---|---|---|
| merchant_no | string | Yes | Merchant no. (obtained from the merchant dashboard) |
| api_key | string | Yes | API Key (obtained from the merchant dashboard) |
| timestamp | int | Yes | Unix timestamp (seconds), used for expiry check (recommended ±300s window) |
| nonce | string\|null | No | Random string for replay protection; when unused, omit it or pass null (do not pass an empty string) |
| out_payout_no | string | Yes | Merchant payout no. (idempotency key; must be unique per merchant) |
| amount | int | Yes | Payout amount (= actual value × 10000 as an integer, e.g. pass 12345 for 1.2345). Must be a positive JSON integer; strings (e.g. "12345") or decimals are not accepted; invalid input returns code=100001 (HTTP 200, with the specific field validation error in message). |
| currency | string | Yes | Currency (e.g. PHP/USDT; used for validation and routing) |
| pay_method | string | Yes | Payment method (choose from the payment-method dictionary, e.g. gcash/maya; for crypto use the chain name such as trc20/erc20) |
| country | string\|null | No | Country ISO code (e.g. PH; required for fiat, optional for crypto) |
| notify_url | string | Yes | Callback URL (the service sends the result to this URL when the payout reaches a final state) |
| account_name | string\|null | No | Payee name / account name (interpreted per channel type; nullable for on-chain address types) |
| account_no | string | Yes | Payee account / address (interpreted per channel type; e.g. bank card number / on-chain address) |
| bank_name | string\|null | No | Bank / chain name (interpreted per channel type; nullable) |
| bank_code | string\|null | No | Bank code (corresponds to the code returned by the "Query Available Banks" endpoint; may be omitted when no bank selection is needed; if bank_name is also provided, the result resolved from bank_code takes precedence) |
| remark | string\|null | No | Remark (can be used for merchant custom notes / reconciliation) |
| client_ip | string\|null | No | End-user IP (can be used for risk control) |
| extra | object\|null | No | Extension field (included in the signature; the object is serialized with stable JSON). ⚠️ It is strongly recommended to include the end-user info extra.customer (see the "Extension info and channel requirements" section below) |
| extra.customer | object | No | End-user info (optional). If omitted, the request is not rejected solely because this field is missing; however, some payment methods require name/phone and the order may fail without them, so including them is recommended. The service validates the format and maps the fields (a full name is split into first/last): provide the full name (or first_name+last_name), email, and phone |
| sign | string | Yes | Signature (computed per the documented rules; the payout endpoint uses api_secret_payout) |
#### Request Example
```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": "San Zhang (optional)",
"account_no": "09171234567",
"bank_name": "Optional: fill in the bank name for bank-card payouts (can be omitted for wallet types such as gcash)",
"client_ip": "1.2.3.4",
"remark": "Merchant withdrawal (optional)",
"extra": {
"user_id": "u123456",
"customer": {
"first_name": "San",
"last_name": "Zhang",
"email": "user@example.com",
"phone": "13800000000"
}
}
}
```
> Signature note: sign is computed over "all top-level fields of the request JSON except sign"; object/array fields are serialized with stable JSON.
### Response Fields and Example
- Unified response structure: `{ code, message, data }`; business failures usually still return HTTP 200.
| Field | Type | Required | Description |
|---|---|---|---|
| payout_no | string | Yes | API payout no. (used for subsequent query and reconciliation) |
| out_payout_no | string | Yes | Merchant payout no. (echoed as-is, to help the merchant link its business) |
| amount | int | Yes | Payout amount (= actual value × 10000 as an integer, e.g. 12345 for 1.2345) |
| currency | string | Yes | Currency (echoes the requested currency as-is, e.g. PHP/USDT) |
| status | string | Yes | Order status: pending on creation (processing; payouts always enter the processing flow first); success/failed are final states. |
| review_status | string\|null | No | Payout approval status (when merchant payout approval is enabled): pending (under review) / approved / rejected; null when no approval is required. |
| fee_amount | int\|null | No | Fee (nullable) |
| freeze_amount | int\|null | No | Frozen amount (nullable; recommended convention amount + fee_amount) |
#### Response Example
```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
}
}
```
### Additional Notes
- A successful creation means "accepted and balance frozen". A payout usually goes through review/dispatch and other steps; the final result is determined by the async notification and the query.
- Idempotency key: out_payout_no is globally unique per merchant; a repeated order with identical parameters is treated as an idempotent success.
- [End-user info (strongly recommended)] Some payment methods require the end-user name and phone (email may be optional); missing them may cause the order to fail. Use these generic fields: account_name for the payee or account name, or the full name in extra.customer.name (or explicit extra.customer.first_name+last_name); email — extra.customer.email (or extra.email); phone — extra.customer.phone (or extra.phone). The service validates and maps these fields, so the merchant does not need separate integrations for different payment methods.
- [Bank-type payout] When pay_method is a bank type, bank_code is required; its value uses the code returned by the "Query Available Banks (payout/banks/query)" endpoint; an invalid code returns 300407.
### Common Errors
- 100104 Invalid signature
- 300201 Payout order idempotency conflict
- 300501 Insufficient balance
- 300404 No payment method is available for the request
- 300403 Fee rate not configured
- 300401 Payment method unavailable / does not exist
- 100001 Parameter validation failed (e.g. notify_url points to an internal network / illegal protocol)
- 300402 Payment method configuration unavailable
- 300406 Missing required order parameter (e.g. bank_code)
- 300407 Invalid bank code
## Payout: Query Order (payout/query)
- Method: `POST`
- Endpoint: `<BASE_URL>/merchant/payout/query`
- Purpose: Query a payout order status by API payout no. or merchant payout no.
### Request Details
- Content-Type: `application/json`
- Required fields: merchant_no, api_key, timestamp, sign, payout_no/out_payout_no (either one)
- Optional fields: nonce
| Field | Type | Required | Description |
|---|---|---|---|
| merchant_no | string | Yes | Merchant no. (obtained from the merchant dashboard) |
| api_key | string | Yes | API Key (obtained from the merchant dashboard) |
| timestamp | int | Yes | Unix timestamp (seconds), used for expiry check (recommended ±300s window) |
| nonce | string\|null | No | Random string for replay protection; when unused, omit it or pass null (do not pass an empty string) |
| payout_no | string\|null | No | API payout no. (provide either this or out_payout_no; at least one is required) |
| out_payout_no | string\|null | No | Merchant payout no. (provide either this or payout_no; at least one is required) |
| sign | string | Yes | Signature (computed per the documented rules; the payout endpoint uses api_secret_payout) |
#### Request Example
```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"
}
```
> Signature note: sign is computed over "all top-level fields of the request JSON except sign"; object/array fields are serialized with stable JSON.
### Response Fields and Example
- Unified response structure: `{ code, message, data }`; business failures usually still return HTTP 200.
| Field | Type | Required | Description |
|---|---|---|---|
| payout_no | string | Yes | API payout no. |
| out_payout_no | string | Yes | Merchant payout no. |
| amount | int | Yes | Payout amount (= actual value × 10000 as an integer, e.g. 12345 for 1.2345) |
| currency | string | Yes | Currency |
| status | string | Yes | Order status: pending (processing) / success / failed; only success/failed are final states. |
| sub_state | string\|null | No | Processing sub-state (normalized): accepted / reviewing / processing (payout in progress) / verifying (payout result being verified) (non-final, keep waiting for the callback or polling); null in a final state |
| channel_order_no | null | No | Always returns null. Use payout_no or out_payout_no for order queries and business correlation. |
| finished_at | string\|null | No | Completion time (ISO8601; may be empty when not yet in a final state) |
| failed_reason | string\|null | No | Failure reason summary (may have a value only on failure) |
| notify_status | string | Yes | Callback delivery status: pending/success/failed (not the same as the payout result) |
#### Response Example
```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": null,
"finished_at": "2025-01-01T13:00:00Z",
"failed_reason": null,
"notify_status": "success"
}
}
```
### Additional Notes
- Provide at least one of payout_no/out_payout_no.
- The final states for status are success/failed; pending means processing (keep waiting for the callback or polling the order query).
- sub_state is a normalized breakdown of the processing state (verifying means the payout result is being verified, still non-final); sub_state is only for display/troubleshooting — base business decisions on the final status (success/failed).
### Common Errors
- 100104 Invalid signature
- 300301 Order does not exist
## Payout: Query Available Banks (payout/banks/query)
- Method: `POST`
- Endpoint: `<BASE_URL>/merchant/payout/banks/query`
- Purpose: Query the currently available bank list by country + payment method (the valid values of bank_code for bank-type payout orders).
### Request Details
- Content-Type: `application/json`
- Required fields: merchant_no, api_key, timestamp, sign, pay_method
- Optional fields: nonce, country, currency
| Field | Type | Required | Description |
|---|---|---|---|
| merchant_no | string | Yes | Merchant no. (obtained from the merchant dashboard) |
| api_key | string | Yes | API Key (obtained from the merchant dashboard) |
| timestamp | int | Yes | Unix timestamp (seconds), used for expiry check (recommended ±300s window) |
| nonce | string\|null | No | Random string for replay protection; when unused, omit it or pass null (do not pass an empty string) |
| pay_method | string | Yes | Payment method (bank type such as bank; wallet types such as gcash usually return an empty list) |
| country | string\|null | No | Country ISO code (e.g. PH) |
| currency | string\|null | No | Currency (e.g. PHP) |
| sign | string | Yes | Signature (computed per the documented rules; the payout endpoint uses api_secret_payout) |
#### Request Example
```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"
}
```
> Signature note: sign is computed over "all top-level fields of the request JSON except sign"; object/array fields are serialized with stable JSON.
### Response Fields and Example
- Unified response structure: `{ code, message, data }`; business failures usually still return HTTP 200.
| Field | Type | Required | Description |
|---|---|---|---|
| banks | array | Yes | Available bank list [{code,name}]; code is the valid value of bank_code for payout orders. |
#### Response Example
```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"
}
]
}
}
```
### Additional Notes
- The available bank list may change over time; query it shortly before placing an order or use a short-lived cache.
- Pure wallet payment methods (pay_method=gcash/maya themselves) do not need a destination selection and return empty banks; for bank-type (pay_method=bank), banks may include e-wallets (such as GCASH/MAYA) as payout destinations, and bank_code may take their code.
### Common Errors
- 100104 Invalid signature
## Payout: Query Payment Proof (payout/proof/query)
- Method: `POST`
- Endpoint: `<BASE_URL>/merchant/payout/proof/query`
- Purpose: Query the payment-proof URL of a successfully paid-out order (not all payment methods/currencies provide proof).
### Request Details
- Content-Type: `application/json`
- Required fields: merchant_no, api_key, timestamp, sign, payout_no/out_payout_no (either one)
- Optional fields: nonce
| Field | Type | Required | Description |
|---|---|---|---|
| merchant_no | string | Yes | Merchant no. (obtained from the merchant dashboard) |
| api_key | string | Yes | API Key (obtained from the merchant dashboard) |
| timestamp | int | Yes | Unix timestamp (seconds), used for expiry check (recommended ±300s window) |
| nonce | string\|null | No | Random string for replay protection; when unused, omit it or pass null (do not pass an empty string) |
| payout_no | string\|null | No | API payout no. (provide either this or out_payout_no; at least one is required) |
| out_payout_no | string\|null | No | Merchant payout no. (provide either this or payout_no; at least one is required) |
| sign | string | Yes | Signature (computed per the documented rules; the payout endpoint uses api_secret_payout) |
#### Request Example
```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"
}
```
> Signature note: sign is computed over "all top-level fields of the request JSON except sign"; object/array fields are serialized with stable JSON.
### Response Fields and Example
- Unified response structure: `{ code, message, data }`; business failures usually still return HTTP 200.
| Field | Type | Required | Description |
|---|---|---|---|
| payout_no | string | Yes | API payout no. |
| out_payout_no | string | Yes | Merchant payout no. |
| proof_url | string | Yes | Payment-proof URL (has an access expiry, see expires_in; use it immediately and do not persist the URL) |
| expires_in | int\|null | No | Proof URL validity (seconds, decided by the channel, e.g. 1800=30 minutes; null means the channel did not declare it) |
| queried_at | string\|null | No | Query time returned by the service (nullable; the format may not be ISO8601) |
#### Response Example
```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"
}
}
```
### Additional Notes
- Only successfully paid-out orders (status=success) can query proof; processing/failed orders return 300409.
- Not all channels/currencies provide proof; some channels only support querying orders from the last few days (returns 300409 beyond the window).
- The proof URL has an access expiry (expires_in seconds); download and re-store the file rather than saving the URL.
### Common Errors
- 100104 Invalid signature
- 300301 Order does not exist
- 300408 Channel does not support proof query
- 300409 Proof temporarily unavailable
## Payout: Query Payment Receipt Image (payout/receipt/query)
- Method: `POST`
- Endpoint: `<BASE_URL>/merchant/payout/receipt/query`
- Purpose: Query/generate a payment receipt image (PNG) for a payout order. Only payout orders with status=success can be generated; render-once persists and reuses it.
### Request Details
- Content-Type: `application/json`
- Required fields: merchant_no, api_key, timestamp, sign, payout_no/out_payout_no (either one)
- Optional fields: nonce, lang, inline
| Field | Type | Required | Description |
|---|---|---|---|
| merchant_no | string | Yes | Merchant no. (obtained from the merchant dashboard) |
| api_key | string | Yes | API Key (obtained from the merchant dashboard) |
| timestamp | int | Yes | Unix timestamp (seconds), used for expiry check (recommended ±300s window) |
| nonce | string\|null | No | Random string for replay protection; when unused, omit it or pass null (do not pass an empty string) |
| payout_no | string\|null | No | API payout no. (provide either this or out_payout_no; at least one is required) |
| out_payout_no | string\|null | No | Merchant payout no. (provide either this or payout_no; at least one is required) |
| lang | string\|null | No | Receipt language (optional, enum: en / zh-CN / zh-TW; when omitted it is auto-derived from the order country) |
| inline | int\|null | No | When set to 1, returns the base64 image data directly (image_base64 + mime); when omitted or set to 0, returns a time-limited signed download link (receipt_url) |
| sign | string | Yes | Signature (computed per the documented rules; the payout endpoint uses api_secret_payout) |
#### Request Example
```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
}
```
> Signature note: sign is computed over "all top-level fields of the request JSON except sign"; object/array fields are serialized with stable JSON.
### Response Fields and Example
- Unified response structure: `{ code, message, data }`; business failures usually still return HTTP 200.
| Field | Type | Required | Description |
|---|---|---|---|
| payout_no | string | Yes | API payout no. |
| out_payout_no | string | Yes | Merchant payout no. |
| lang | string | Yes | Language actually used for the receipt (en / zh-CN / zh-TW) |
| receipt_url | string\|null | No | Receipt image download URL (relative path, e.g. /api/open/v1/payout/receipt/file?token=…; prepend your openapi_base to form the full download URL; a GET downloads it; returned only when inline=0 or omitted; the link has an expiry, see expires_in; use it immediately and do not persist the URL) |
| expires_in | int\|null | No | Download link validity (seconds, e.g. 3600=1 hour; null means no declared expiry); meaningful only when inline=0 or omitted |
| mime | string\|null | No | Image MIME type (e.g. image/png); returned only when inline=1 |
| image_base64 | string\|null | No | Base64-encoded receipt image data (without the data URI prefix); returned only when inline=1 |
#### Response Example
```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
}
}
```
### Additional Notes
- Only successfully paid-out orders (status=success) can generate a receipt; a non-success status returns 300410.
- render-once: after the first generation the image is persisted and reused; repeated queries with the same parameters return the cached result directly.
- When lang is omitted, the service selects a language from the order country (e.g. PH → en); an explicit value overrides this.
- When inline=1, the response body directly contains the Base64 image (image_base64 + mime), suitable for immediate server-side processing; when inline=0 (default), it returns a time-limited signed receipt_url download link, suitable for frontend redirect/embed.
- The download link has an expiry (expires_in seconds); do not persist the URL; for long-term retention, download the image and re-store it in your own storage.
### Common Errors
- 100104 Invalid signature
- 300301 Order does not exist
- 300410 Payout receipt temporarily unavailable (order is not success)
- 300411 Receipt generation failed
## Common: Query Available Payment Methods (pay-methods/query)
- Method: `POST`
- Endpoint: `<BASE_URL>/merchant/pay-methods/query`
- Purpose: Query the available payment-method dictionary (the valid values of pay_method/country for placing orders).
### Request Details
- Content-Type: `application/json`
- Required fields: merchant_no, api_key, timestamp, sign
- Optional fields: nonce, country
| Field | Type | Required | Description |
|---|---|---|---|
| merchant_no | string | Yes | Merchant no. (obtained from the merchant dashboard) |
| api_key | string | Yes | API Key (obtained from the merchant dashboard) |
| timestamp | int | Yes | Unix timestamp (seconds), used for expiry check (recommended ±300s window) |
| nonce | string\|null | No | Random string for replay protection; when unused, omit it or pass null (do not pass an empty string) |
| country | string\|null | No | Country ISO code (e.g. PH; when omitted, all are returned) |
| sign | string | Yes | Signature (computed per the documented rules; this endpoint uses api_secret_pay) |
#### Request Example
```json
{
"merchant_no": "<YOUR_MERCHANT_NO>",
"api_key": "<YOUR_API_KEY>",
"timestamp": 1736073600,
"nonce": "random-xyz",
"sign": "<SIGN>",
"country": "PH"
}
```
> Signature note: sign is computed over "all top-level fields of the request JSON except sign"; object/array fields are serialized with stable JSON.
### Response Fields and Example
- Unified response structure: `{ code, message, data }`; business failures usually still return HTTP 200.
| Field | Type | Required | Description |
|---|---|---|---|
| methods | array | Yes | Payment method list [{pay_method,name,country,currency}]; pay_method is the valid value for the order endpoints |
#### Response Example
```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"
}
]
}
}
```
### Additional Notes
- Returns the currently available payment-method dictionary; whether a payment method can currently be used is determined by the actual response of the order-creation endpoint (300404 is returned when none is available).
### Common Errors
- 100104 Invalid signature
## Common: Query Account Balance (balance/query)
- Method: `POST`
- Endpoint: `<BASE_URL>/merchant/balance/query`
- Purpose: Query the available/frozen balance of each currency in the merchant transaction account.
### Request Details
- Content-Type: `application/json`
- Required fields: merchant_no, api_key, timestamp, sign
- Optional fields: nonce, currency
| Field | Type | Required | Description |
|---|---|---|---|
| merchant_no | string | Yes | Merchant no. (obtained from the merchant dashboard) |
| api_key | string | Yes | API Key (obtained from the merchant dashboard) |
| timestamp | int | Yes | Unix timestamp (seconds), used for expiry check (recommended ±300s window) |
| nonce | string\|null | No | Random string for replay protection; when unused, omit it or pass null (do not pass an empty string) |
| currency | string\|null | No | Currency (e.g. PHP; when omitted, all currencies are returned) |
| sign | string | Yes | Signature (computed per the documented rules; this endpoint uses api_secret_pay) |
#### Request Example
```json
{
"merchant_no": "<YOUR_MERCHANT_NO>",
"api_key": "<YOUR_API_KEY>",
"timestamp": 1736073600,
"nonce": "random-xyz",
"sign": "<SIGN>",
"currency": "PHP"
}
```
> Signature note: sign is computed over "all top-level fields of the request JSON except sign"; object/array fields are serialized with stable JSON.
### Response Fields and Example
- Unified response structure: `{ code, message, data }`; business failures usually still return HTTP 200.
| Field | Type | Required | Description |
|---|---|---|---|
| balances | array | Yes | Balance list [{currency,available,frozen}]; amount = actual value × 10000 as an integer |
#### Response Example
```json
{
"code": 0,
"message": "ok",
"data": {
"balances": [
{
"currency": "PHP",
"available": 12345600,
"frozen": 50000
}
]
}
}
```
### Additional Notes
- available is the available balance, frozen is the frozen amount (including in-flight payout freezes); both are integers in the minor unit.
### Common Errors
- 100104 Invalid signature
## Common: Service Version (GET /version)
- Method: `GET`
- Endpoint: `<BASE_URL>/version`
- Purpose: Query the server-side OpenAPI version (no authentication required); all /api/open/ responses carry an X-Api-Version header.
### Request Details
- Content-Type: `application/json`
- Required fields: None
- Optional fields: None
_None_
#### Request Example
```json
{}
```
### Response Fields and Example
- Unified response structure: `{ code, message, data }`; business failures usually still return HTTP 200.
| Field | Type | Required | Description |
|---|---|---|---|
| major | string | Yes | Major version (path prefix, e.g. v1) |
| version | string | Yes | Semantic version (major.minor.patch) |
| base_path | string | Yes | OpenAPI path prefix |
#### Response Example
```json
{
"code": 0,
"message": "ok",
"data": {
"major": "v1",
"version": "1.2.0",
"base_path": "/api/open/v1"
}
}
```
### Additional Notes
- No authentication required; used for liveness probing and version canary troubleshooting.
## Payout: Download Payment Receipt File (GET /payout/receipt/file)
- Method: `GET`
- Endpoint: `<BASE_URL>/payout/receipt/file`
- Purpose: Download the payout payment receipt image (binary PNG). This endpoint is not called directly; it is derived from the receipt_url returned by "Query Payment Receipt Image (payout/receipt/query)" — the merchant prepends its dedicated domain and simply performs a GET to download.
### Request Details
- Content-Type: `application/json`
- Required fields: token
- Optional fields: None
| Field | Type | Required | Description |
|---|---|---|---|
| token | string | Yes | Download token (query parameter). Issued by payout/receipt/query (the receipt_url returned when inline=0 / omitted); it embeds the payout no. / merchant / language / expiry and is time-limited; do not construct it yourself or persist it long-term. |
#### Request Example
```json
{}
```
> Signature note: sign is computed over "all top-level fields of the request JSON except sign"; object/array fields are serialized with stable JSON.
### Response Fields and Example
- Unified response structure: `{ code, message, data }`; business failures usually still return HTTP 200.
_None_
#### Response Example
```json
{}
```
### Additional Notes
- This endpoint returns a **binary image** (`Content-Type: image/png`, `content-disposition: inline`), not a `{code,message,data}` JSON envelope; a successful download yields the PNG byte stream directly.
- Usage: first call "Query Payment Receipt Image (payout/receipt/query)" (inline=0 or omitted) to obtain `receipt_url` (a relative path), prepend your openapi_base to form the full URL, then simply `GET` that URL to download.
- **The official SDK does not wrap this endpoint**: receipt_url is a time-limited signed download link — use any HTTP client to GET it yourself (no further signing/verification needed); an expired link (invalid token) returns `100001`, so just call payout/receipt/query again for a fresh link.
- The authentication differs from other endpoints: this endpoint does not use merchant_no/api_key/sign — it is authenticated solely by the `token` in the query (HMAC-issued), so signature fields are neither needed nor should be attached.
### Common Errors
- 100001 Invalid or expired token (call payout/receipt/query again for a fresh link)
- 300410 Payout receipt temporarily unavailable (order is not success)
## Test Sandbox: Manually Complete a Collection Test Order (pay/test/complete)
- Method: `POST`
- Endpoint: `<BASE_URL>/merchant/pay/test/complete`
- Purpose: [Test credentials only] Manually push a collection TEST order to a final state (success/failed) for integration testing without a live payment flow; production credentials are rejected.
### Request Details
- Content-Type: `application/json`
- Required fields: merchant_no, api_key, timestamp, sign, out_order_no/order_no (either one), result
- Optional fields: nonce, actual_amount
| Field | Type | Required | Description |
|---|---|---|---|
| merchant_no | string | Yes | Merchant no. (obtained from the merchant dashboard) |
| api_key | string | Yes | API Key (must be the **test** environment key; calling with a production key is rejected) |
| timestamp | int | Yes | Unix timestamp (seconds), used for expiry check (recommended ±300s window) |
| nonce | string\|null | No | Random string for replay protection; when unused, omit it or pass null (do not pass an empty string) |
| out_order_no | string\|null | No | Merchant order no. (provide either this or order_no; at least one is required) |
| order_no | string\|null | No | API order no. (provide either this or out_order_no; at least one is required) |
| result | string | Yes | Desired final state: success or failed |
| actual_amount | int\|null | No | End-user actual paid amount (= actual value × 10000 as an integer, optional; effective only when result=success — defaults to the order amount if omitted; ignored when result=failed) |
| sign | string | Yes | Signature (computed per the documented rules; this endpoint uses the api_secret_pay test credential) |
#### Request Example
```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,
"result": "success",
"actual_amount": 10000
}
```
> Signature note: sign is computed over "all top-level fields of the request JSON except sign"; object/array fields are serialized with stable JSON.
### Response Fields and Example
- Unified response structure: `{ code, message, data }`; business failures usually still return HTTP 200.
| Field | Type | Required | Description |
|---|---|---|---|
| ok | bool | Yes | Whether the operation succeeded (always true; failures are expressed via code) |
| order_no | string | Yes | API order no. |
| status | string | Yes | The order final state after pushing: success/failed |
#### Response Example
```json
{
"code": 0,
"message": "ok",
"data": {
"ok": true,
"order_no": "P20250101000001",
"status": "success"
}
}
```
### Additional Notes
- [Test credentials only] Only test-environment credentials (env=test) can call this; production credentials return 100001 (only test credentials can call the test-complete endpoint).
- Applicable only to test orders (is_test=1): orders created with test credentials are marked is_test=1, use the mock flow, do not move real funds, and are excluded from statistics. Calling it on a non-test order, a non-existent order, or an already-final order returns 100001 (order not found / non-test order cannot be completed manually / order already in a final state).
- After setting it to success, the merchant callback is enqueued and delivered as usual (the sandbox "fires callbacks too"), which is useful for testing callback verification and crediting logic; the pushed result is also queryable via pay/query.
### Common Errors
- 100104 Invalid signature
- 100001 Test credentials only / order not found / not a test order / order already final (message carries the specific reason)
## Test Sandbox: Manually Complete a Payout Test Order (payout/test/complete)
- Method: `POST`
- Endpoint: `<BASE_URL>/merchant/payout/test/complete`
- Purpose: [Test credentials only] Manually push a payout TEST order to a final state (success/failed) for integration testing without a live payout flow; production credentials are rejected.
### Request Details
- Content-Type: `application/json`
- Required fields: merchant_no, api_key, timestamp, sign, out_payout_no/payout_no (either one), result
- Optional fields: nonce
| Field | Type | Required | Description |
|---|---|---|---|
| merchant_no | string | Yes | Merchant no. (obtained from the merchant dashboard) |
| api_key | string | Yes | API Key (must be the **test** environment key; calling with a production key is rejected) |
| timestamp | int | Yes | Unix timestamp (seconds), used for expiry check (recommended ±300s window) |
| nonce | string\|null | No | Random string for replay protection; when unused, omit it or pass null (do not pass an empty string) |
| out_payout_no | string\|null | No | Merchant payout no. (provide either this or payout_no; at least one is required) |
| payout_no | string\|null | No | API payout no. (provide either this or out_payout_no; at least one is required) |
| result | string | Yes | Desired final state: success or failed |
| sign | string | Yes | Signature (computed per the documented rules; this endpoint uses the api_secret_payout test credential) |
#### Request Example
```json
{
"merchant_no": "<YOUR_MERCHANT_NO>",
"api_key": "<YOUR_API_KEY>",
"timestamp": 1736073600,
"nonce": "random-xyz",
"sign": "<SIGN>",
"out_payout_no": "WD202501010001",
"payout_no": null,
"result": "success"
}
```
> Signature note: sign is computed over "all top-level fields of the request JSON except sign"; object/array fields are serialized with stable JSON.
### Response Fields and Example
- Unified response structure: `{ code, message, data }`; business failures usually still return HTTP 200.
| Field | Type | Required | Description |
|---|---|---|---|
| ok | bool | Yes | Whether the operation succeeded (always true; failures are expressed via code) |
| payout_no | string | Yes | API payout no. |
| status | string | Yes | The payout final state after pushing: success/failed |
#### Response Example
```json
{
"code": 0,
"message": "ok",
"data": {
"ok": true,
"payout_no": "W20250101000001",
"status": "success"
}
}
```
### Additional Notes
- [Test credentials only] Only test-environment credentials (env=test) can call this; production credentials return 100001 (only test credentials can call the test-complete endpoint). Payout test completion has no actual_amount parameter (payout uses the order amount).
- Applicable only to test orders (is_test=1): test orders never move real funds (the freeze at creation is not unfrozen here), do not enter the live payment flow, and are excluded from statistics. Calling it on a non-test order, a non-existent order, or an already-final order returns 100001 (order not found / non-test order cannot be completed manually / order already in a final state).
- Both payout final states (success/failed) enqueue and deliver the merchant callback as usual (the sandbox "fires callbacks too"), useful for testing payout callback verification; the pushed result is also queryable via payout/query.
### Common Errors
- 100104 Invalid signature
- 100001 Test credentials only / order not found / not a test order / order already final (message carries the specific reason)
## Signature and Common Fields
Common fields (recommended to place in the JSON request body): `merchant_no`, `api_key`, `timestamp`, `nonce` (optional), `sign`.
Recommended algorithm: HMAC-SHA256. Sort all signing fields (excluding sign) in ascending ASCII order by field name, join them as `key=value` with `&`, append `&secret=...` at the end, run HMAC-SHA256 over the raw string, and use the lowercase hexadecimal result as sign.
The timestamp window is **±300 seconds** (a request whose time differs from the server by more than this returns `100101`). Replay protection: when `nonce` is provided it is de-duplicated by nonce, otherwise a "signature fingerprint" fallback is used; both de-duplication records are retained for **300 seconds**, i.e. the same nonce / the same signed request cannot be repeated within 300 seconds (a repeat returns `100103`). Generate a globally unique nonce per request (e.g. a UUID) and ensure it is not reused within at least 300 seconds.
> Tip: the signing rules above are already implemented in the Official SDKs — call the SDK directly when integrating, no need to hand-write signing or stable serialization.
## Amount Convention
All amounts follow the convention **amount = actual value × 10000 as an integer** (minor accounting unit 0.0001), e.g. pass `12345` for `1.2345`, transmitted as integers to avoid floating-point errors. This applies to every request, response and callback amount field, regardless of currency (do not interpret it as any specific currency unit).
## Test Credential Sandbox
**Test-environment credentials (env=test)** are available for integration testing: collection/payout orders created with a test credential are marked as test orders (`is_test=1`), **short-circuit through a mock — no live payment flow, no real funds moved, and excluded from statistics/reconciliation** — while **callbacks are still fired** (so you can test callback verification and crediting logic).
Since test orders never enter the live payment flow, two **manual-completion endpoints** let you push a test order to a final state yourself: `POST /merchant/pay/test/complete` (collection, uses api_secret_pay, optional actual_amount) and `POST /merchant/payout/test/complete` (payout, uses api_secret_payout), with `result` of `success`/`failed`; see the corresponding endpoint sections above.
> Note: the manual-completion endpoints are **callable only with a test credential (env=test)**; a production credential is always rejected (returns `100001`, message "only test credentials can call the test-complete endpoint"); and they apply only to test orders (`is_test=1`) — calling them on a production order, a non-existent order, or an already-final order also returns `100001`. **Production (env=prod) credentials have no sandbox behavior: production orders cannot be completed manually.**
## Callback Details
When an order reaches a final state, the service sends the collection/payout result via `POST application/json` to the `notify_url` provided when the order was created.
The callback also carries signature fields; the merchant should verify the signature using the respective secret for the corresponding business (pay/payout).
**Collection callback timing**: Collection sends a callback to the merchant only when the order succeeds (`status=success`); non-success states such as failed/timeout trigger no callback, and the merchant should obtain the final result via the order query API (`pay/query`).
(Payout is not subject to this restriction: Payout sends a callback at both final states `success/failed`.)
**Success acknowledgement rule**: After processing successfully, the merchant must return HTTP 200 with a response body of `success` or `ok` (case-insensitive);
JSON formats are also accepted: `{"success":true}` / `{"code":0}` / `{"message":"success"}` / `{"message":"ok"}`.
If the response does not follow this rule (even an HTTP 200 with a different body, such as an empty response or an HTML error page), the callback is treated as failed,
retries it per policy, and triggers an alert when consecutive failures reach the threshold.
### Collection Callback Fields
| Field | Type | Required | Description |
|---|---|---|---|
| merchant_no | string | Yes | Merchant no. |
| order_no | string | Yes | API order no. |
| out_order_no | string | Yes | Merchant order no. |
| amount | int | Yes | Reference order amount (= actual value × 10000 as an integer); reference only — credit by actual_amount |
| actual_amount | int\|null | No | End-user's actual paid amount (the primary basis for collection crediting and reconciliation; use it first; null if unknown) |
| fee_amount | int\|null | No | Fee (recomputed from the actual paid amount; null if unknown) |
| net_amount | int\|null | No | Net credited amount = actual paid − fee (null if unknown) |
| currency | string | Yes | Currency |
| status | string | Yes | Normalized status; collection only sends a callback on success, so this is always success |
| channel_order_no | null | No | Always returns null; use order_no or out_order_no for order correlation. Because it is null, this field is excluded from the signature. |
| paid_at | string\|null | No | Payment success time (ISO8601; may be empty if not successful) |
| sign | string | Yes | Signature (computed with api_secret_pay for collection callbacks) |
### Payout Callback Fields
| Field | Type | Required | Description |
|---|---|---|---|
| merchant_no | string | Yes | Merchant no. |
| payout_no | string | Yes | API payout no. |
| out_payout_no | string | Yes | Merchant payout no. |
| amount | int | Yes | Payout amount (= actual value × 10000 as an integer) |
| currency | string | Yes | Currency |
| status | string | Yes | Normalized status: success/failed (payout sends a callback at every final state) |
| fee_amount | int\|null | No | Fee (null if unknown) |
| channel_order_no | null | No | Always returns null; use payout_no or out_payout_no for order correlation. Because it is null, this field is excluded from the signature. |
| finished_at | string\|null | No | Completion time (ISO8601) |
| failed_reason | string\|null | No | Failure reason (returned when failed) |
| sign | string | Yes | Signature (computed with api_secret_payout for payout callbacks) |
> Note: the payout callback body does **not** contain `notify_status`. `notify_status` is returned only by the order query API and never appears in the callback body.
### Refund Callback Fields
| Field | Type | Required | Description |
|---|---|---|---|
| merchant_no | string | Yes | Merchant no. |
| order_no | string\|null | No | API order no. of the source collection order being refunded (may be null in edge cases) |
| out_order_no | string\|null | No | Merchant order no. of the source collection order being refunded (may be null in edge cases) |
| source_channel_order_no | null | No | Always returns null; the refund callback does not provide another order number for the source collection order. Because it is null, this field is excluded from the signature. |
| refund_no | string | Yes | API refund no. |
| out_refund_no | string | Yes | Merchant refund no. |
| amount | int | Yes | Refund amount (= actual value × 10000 as an integer) |
| currency | string | Yes | Currency |
| status | string | Yes | Normalized status: success/failed (refund sends a callback at every final state, consistent with the payout final-state set) |
| channel_order_no | null | No | Always returns null; use refund_no or out_refund_no for refund correlation. Because it is null, this field is excluded from the signature. |
| finished_at | string\|null | No | Completion time (ISO8601) |
| failed_reason | string\|null | No | Failure reason (a desensitized generic message; returned when failed, otherwise null) |
| sign | string | Yes | Signature (computed with api_secret_pay for refund callbacks, shared with collection) |
> Refund callback note: the callback body is **verified with `api_secret_pay`** (shared with collection, so no separate refund callback secret is needed); the verification algorithm is the same as collection/payout callbacks (see "Callback Signature Verification" below). `channel_order_no` / `source_channel_order_no` are always `null` and are **excluded from the signature**. `refund_no` / `out_refund_no` are the refund order numbers, while `order_no` / `out_order_no` link to the original collection order being refunded.
### Callback Signature Verification
The callback signature algorithm is **identical** to the request signature (see the "Signature and Common Fields" section):
1. Take all top-level fields of the callback JSON except `sign` and whose value is not `null`;
2. Sort by field name in ascending ASCII (code-point) order, and join them as `key=value` with `&`;
3. Append `&secret=<corresponding secret>`, compute HMAC-SHA256 over that raw string, and output lowercase hex;
4. Compare against the callback body's `sign` (use a constant-time comparison such as `crypto.timingSafeEqual` to prevent timing attacks).
Secret selection: **collection callbacks use `api_secret_pay`, payout callbacks use `api_secret_payout`**. Fields whose value is `null` (such as `channel_order_no`) are **excluded from the signature** (consistent with the request signature, which skips null).
> **Security red lines (must follow)**: (1) **Verify the signature first, then process the business and credit funds** — never act on callback content before verifying; (2) Credit/reconcile using `actual_amount` (the actual amount paid by the user) and `net_amount` (net credited = actual paid − fee), **never `amount`** (only a reference order amount); (3) Callbacks may be delivered more than once due to retries — handle them **idempotently** by `out_order_no` / `out_payout_no`; a duplicate notification must not credit twice; (4) The callback source IP is **not fixed** — **do not restrict the callback source by IP allowlist; signature verification is the only trustworthy check**.
### Retry / Timeout / Security
- **Timeout**: the callback service waits `8000ms` for the merchant response; no response counts as a failure for this attempt.
- **Retry**: up to `6` attempts (including the first), with backoff intervals of approximately `1min / 2min / 5min / 10min / 30min / 60min` (the last value is used beyond that); if it still fails after the cap, the callback task is set to `failed`, and the merchant can fall back to the order query API to obtain the final state.
- **No redirect following**: the callback service does not follow 30x redirects for `notify_url`.
- **SSRF protection**: a `notify_url` pointing to internal / loopback / link-local addresses is blocked, counted as a failure with **no retry** (the address will not change on its own); at order creation, such a `notify_url` also returns `100001`.
### Field Conventions and External Contract
- Callback amounts follow the same convention as requests: actual value × 10000 as an integer.
- `status` is the unified status returned by the API: collection is always `success`; payout is `success/failed`.
### Collection vs Payout Differences
- **Trigger timing**: collection triggers a callback **only on `success`**; payout triggers at **both** final states `success/failed`.
- **Field differences**: collection includes `actual_amount` / `net_amount` / `paid_at`; payout includes `finished_at` / `failed_reason`.
## IP Allowlist/Blocklist Details
- The allowlist is disabled by default (all source IPs are allowed to access the OpenAPI).
- Once the policy is enabled, a blocklist match is rejected outright; when an allowlist exists, access is allowed only if it matches the allowlist.
- The exported document does not include the merchant real allowlist configuration; please check it in the merchant console.
## Error Code Reference
All business errors return **HTTP 200** with the envelope `{ code, message, data }`; **HTTP 4xx/5xx is never used to express a business failure** (a transport-layer 4xx/5xx only indicates a network/gateway issue — investigate the link or retry). Each endpoint Common Errors list shows only high-frequency items; this table is the complete reference. Authentication errors (`100xxx`) are common to all authenticated endpoints.
| Code | Meaning | HTTP | Retryable | Notes / Troubleshooting |
|---|---|---|---|---|
| `100000` | Generic business failure / unified auth-failure code | 200 | Depends | When the order is not accepted, it is set to failed in real time — do not retry with the same out_order_no; during auth it is a unified authentication-failed message |
| `100001` | Parameter validation failed | 200 | No | message carries the specific field validation error (in English, e.g. `"amount" must be a number`); amount must be a JSON integer — strings/decimals return this code. Fix params and resend |
| `100101` | Request expired | 200 | Yes | timestamp is outside the ±300s window; calibrate local time and retry |
| `100102` | IP not in allowlist | 200 | No | Add the source IP to the allowlist in the merchant console and retry |
| `100103` | Replay / duplicate request | 200 | No | nonce or signature fingerprint repeated within 300s; use a new nonce (or change params) and retry |
| `100104` | Invalid signature | 200 | No | Check the secret and signing algorithm (pay uses api_secret_pay, payout uses api_secret_payout) |
| `100105` | IP in blocklist | 200 | No | Remove the source IP from the blocklist and retry |
| `100106` | Too many auth failures (rate limited) | 200 | Yes | Triggered when the same merchant_no + source IP fails auth 60 times within 60s; wait ~60s and retry |
| `200002` | Service account disabled | 200 | No | Contact your service provider |
| `210002` | Merchant disabled | 200 | No | Contact your service provider |
| `300101` | Collection idempotency conflict | 200 | No | out_order_no exists with mismatched params; align params or use a new order no. |
| `300201` | Payout idempotency conflict | 200 | No | out_payout_no exists with mismatched params; align params or use a new payout no. |
| `300301` | Order not found | 200 | No | Check the order no. (order_no / out_order_no / payout_no / out_payout_no) |
| `300401` | Payment method unavailable / missing | 200 | No | The payment method is unavailable or does not exist |
| `300402` | Payment method configuration unavailable | 200 | Yes | No valid configuration matched the request; change parameters or retry later |
| `300403` | Fee rate not configured | 200 | No | Contact your service provider to configure the fee rate, then retry |
| `300404` | No payment method is available for the request | 200 | Yes | No available combination for country + currency + pay_method; change parameters or retry later. Use `pay-methods/query` to list payment methods |
| `300405` | Missing required extra info (extra.customer) | 200 | No | Fill the fields listed in `data.missing_fields` under extra.customer |
| `300406` | Missing required order parameter (e.g. bank_code) | 200 | No | Fill the required field for that route and retry |
| `300407` | Invalid bank code | 200 | No | Use a code returned by Query Available Banks (payout/banks/query) |
| `300408` | Payment method does not support payout proof query | 200 | No | The payment method does not support proof queries |
| `300409` | Payout proof temporarily unavailable | 200 | Yes | Order is not successful / proof is not ready / outside the query window; confirm success and retry within the window |
| `300410` | Payout receipt temporarily unavailable (order not success) | 200 | Yes | Retry after the payout succeeds |
| `300411` | Receipt generation failed | 200 | Yes | Rendering error; retry later |
| `300501` | Insufficient balance | 200 | Yes | Available balance < frozen amount (amount + fee); top up and retry |
## Rate Limiting and Polling Guidance
- **Rate limiting only applies to auth failures**: once the same `merchant_no` + source IP accumulates 60 auth failures within 60 seconds, further requests are rejected with `100106` and recover as the window rolls (~60s). Successfully authenticated business requests have **no hard QPS cap**.
- **Do not retry auth failures without backoff** (signature error, clock skew, etc.), or you will easily hit `100106`; fix the cause before retrying.
- **Query polling guidance**: rely on the callback for the final state first; only poll the query API when no callback arrives, using exponential backoff (e.g. first delay 3–5s, then 5s → 15s → 30s → 60s up to the final state or a reasonable cap). Avoid high-frequency empty polling on non-final orders.
## Integration Notes
- **Credit by `actual_amount`**: collection credits and reconciles by the amount the user **actually paid**; use `actual_amount` (actual paid) and `net_amount` (net credited = actual paid − fee) from the callback / query, not `amount`, which is only a reference order amount and must not be used for crediting.
- **Amount limits**: min/max amounts for each `pay_method` are **validated in real time** and are not published as fixed values; an out-of-range order returns the corresponding error code (e.g. `300402` / `300404`). Do not hard-code amount ranges on the client.
- **Order validity**: use the `expire_at` returned by the order-creation endpoint instead of hard-coding a fixed duration. An unpaid timeout is normalized to `pending`; rely on the order query result.
- **Callback source**: the callback egress IP is not fixed; do not restrict the callback source by IP allowlist — use **signature verification** as the only trustworthy check (see Callback Signature Verification and the security red lines above).
=== API documentation ends ===
Now please begin. If there is any ambiguity about the fields or the flow, please ask me first before writing the code.