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=` 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): ``, ``, ``, ``, ``. 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 multi-language examples 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 `` as a placeholder). - API version: `v1` (server version 1.2.0; every response carries the `X-Api-Version` header). - Export date: 20260621 > 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 /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: - Merchant no.: ## Collection: Create Order (pay/create) - Method: `POST` - Endpoint: `/merchant/pay/create` - Purpose: Create a collection order; returns the platform 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 (integer in minor unit, precision 0.0001; scale up by 10000, 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 HTTP 400 (code=400). | | currency | string | Yes | Currency (e.g. PHP/USDT; used for validation and routing) | | pay_method | string | Yes | Payment method (provided by the platform, 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 platform pushes 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 platform cannot infer the end-user origin) | | 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). The platform no longer rejects an order due to its absence (no longer returns 300405); however, upstream channels for mainstream payment methods such as gcash/maya effectively require name/phone, and missing them will cause the upstream to reject the order and the order to fail, so it is strongly recommended to include it. When provided, the platform validates the format and auto-maps it to each channel's format (a full name is auto-split into first/last): you may 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": "", "api_key": "", "timestamp": 1736073600, "nonce": "random-xyz", "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 | Platform 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 (integer in minor unit, precision 0.0001; scaled up by 10000, 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 - The amount is an integer in the minor unit, precision 0.0001; scale up by 10000, e.g. pass 12345 for 1.2345, 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 upstream does not accept the order, the endpoint returns code=100000 in real time and the order is set 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)] Upstream channels for mainstream payment methods such as gcash/maya effectively require the end-user's name/phone (email is optional for some channels); missing them will cause the upstream to reject the order and the order to fail. The platform no longer blocks with 300405 (customer is optional; when provided it is format-validated and the full name is auto-split). Please provide it via the generic fields and the platform will auto-map to each channel's required format (e.g. split a full name into first/last_name): name — extra.customer.name full name (or explicitly extra.customer.first_name+last_name); email — extra.customer.email (or extra.email); phone — extra.customer.phone (or extra.phone). This way the merchant only integrates one set of generic fields, with no per-channel adaptation. - [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 (upstream did not accept; order set to failed in real time) - 300404 No available payment-method channel (country/currency/pay_method matched no available upstream) - 100001 Parameter validation failed (e.g. notify_url points to an internal network / illegal protocol) - 300402 Upstream configuration unavailable - 300403 Fee rate not configured ## Collection: Query Order (pay/query) - Method: `POST` - Endpoint: `/merchant/pay/query` - Purpose: Query a collection order status by platform 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 | Platform 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": "", "api_key": "", "timestamp": 1736073600, "nonce": "random-xyz", "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 | Platform order no. | | out_order_no | string | Yes | Merchant order no. | | amount | int | Yes | Order amount (integer in minor unit, precision 0.0001; scaled up by 10000, 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; unpaid-and-timed-out is no longer returned separately as expired and is normalized to pending. | | channel_order_no | string\|null | No | Channel-side order no. (nullable) | | paid_at | string\|null | No | Payment success time (ISO8601; may be empty when not yet successful) | | notify_status | string | Yes | Status of the platform pushing the callback to the merchant: 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, "currency": "USDT", "status": "success", "channel_order_no": "CH20250101XXX", "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: `/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 (integer in minor unit, precision 0.0001; scale up by 10000, 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 HTTP 400 (code=400). | | currency | string | Yes | Currency (e.g. PHP/USDT; used for validation and routing) | | pay_method | string | Yes | Payment method (provided by the platform, 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 platform pushes 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; the platform translates it to the upstream code and backfills bank_name; 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). The platform no longer rejects an order due to its absence (no longer returns 300405); however, upstream channels for mainstream payment methods such as gcash/maya effectively require name/phone, and missing them will cause the upstream to reject the order and the order to fail, so it is strongly recommended to include it. When provided, the platform validates the format and auto-maps it to each channel's format (a full name is auto-split into first/last): you may 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": "", "api_key": "", "timestamp": 1736073600, "nonce": "random-xyz", "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 | Platform 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 (integer in minor unit, precision 0.0001; scaled up by 10000, 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)] Upstream channels for mainstream payment methods such as gcash/maya effectively require the end-user's name/phone (email is optional for some channels); missing them will cause the upstream to reject the order and the order to fail. The platform no longer blocks with 300405 (customer is optional; when provided it is format-validated and the full name is auto-split). Please provide it via the generic fields and the platform will auto-map to each channel's required format (e.g. split a full name into first/last_name): name — for payout you may use account_name (payee / account name, auto-mapped to last_name), or extra.customer.name full name (or explicitly extra.customer.first_name+last_name); email — extra.customer.email (or extra.email); phone — extra.customer.phone (or extra.phone). This way the merchant only integrates one set of generic fields, with no per-channel adaptation. - [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 available payment-method channel - 300403 Fee rate not configured - 300401 Channel unavailable / does not exist - 100001 Parameter validation failed (e.g. notify_url points to an internal network / illegal protocol) - 300402 Upstream configuration unavailable - 300406 Missing required order parameter (e.g. bank_code) - 300407 Invalid bank code ## Payout: Query Order (payout/query) - Method: `POST` - Endpoint: `/merchant/payout/query` - Purpose: Query a payout order status by platform 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 | Platform 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": "", "api_key": "", "timestamp": 1736073600, "nonce": "random-xyz", "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 | Platform payout no. | | out_payout_no | string | Yes | Merchant payout no. | | amount | int | Yes | Payout amount (integer in minor unit, precision 0.0001; scaled up by 10000, 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 | string\|null | No | Channel-side order no. (nullable) | | 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 | Status of the platform pushing the callback to the merchant: 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": "CHW20250101XXX", "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: `/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": "", "api_key": "", "timestamp": 1736073600, "nonce": "random-xyz", "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 (a cross-channel unified code; the platform auto-translates to the upstream) | #### 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 bank list changes dynamically with upstream channel availability; it is recommended to query it in real time before placing an order (or cache it briefly). - 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: `/merchant/payout/proof/query` - Purpose: Query the upstream payment-proof URL of a successfully paid-out order (not all channels/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 | Platform 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": "", "api_key": "", "timestamp": 1736073600, "nonce": "random-xyz", "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 | Platform 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 channel (nullable) | #### 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: `/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 | Platform 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": "", "api_key": "", "timestamp": 1736073600, "nonce": "random-xyz", "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 | Platform 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=…; the merchant must prepend its own openapi_base, i.e. the upstream agent's dedicated domain, 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 platform auto-derives the 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: `/merchant/pay-methods/query` - Purpose: Query the dictionary of payment methods enabled on the platform (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": "", "api_key": "", "timestamp": 1736073600, "nonce": "random-xyz", "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 dictionary of payment methods enabled on the platform; whether a given payment method currently has an available upstream is determined by the actual response of the order endpoint (returns 300404 when no channel is available). ### Common Errors - 100104 Invalid signature ## Common: Query Account Balance (balance/query) - Method: `POST` - Endpoint: `/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": "", "api_key": "", "timestamp": 1736073600, "nonce": "random-xyz", "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}]; amounts are integers in the minor unit (10000 = 1 unit) | #### 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: `/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 {} ``` > 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 | |---|---|---|---| | 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. ## 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. A timestamp window of ±300 seconds is recommended; if nonce is used, the server performs replay protection within a short time window. ## Multi-language Examples (pay/create) > Note: the examples already apply stable JSON serialization to nested fields such as extra before signing, and can be copied as-is (usable after replacing the placeholders and credentials). ### JavaScript(Node.js) ```js // Demo of signing and order creation. sign = HMAC-SHA256(params sorted by key as k=v joined by "&", then append "&secret=") in hex. Nested objects (extra etc.) are serialized as compact JSON with keys sorted before signing. Replace placeholders with your real credentials. const crypto = require('crypto'); const baseUrl = ''; const merchantNo = ''; const apiKey = ''; const apiSecret = ''; 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: '', last_name: '', email: '', 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 ") in hex. Nested objects (extra etc.) are serialized as compact JSON with keys sorted before signing. Replace placeholders with your real credentials. $baseUrl = ""; $merchantNo = ""; $apiKey = ""; $apiSecret = ""; $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' => '', 'last_name' => '', 'email' => '', '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 of signing and order creation. sign = HMAC-SHA256(params sorted by key as k=v joined by "&", then append "&secret=") in hex. Nested objects (extra etc.) are serialized as compact JSON with keys sorted before signing. Replace placeholders with your real credentials. 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 = ""; String merchantNo = ""; String apiKey = ""; String apiSecret = ""; Map 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 extra = new TreeMap<>(); Map customer = new TreeMap<>(); customer.put("first_name", ""); customer.put("last_name", ""); customer.put("email", ""); customer.put("phone", ""); extra.put("customer", customer); payload.put("extra", extra); // signMapper: serializes nested objects with keys in ascending order, consistent with the server stable-signature convention ObjectMapper signMapper = new ObjectMapper(); signMapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true); StringBuilder raw = new StringBuilder(); for (Map.Entry 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 resp = HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString()); System.out.println(resp.body()); ``` ### Go ```go // Demo of signing and order creation. sign = HMAC-SHA256(params sorted by key as k=v joined by "&", then append "&secret=") in hex. Nested objects (extra etc.) are serialized as compact JSON with keys sorted before signing. Replace placeholders with your real credentials. package main import ( "bytes" "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "net/http" "sort" "strings" "time" ) func main() { baseUrl := "" merchantNo := "" apiKey := "" apiSecret := "" 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": "", "last_name": "", "email": "", "phone": "", }, }, } // stableJSON: disables HTML escaping (Go escapes <>& by default), consistent with the semantics of 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 of signing and order creation. sign = HMAC-SHA256(params sorted by key as k=v joined by "&", then append "&secret=") in hex. Nested objects (extra etc.) are serialized as compact JSON with keys sorted before signing. Replace placeholders with your real credentials. import time import hmac import hashlib import json import requests base_url = "" merchant_no = "" api_key = "" 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": "", "last_name": "", "email": "", "phone": "", }, }, } def _sv(v): """Stable serialization: nested object/array -> compact JSON with keys in ascending order; scalar -> 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) ``` ## Amount Convention All amounts are expressed as integers in the "minor accounting unit"; the current precision convention is `0.0001`, so multiply values by 10000, e.g. pass `12345` for `1.2345`, to avoid floating-point errors. ## Callback Details The platform 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 platform treats the callback 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 | Platform order no. | | out_order_no | string | Yes | Merchant order no. | | amount | int | Yes | Reference order amount (integer in minor unit, ×10000) | | actual_amount | int\|null | No | End-user's actual paid amount (obtained by querying the upstream; the basis for crediting on success; 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 | Upstream order no. is not exposed to the merchant; always null | | 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 | Platform payout no. | | out_payout_no | string | Yes | Merchant payout no. | | amount | int | Yes | Payout amount (integer in minor unit, ×10000) | | 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 | Upstream order no. is not exposed to the merchant; always null | | 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 only a push-status field returned by the order query API (platform → merchant) and never appears in the callback body. ### 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=`, 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). ### Retry / Timeout / Alert / Security - **Timeout**: the platform 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. - **Alert**: when consecutive failures reach the threshold (`3`), a platform-side in-app alert is triggered (for operations to investigate); this does not affect the merchant-side retries. - **No redirect following**: the platform does not follow 30x redirects for `notify_url` (security hardening). - **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: integers in the minor unit (×10000). - `status` is the external **normalized form** (not the internal raw value): collection is always `success`; payout is `success/failed`. - `channel_order_no` is **always `null`** for the merchant (the upstream order no. is not exposed) — this is an external contract, not a missing value. ### 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. ## Common Authentication Error Codes The following error codes are common to all authenticated endpoints; the "Common Errors" of each endpoint lists only high-frequency or business-related items and does not imply that other authentication codes will not be returned: - `100001` Parameter validation failed (HTTP 200; message is the raw Joi field error, e.g. `"amount" must be a number`) - `100101` Request expired (timestamp outside the allowed window) - `100102` IP not in the allowlist - `100103` Replay (duplicate nonce/signature fingerprint) - `100104` Invalid signature - `100105` IP in the blocklist - `100106` Too many authentication failures (rate limited) === 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.