--- url: /en/index.md --- # Overview This document is for contracted merchants only. Do not publish or redistribute it. ## Transaction types ST Pay offers two types of transactions. Both are created by your server through the API: | Transaction | Flow of funds | Process | | --- | --- | --- | | Collection | Payer → ST Pay receiving account → Your ST Pay balance | You create a collection order and receive the receiving account number and the **payable amount**, then ask the payer to make the transfer. Once ST Pay confirms that the payment has been credited, the settlement amount (after the fee is deducted) is added to your balance, and ST Pay notifies you by Callback. | | Payout | Your ST Pay balance → Payee | You create a payout order and specify the payee's account number. After ST Pay pays out, the payout amount plus the fee is deducted from your balance, and ST Pay notifies you by Callback. | Two channels are supported: `fps` (FPS, Faster Payment System) and `bank_transfer` (bank transfer). ## API overview | API | Purpose | Method and path | | --- | --- | --- | | [Create a collection order](./api/collect.md) | Create a collection order and get the payment details | `POST /api/v1/collect/orders` | | [Create a payout order](./api/payout.md) | Create a payout order | `POST /api/v1/payout/orders` | | [Query a collection order](./api/query.md#query-a-collection-order) | Get the current status and amounts of a collection order | `POST /api/v1/collect/orders/query` | | [Query a payout order](./api/query.md#query-a-payout-order) | Get the current status and amounts of a payout order | `POST /api/v1/payout/orders/query` | | [Callback](./api/callback.md) | ST Pay notifies you when an order has a result | ST Pay sends a `POST` to the `callback_url` you sent when creating the order | ## Integration process 1. **Provide the following to ST Pay** * Server egress IPs: every source IP from which you call the API. Each one must be a single IPv4 or IPv6 address; IP ranges are not supported. * Back office login IPs: the source IPs from which you log in to the [merchant back office](./guide/backoffice.md), in the same format. This is a separate whitelist from the API one. 2. **Obtain your API credential and back office account**: ST Pay delivers your `api_identity` (API identity) and **API secret key**. The secret key is shown only once, when the credential is created, and ST Pay does not show it again afterwards. Store it securely right away. ST Pay also creates your merchant supervisor account for the back office and delivers its initial password, which must be changed at first login. 3. **Complete the integration in the test environment**: Implement the four APIs and the Callback as described in this document, and verify every item on the [Go-live checklist](./reference/checklist.md). 4. **Go live in the production environment**: The production environment's domain, credentials, and IP whitelist are completely separate from those of the test environment and must be requested separately. The API and back office login IP whitelists and the credentials are configured by ST Pay. If you need to change any of them, contact your ST Pay integration contact. IP whitelist changes take effect in about 1 minute. You do not need to give ST Pay a Callback URL. Send it as `callback_url` each time you create an order; see [Callback URL](./api/callback.md#callback-url) for the rules. ## Environments | Environment | API domain | Merchant back office | Notes | | --- | --- | --- | --- | | Test environment | Provided by your ST Pay integration contact | Provided by your ST Pay integration contact | No real money is moved; use credentials issued for the test environment | | Production environment | Provided after you complete the integration in the test environment | Provided after you complete the integration in the test environment | Real money is moved; use credentials issued for the production environment | The full URL is "domain + path", for example `https://merchant.vincoterra.com/api/v1/collect/orders`. The [merchant back office](./guide/backoffice.md) shares the same domain as the API (a different path), while the payment page is served from `pay.vincoterra.com`. All examples in this document use the production domains: `merchant.vincoterra.com` for the API and the merchant back office, `pay.vincoterra.com` for the payment page. Test-environment domains are provided separately by your ST Pay integration contact. --- --- url: /en/api/callback.md --- # Callback notifications When an order has a result, ST Pay sends a `POST` to the `callback_url` you sent when creating that order. ## When Callbacks are sent | Event | `status` | Sent? | | --- | --- | --- | | Collection succeeded | `success` | Sent | | Collection rejected (rejected manually by ST Pay, or rejected by the system when the 30 minutes expire) | `rejected` | Sent | | Collection late credit | `late_credited` | **Not sent.** Confirm with an order query; see [Order status](../reference/status.md#collection-orders) | | Payout paid | `paid` | Sent | | Payout rejected | `rejected` | Sent | ## Callback URL * Send it as `callback_url` each time you create an order ([collection](./collect.md), [payout](./payout.md)). You do not need to give it to ST Pay in advance. Each order can use a different URL, for example separate URLs for the test and production environments. * **It cannot be changed after the order is created.** Automatic Callbacks and manual resends by ST Pay both go to the URL sent when the order was created. If you later change your URL, notifications for older orders still go to the old URL. If the old URL can no longer receive them, use [order query](./query.md) to confirm the results of those orders. * Up to 500 characters. It is stored exactly as sent, without any conversion. * Both `http` and `https` are accepted. `https` is recommended, because `http` sends the order numbers and amounts in plain text. Either way, **always verify `sign`** to reject forged or tampered notifications. * When you use `https`, your server's TLS certificate must be issued by a public certificate authority, be within its validity period, match the host name in the URL, and be served with the complete certificate chain (including intermediate certificates). A self-signed, expired, or mismatched certificate, or a missing intermediate certificate, counts as a delivery failure and is **not retried**. * It must not contain credentials (`user:pass@`), `#`, whitespace, or non-ASCII characters. Convert internationalized domain names to punycode (starting with `xn--`). A path and query string are allowed. * The host name may contain only English letters, digits, `.`, and `-` (no underscore `_`), or be an IPv6 address in `[ ]`. If a port is given, it must be 1 to 65535 (a bare `:` is not allowed). * The path and query must not contain `|`, `{`, `}`, `^`, `"`, `<`, `>`, `\`, or backticks as-is, and `%` must be followed by two hexadecimal digits. URL-encode these characters first (for example, write `%` as `%25`). * A missing, empty, or whitespace-only value returns `missing_parameter`. * It must not be a local or private network address, in both the test and production environments: * `localhost`, domains ending in `.localhost`, `.local`, `.localdomain`, `.internal`, `.intranet`, `.lan`, `.corp`, `.home.arpa`, or `.private`, and host names without a dot. * Private, loopback, link-local, CGNAT, reserved, documentation, and multicast IP addresses, such as `10.x.x.x`, `172.16.x.x`–`172.31.x.x`, `192.168.x.x`, `127.x.x.x`, `169.254.x.x`, and `100.64.x.x`. For IPv6, only global unicast addresses (`2000::/3`) are accepted, excluding Teredo `2001::/32`, documentation `2001:db8::/32`, and 6to4 `2002::/16`. * IP addresses written as a single number, in hexadecimal or octal, or with parts omitted, such as `2130706433`, `0x7f.0.0.1`, `0177.0.0.1`, and `127.1`. * A public IP address can be used directly. * If these rules are not met, order creation returns `invalid_callback_url` (or `invalid_parameter` for a malformed URL or one longer than 500 characters), and no order is created. * **Before every send** (including manual resends), ST Pay resolves the domain again. If resolution fails, or any resolved address is not a public address, the Callback is not sent and counts as a delivery failure. Make sure the domain resolves on the public internet and points only to public IP addresses. ## Request format * Method `POST`, `Content-Type: application/json`. The body is a JSON object, and all values are strings. * Includes `sign`. The [signature](../guide/signature.md) rules are the same as for API requests and use your API secret key (see [Key rotation](../guide/security.md#key-rotation)). Callbacks do not include `api_identity` or `timestamp`. * ST Pay waits at most 5 seconds to connect and at most 10 seconds for a response, and **does not follow redirects (3xx)**: a 3xx response counts as a delivery failure. Use the final URL directly; for example, an `http` to `https` redirect or a missing trailing `/` can cause a redirect. * The Callback body does not include `callback_url`. * Requests always carry `User-Agent: QPay-Callback/1.0` and `Accept: */*`. If your Callback URL is behind Cloudflare, a WAF, or another firewall, allow this User-Agent and `POST` requests; otherwise they may be blocked with 403 and count as a delivery failure. ## What you must respond with Respond with HTTP **2xx** and a response body of plain text `ok` (leading and trailing whitespace is ignored). Any other response, including a timeout, a non-2xx status, or a body other than `ok`, is treated as a **delivery failure**. ```http HTTP/1.1 200 OK Content-Type: text/plain ok ``` ::: warning Callbacks are not retried automatically Each order's Callback is sent automatically only once. If delivery fails, only ST Pay staff can resend it manually, and a resend always goes to the `callback_url` sent when the order was created. Therefore: * Do not rely on Callbacks alone. For orders that have had no result for a long time, query them periodically to confirm. * The same order may receive more than one Callback (manual resends). Process them idempotently using `order_no` plus `status`, and respond `ok` directly to notifications you have already processed. * A Callback delivery failure does not affect the transaction result; the order status and balance are not reverted because of it. ::: ## Fields | Field | Collection succeeded | Collection rejected | Payout paid | Payout rejected | Description | | --- | --- | --- | --- | --- | --- | | `order_no` | ✓ | ✓ | ✓ | ✓ | ST Pay order number | | `merchant_order_no` | ✓ | ✓ | ✓ | ✓ | Merchant order number | | `status` | `success` | `rejected` | `paid` | `rejected` | Order status | | `received_amount` | ✓ | | | | Received amount | | `fee` | ✓ | | ✓ | | Fee | | `settlement_amount` | ✓ | | | | Settlement amount | | `payable_amount` | | ✓ | | | Payable amount (including the tail) | | `amount` | | | ✓ | ✓ | Payout amount | | `finished_at` | ✓ | ✓ | ✓ | ✓ | Completion time | | `sign` | ✓ | ✓ | ✓ | ✓ | Signature | Callbacks do not include the rejection reason. If you need it, get it with an order query. If your collection and payout orders use the same `callback_url`, the `status` of both can be `rejected`, and a collection order and a payout order can have the same merchant order number. Match Callbacks to orders using the `order_no` you received when creating the order. If you want to match by merchant order number, make sure your collection orders and payout orders use order numbers that never collide, for example by adding different prefixes. ## Handling recommendations 1. Parse the JSON and recompute the signature using **all the fields you received** (except `sign`), then compare. Do not pick only the fields listed in this document, because fields may be added in the future. 2. If the signature does not match, discard the Callback and do not update the order. 3. Find your order by `order_no`. If you have already processed the same result, respond `ok` directly. 4. Respond `ok` after updating the order. Run time-consuming follow-up work (shipping, notifying the member, etc.) in the background so that you do not exceed 10 seconds. 5. For a successful collection, credit the order with `received_amount` (the amount actually received, or `settlement_amount` after the fee), not the amount at order creation. If the received amount differs from the amount you expected, send it for manual review instead of crediting it automatically. 6. Once an order is in a final status (`success`, `paid`, `rejected`), do not roll it back when a later notification with a different status arrives. Log it for manual review; when in doubt, the [order query](./query.md) result prevails. ## Examples All signatures below are actual values computed with the [sample key](../guide/signature.md#example). ::: code-tabs @tab Collection succeeded ```json { "order_no": "C202610131000051230000", "merchant_order_no": "MO20261013-0001", "status": "success", "received_amount": "1000.37", "fee": "25.00", "settlement_amount": "975.37", "finished_at": "2026-10-13 10:12:30", "sign": "49d9d35c3b3a97a55fe60976a331d36913b0d3cd9b82e840e0315c445653d207" } ``` @tab Collection rejected ```json { "order_no": "C202610131001178820000", "merchant_order_no": "MO20261013-0002", "status": "rejected", "payable_amount": "2000.58", "finished_at": "2026-10-13 10:31:17", "sign": "cd85bf732776bb2897f6e767dda873ce580330f979be3f812c390ec09af0d2f4" } ``` @tab Payout paid ```json { "order_no": "P202610131005224410000", "merchant_order_no": "PO20261013-0001", "status": "paid", "amount": "500.00", "fee": "7.50", "finished_at": "2026-10-13 10:20:41", "sign": "64e3b813fb086b98c1a4e68c2689423ff347336219f182293e5344a3f6853562" } ``` @tab Payout rejected ```json { "order_no": "P202610131006403520000", "merchant_order_no": "PO20261013-0002", "status": "rejected", "amount": "3000.00", "finished_at": "2026-10-13 10:25:09", "sign": "bdf0a8031ac0beff801a4b2591793ea6d6edbfd27dbdadce24ab871602f99094" } ``` ::: The string to sign for the collection success example: ```text fee=25.00&finished_at=2026-10-13 10:12:30&merchant_order_no=MO20261013-0001&order_no=C202610131000051230000&received_amount=1000.37&settlement_amount=975.37&status=success ``` --- --- url: /en/api/collect.md --- # Create a collection order ```text POST /api/v1/collect/orders ``` Create a collection order and get the payment details to give to the payer. ::: tip The payer must transfer the payable amount (payable_amount), not the original amount you sent ST Pay adds an identification tail of 0.01 to 0.99 to the original amount. For example, if the original amount is 1000.00, the payable amount might be 1000.37. ST Pay uses this exact amount to identify which order a payment belongs to. If you use json mode to display the payment screen yourself, show the payable amount clearly and remind the payer to transfer exactly this amount. ::: * The order expires **30 minutes** after it is created (`expires_at`). If no payment has been credited by then, the system rejects the order and sends a failure Callback. * `response_mode` determines what is returned: * `url`: Returns the ST Pay payment page URL `payment_url`. Redirect the payer to this page. * `json`: Returns the receiving account details, and you display the payment screen yourself. * A single member can have at most **10** pending (`pending`) collection orders at the same time. Beyond that, `user_active_orders_limit` is returned. ## Request fields | Field | Required | Format | Description | | --- | --- | --- | --- | | `api_identity` | Yes | String | The API identity issued by ST Pay | | `merchant_order_no` | Yes | 1–200 characters; English letters, digits, `_`, `-` | Your order number. It must be unique among your collection orders, regardless of the status of the existing order | | `merchant_user_id` | Either one | 1–200 characters; English letters, digits, `_`, `-` | The unique identifier of the paying member in your system. When it is sent, it is used to determine whether orders belong to the same member | | `payer_name` | Either one | 1–200 characters; Chinese characters, English letters, and spaces only | The payer's real name. When `merchant_user_id` is not sent, the name is used to determine whether orders belong to the same member (case-insensitive, ignoring all whitespace). It must not contain digits or punctuation; for example, the hyphen in `Chan Tai-man` will be rejected | | `amount` | Yes | Amount string | The original amount. The per-transaction minimum and maximum are checked against this amount (excluding the tail) | | `channel_type` | Yes | `fps` or `bank_transfer` | Channel type | | `response_mode` | Yes | `url` or `json` | Response mode; see above | | `language` | No | `zh-TW`, `zh-CN`, or `en` | Language of the payment page: Traditional Chinese, Simplified Chinese, or English. Defaults to `zh-TW` if omitted or empty. Affects only the payment page in `url` mode | | `callback_url` | Yes | URL, up to 500 characters | The [Callback](./callback.md) for this order is sent to this URL. It cannot be changed after the order is created. Both `http` and `https` are accepted; it must not be a local or private network address. See [Callback URL](./callback.md#callback-url) | | `timestamp` | Yes | `yyyy-MM-dd HH:mm:ss` | The time the request is sent (UTC+8). Rejected if it differs from the ST Pay server time by more than 5 minutes. See [Request and response format](../guide/conventions.md#requests) | | `sign` | Yes | 64 lowercase hexadecimal characters | See [Signature](../guide/signature.md) | You must send at least one of `merchant_user_id` and `payer_name`. An empty string or a value consisting only of whitespace is treated as not sent. If neither is sent, `missing_member_identity` is returned. If both are sent, `merchant_user_id` is used to identify the member, and the name is used for display only. ## Request example ```json { "api_identity": "qp_Xk3vT9aLm2QwE7rB1nZc", "merchant_order_no": "MO20261013-0001", "merchant_user_id": "U10086", "payer_name": "王小明", "amount": "1000.00", "channel_type": "fps", "response_mode": "url", "callback_url": "https://shop.example.com/qpay/callback", "timestamp": "2026-10-13 10:00:00", "sign": "566fac06060d54d7450b41b6858b1ddf33df9d37117ab8a8c495ce748469f2b2" } ``` ## Response fields | Field | Present when | Description | | --- | --- | --- | | `order_no` | Always | ST Pay order number | | `merchant_order_no` | Always | The merchant order number you sent | | `status` | Always | Always `pending` | | `payment_url` | `url` mode | Payment page URL; see [Payment page](#payment-page) | | `channel_type` | `json` mode | `fps` or `bank_transfer` | | `bank_name` | `json` mode with `bank_transfer` | Name of the receiving bank | | `bank_code` | `json` mode with `bank_transfer` | Code of the receiving bank (a string; leading zeros are preserved) | | `account_no` | `json` mode | Full receiving account number | | `account_name` | `json` mode | Receiving account name | | `payable_amount` | Always | The payable amount, equal to the original amount plus the identification tail. The payer must transfer this amount | | `expires_at` | Always | Expiry time, equal to the order creation time plus 30 minutes | | `sign` | Always | Response signature, computed with the key used for this request. See [Verifying responses](../guide/signature.md#verifying-responses). Use `account_no` and `payment_url` only after the signature verifies | ### Response example: url mode ```json { "code": "OK", "message": "成功", "data": { "order_no": "C202610131000051230000", "merchant_order_no": "MO20261013-0001", "status": "pending", "payment_url": "https://pay.vincoterra.com/pay/QzIwMjYxMDEzMTAwMDA1MTIzMDAwMA.v3Kd9QeLr7Zp1sWm4XyT8bN2cF6hJ0aG5uE-iOkRtYw", "payable_amount": "1000.37", "expires_at": "2026-10-13 10:30:05", "sign": "7e876d9780d033c6224f52015eb15e1d1a9bbda9b75764fff0cdbf3d36b183ee" } } ``` ### Response example: json mode, bank transfer The request is `"merchant_order_no": "MO20261013-0002"`, `"payer_name": "Chan Tai Man"`, `"amount": "2000.00"`, `"channel_type": "bank_transfer"`, `"response_mode": "json"`, `"callback_url": "https://shop.example.com/qpay/callback"`, `"timestamp": "2026-10-13 10:01:17"`, and the signature is `e20f45942090246543ab7bd95a1fecd52011699382a93637529c2aa987767848`. ```json { "code": "OK", "message": "成功", "data": { "order_no": "C202610131001178820000", "merchant_order_no": "MO20261013-0002", "status": "pending", "channel_type": "bank_transfer", "bank_name": "The Hongkong and Shanghai Banking Corporation Limited", "bank_code": "004", "account_no": "123-456789-001", "account_name": "WONG KA YAN", "payable_amount": "2000.58", "expires_at": "2026-10-13 10:31:17", "sign": "4cd29b8f52d256516363af07eccfb821caaef52a10777cdc5495b809447aa175" } } ``` In json mode with the `fps` channel, there is no `bank_name` or `bank_code`, and `account_no` is the FPS receiving account. ## Payment page When you use `url` mode: * The page shows the payable amount, channel type, receiving account number and account name, and expiry time. For bank transfers, it also shows the bank name and bank code. * The payable amount, bank code, account number, and account name each have a "Copy" button. * Once the order has ended (success, rejected, or expired), the page shows only "This order has ended" and no longer shows the receiving details. If the payer still has the page open, it switches to this automatically; no refresh is needed. * The page is shown in the `language` you specified when creating the order. The payer cannot change it. * The URL itself is an access credential: anyone who has the URL can see the full receiving account number. Give it only to the payer of that order, and do not write it into public logs, analytics tools, or shareable links. * Use the returned URL exactly as it is. Do not modify it or build it yourself; any change causes the page to show 「找不到頁面」 ("page not found"). ## Possible errors `invalid_signature`, `timestamp_expired`, `ip_not_allowed`, `merchant_disabled`, `missing_parameter`, `invalid_parameter`, `invalid_amount`, `missing_member_identity`, `invalid_callback_url`, `amount_out_of_range`, `merchant_order_no_duplicate`, `user_active_orders_limit`, `channel_full`, `no_available_card`, `internal_error`. For details, see [Error codes](../reference/errors.md). --- --- url: /en/api/payout.md --- # Create a payout order ```text POST /api/v1/payout/orders ``` Create a payout order. ST Pay pays the specified payee from your balance. * When the order is created, ST Pay checks that **available balance ≥ payout amount + fee**. If the balance is insufficient, `insufficient_balance` is returned and no order is created. * Creating an order **does not freeze or reserve** your balance. The amount is deducted only when ST Pay actually pays out, and the balance is checked again at that time. If you create multiple payout orders at the same time, their combined amount may exceed your balance; any order for which the balance is insufficient at payout time remains pending and may be rejected. * The fee rate is locked in at the moment the order is created, and later rate changes do not affect the order. The order creation response does not include the fee; after the payout, you can get it from the Callback or an order query. ## Request fields | Field | Required | Format | Description | | --- | --- | --- | --- | | `api_identity` | Yes | String | The API identity issued by ST Pay | | `merchant_order_no` | Yes | 1–200 characters; English letters, digits, `_`, `-` | Your order number. It must be unique among your payout orders; collection orders and payout orders are counted separately | | `amount` | Yes | Amount string | The payout amount, which is also the basis for calculating the fee | | `channel_type` | Yes | `fps` or `bank_transfer` | Channel type | | `account_no` | Yes | Depends on the channel; see [Payee account format](#payee-account-format) | Payee account number | | `account_name` | Yes | 1–200 characters | Payee's name or account name | | `bank_name` | Required for bank transfer | 1–100 characters | Name of the payee's bank. Must not be sent for FPS | | `bank_code` | Required for bank transfer | String | Code of the payee's bank. It must be a value from the [Bank codes](../reference/banks.md) list, with leading zeros preserved. Must not be sent for FPS | | `callback_url` | Yes | URL, up to 500 characters | The [Callback](./callback.md) for this order is sent to this URL. It cannot be changed after the order is created. Both `http` and `https` are accepted; it must not be a local or private network address. See [Callback URL](./callback.md#callback-url) | | `timestamp` | Yes | `yyyy-MM-dd HH:mm:ss` | The time the request is sent (UTC+8). Rejected if it differs from the ST Pay server time by more than 5 minutes. See [Request and response format](../guide/conventions.md#requests) | | `sign` | Yes | 64 lowercase hexadecimal characters | See [Signature](../guide/signature.md) | ### Payee account format | Channel | Accepted `account_no` formats | | --- | --- | | `fps` | Any one of the following three, sent in the same field:• Mobile number: 8 digits, for example `91234567`• Email: up to 200 characters, containing only English letters, digits, and `@ . _ -`, with exactly one `@` that has characters on both sides and a `.` after it, for example `chan.taiman@example.com`• FPS ID: 1 to 50 digits | | `bank_transfer` | Digits and hyphens `-`, 1 to 50 characters, for example `012-345678-001` | If an FPS order includes `bank_name` or `bank_code`, `invalid_parameter` is returned. If a bank transfer order is missing either of them, `missing_parameter` is returned. ## Request examples ::: code-tabs @tab FPS ```json { "api_identity": "qp_Xk3vT9aLm2QwE7rB1nZc", "merchant_order_no": "PO20261013-0001", "amount": "500.00", "channel_type": "fps", "account_no": "91234567", "account_name": "陳大文", "callback_url": "https://shop.example.com/qpay/callback", "timestamp": "2026-10-13 10:05:22", "sign": "693c80aaf437abcd6dbf60f8aed9e4eda09deed052977fa63cb9b1768150044f" } ``` @tab Bank transfer ```json { "api_identity": "qp_Xk3vT9aLm2QwE7rB1nZc", "merchant_order_no": "PO20261013-0002", "amount": "3000.00", "channel_type": "bank_transfer", "account_no": "012-345678-001", "account_name": "CHAN TAI MAN", "bank_name": "Bank of China (Hong Kong) Limited", "bank_code": "012", "callback_url": "https://shop.example.com/qpay/callback", "timestamp": "2026-10-13 10:06:40", "sign": "44aadf0a2be821c13c1aef71d8cc7cfc6cf012d3b15f543d2650cdf762002a2b" } ``` ::: ## Response fields | Field | Description | | --- | --- | | `order_no` | ST Pay order number | | `merchant_order_no` | The merchant order number you sent | | `status` | Always `pending` | | `amount` | Payout amount | | `sign` | Response signature, computed with the key used for this request. See [Verifying responses](../guide/signature.md#verifying-responses) | ```json { "code": "OK", "message": "成功", "data": { "order_no": "P202610131005224410000", "merchant_order_no": "PO20261013-0001", "status": "pending", "amount": "500.00", "sign": "e2fe8e5d430ba4bf8a1003b0d7fc8eabcc5d7d974ceda5135783d1aecfa44c8a" } } ``` ## Possible errors `invalid_signature`, `timestamp_expired`, `ip_not_allowed`, `merchant_disabled`, `missing_parameter`, `invalid_parameter`, `invalid_amount`, `invalid_callback_url`, `amount_out_of_range`, `merchant_order_no_duplicate`, `insufficient_balance`, `internal_error`. For details, see [Error codes](../reference/errors.md). --- --- url: /en/api/query.md --- # Order query Query the current status and amounts of an order. Use an order query to confirm the result in the following cases: * You did not receive a Callback. * A collection order received a [late credit](../reference/status.md#collection-orders) (no Callback is sent). * Your order creation request timed out, or you received `internal_error`. For a pending order, the response also includes the payment details, so the payer can still pay that order (see below). * Order creation returned `merchant_order_no_duplicate`, and you want to know the status of the existing order. An order query is a read-only operation and can be called repeatedly. ## Request fields Collection order queries and payout order queries use the same request fields: | Field | Required | Description | | --- | --- | --- | | `api_identity` | Yes | The API identity issued by ST Pay | | `order_no` | Either one | ST Pay order number | | `merchant_order_no` | Either one | Your merchant order number | | `timestamp` | Yes | The time the request is sent (UTC+8, `yyyy-MM-dd HH:mm:ss`). Rejected if it differs from the ST Pay server time by more than 5 minutes. See [Request and response format](../guide/conventions.md#requests) | | `sign` | Yes | See [Signature](../guide/signature.md) | * If neither is sent, `missing_parameter` is returned. * If both are sent, they must refer to the same order. If they refer to different orders, `query_conflict` is returned; if either of them cannot be found, `order_not_found` is returned. * You can only query your own orders. If the order does not exist, belongs to another merchant, or has been deleted after 6 months, `order_not_found` is returned in every case. Possible errors: `invalid_signature`, `timestamp_expired`, `ip_not_allowed`, `missing_parameter`, `invalid_parameter`, `query_conflict`, `order_not_found`, `internal_error`. ## Query a collection order ```text POST /api/v1/collect/orders/query ``` ### Request example ```json { "api_identity": "qp_Xk3vT9aLm2QwE7rB1nZc", "merchant_order_no": "MO20261013-0001", "timestamp": "2026-10-13 10:35:00", "sign": "c29089b08a6fdcd3d9c17a404cae90a4aaa0a79210778e53997e4f9f43bb6462" } ``` ### Response fields | Field | Description | | --- | --- | | `order_no` | ST Pay order number | | `merchant_order_no` | Merchant order number | | `channel_type` | `fps` or `bank_transfer` | | `status` | `pending`, `success`, `rejected`, or `late_credited`; see [Order status](../reference/status.md) | | `amount` | Original amount | | `payable_amount` | Payable amount (the original amount plus the tail) | | `received_amount` | Received amount. Has a value only for `success` and `late_credited`; `null` for other statuses | | `fee` | Fee. Has a value for `success` and `late_credited`; `"0.00"` for `rejected`; `null` for `pending` | | `settlement_amount` | Settlement amount (the amount added to your balance). Has a value only for `success` and `late_credited`; `null` for other statuses | | `created_at` | Order creation time | | `finished_at` | Completion time; `null` for `pending` | | `reject_reason` | The rejection reason text for `rejected`: when rejected manually by ST Pay, the reason that was selected (`null` if no reason was selected); when rejected on expiry by the system, 「訂單已逾時」 ("order expired"). This is text for humans to read, not a code. Ignore this field for other statuses | | `payment_url` | Payment page URL, the same as in the order creation response. Present only for orders that are **pending and not yet expired** and were created in `url` mode | | `bank_name`, `bank_code` | Bank name and code. Present only for orders that are pending and not yet expired and were created in `json` mode with channel `bank_transfer` | | `account_no`, `account_name` | Full receiving account number and account name (or payee name). Present only for orders that are pending and not yet expired and were created in `json` mode | | `expires_at` | Expiry time. Present only for orders that are pending and not yet expired | | `sign` | Response signature, computed with the key used for this request. See [Verifying responses](../guide/signature.md#verifying-responses) | If your order creation request timed out and you did not get a response, query by merchant order number: if the order is `pending` and the payment details above are present, you can send the payer to `payment_url` (or show the receiving account) to pay that order, instead of creating a new order with a new merchant order number. The expiry time stays the original `expires_at` and is not extended; once the order has expired or ended, the payment details are no longer returned. Verify the signature first, and use these details only if it passes. ### Response examples ::: code-tabs @tab Pending (url mode) ```json { "code": "OK", "message": "成功", "data": { "order_no": "C202610131000051230000", "merchant_order_no": "MO20261013-0001", "channel_type": "fps", "status": "pending", "amount": "1000.00", "payable_amount": "1000.37", "received_amount": null, "fee": null, "settlement_amount": null, "created_at": "2026-10-13 10:00:05", "finished_at": null, "reject_reason": null, "payment_url": "https://pay.vincoterra.com/pay/QzIwMjYxMDEzMTAwMDA1MTIzMDAwMA.v3Kd9QeLr7Zp1sWm4XyT8bN2cF6hJ0aG5uE-iOkRtYw", "expires_at": "2026-10-13 10:30:05", "sign": "8cba06866fc16ea3b585309cd13d851d7bda49bf2e95605275f2d99047620487" } } ``` @tab Success ```json { "code": "OK", "message": "成功", "data": { "order_no": "C202610131000051230000", "merchant_order_no": "MO20261013-0001", "channel_type": "fps", "status": "success", "amount": "1000.00", "payable_amount": "1000.37", "received_amount": "1000.37", "fee": "25.00", "settlement_amount": "975.37", "created_at": "2026-10-13 10:00:05", "finished_at": "2026-10-13 10:12:30", "reject_reason": null, "sign": "889c767037b6748de1d9fa399129b97012c85f3599a0cce9aff289cf79477626" } } ``` @tab Rejected on expiry ```json { "code": "OK", "message": "成功", "data": { "order_no": "C202610131001178820000", "merchant_order_no": "MO20261013-0002", "channel_type": "bank_transfer", "status": "rejected", "amount": "2000.00", "payable_amount": "2000.58", "received_amount": null, "fee": "0.00", "settlement_amount": null, "created_at": "2026-10-13 10:01:17", "finished_at": "2026-10-13 10:31:17", "reject_reason": "訂單已逾時", "sign": "6a99a7a5085900260830aee441271fb2a0e94d88d33e49b15bfb5a46d6a6e05e" } } ``` @tab Order not found ```json { "code": "order_not_found", "message": "查無此單", "data": null } ``` ::: ## Query a payout order ```text POST /api/v1/payout/orders/query ``` ### Request example ```json { "api_identity": "qp_Xk3vT9aLm2QwE7rB1nZc", "order_no": "P202610131005224410000", "timestamp": "2026-10-13 10:40:00", "sign": "adae0ba479bd688b63425e34dcb77cbf5114b34bee68e266ba045ea69748f2b9" } ``` ### Response fields | Field | Description | | --- | --- | | `order_no` | ST Pay order number | | `merchant_order_no` | Merchant order number | | `channel_type` | `fps` or `bank_transfer` | | `status` | `pending`, `paid`, or `rejected`; see [Order status](../reference/status.md) | | `amount` | Payout amount | | `fee` | Fee. Has a value for `paid`; `"0.00"` for `rejected`; `null` for `pending` | | `total_deducted` | Total deducted (the payout amount plus the fee). Has a value only for `paid`; `null` for other statuses | | `created_at` | Order creation time | | `finished_at` | Completion time; `null` for `pending` | | `reject_reason` | The rejection reason text for `rejected` (`null` if no reason was selected). Ignore this field for other statuses | | `sign` | Response signature, computed with the key used for this request. See [Verifying responses](../guide/signature.md#verifying-responses) | ### Response example ```json { "code": "OK", "message": "成功", "data": { "order_no": "P202610131005224410000", "merchant_order_no": "PO20261013-0001", "channel_type": "fps", "status": "paid", "amount": "500.00", "fee": "7.50", "total_deducted": "507.50", "created_at": "2026-10-13 10:05:22", "finished_at": "2026-10-13 10:20:41", "reject_reason": null, "sign": "67d2dfa30d5df0d5456f706400e22faaf5acbb22b164885d5e08f1102e8538a7" } } ``` --- --- url: /en/guide/backoffice.md --- # Merchant back office In addition to the API, ST Pay provides a merchant back office where you can query orders, reconcile your balance, and manage staff accounts. You cannot create orders or export data in the back office; orders are always created through the API. ## Logging in | Item | Details | | --- | --- | | URL | Provided by your ST Pay integration contact (a different domain from the API). The test and production environments use different domains; the production domain is provided after you complete the integration in the test environment. Accounts in the two environments are separate. | | Credentials | Username, password, and a 4-digit image verification code. If you can't read the code, click the image to get a new one. | | Source IP | You can log in only from IPs on the **back office login whitelist** that ST Pay sets up for you (a separate list from the API whitelist). If the whitelist is empty, nobody can log in. To add or change IPs, contact your ST Pay integration contact. | | First account | ST Pay creates your merchant supervisor account and delivers its initial password. | | Password | At least 6 characters, including letters and digits, and different from the username. Initial and reset passwords must be changed at first login; this cannot be skipped, and nothing else is available until you change it. The new password must differ from the current one. Afterwards, use "Change password" in the account menu at the top right. | | Lockout | After 5 consecutive wrong passwords, the account is locked (wrong verification codes do not count; a wrong old password when changing your password also counts). Merchant staff are unlocked by the merchant supervisor; merchant supervisors should contact ST Pay. | | Forgotten password | There is no self-service reset. Merchant staff passwords are reset by the merchant supervisor; merchant supervisors should contact ST Pay. | | Automatic logout | After 30 minutes of inactivity, 12 hours after login, or immediately when the account is disabled or the source IP is no longer on the back-office login whitelist (removed, or you switched to an IP outside it). | | Language | Switch between 繁體中文, 简体中文, and English at the top right. | If ST Pay disables your merchant, you can no longer create orders, but you can still log in to the back office and query orders through the API. ## Roles and pages | Page | Merchant supervisor | Merchant staff | Content | | --- | --- | --- | --- | | Order query | Yes | Yes | Your merchant's collection and payout orders | | Balance ledger | Yes | No | Current available balance and every balance change | | Staff accounts | Yes | No | Create and manage merchant staff accounts | The merchant supervisor can also turn off individual pages or the "Show full account number" permission for each staff member (see [Staff accounts](#staff-accounts)). ## Order query * There are two tabs, "Collection orders" and "Payout orders". The newest orders come first, and up to the latest 500 orders are listed at a time. * Filter by status, creation time, order number (full), merchant order number, and channel type. Creation time defaults to today (00:00:00 to 23:59:59); you can change or clear it, and "Reset" returns it to today. The page does not refresh automatically; click "Refresh" or "Search". * Only orders within the retention period (6 months) are listed. * Status labels map to the API `status` as follows (see also [Order status](../reference/status.md)): | `status` | Back office label | | --- | --- | | `pending` | Pending | | `success` | Success | | `rejected` | Rejected | | `late_credited` | Late credited | | `paid` | Paid | * Field names match the API: payable amount `payable_amount`, original amount `amount`, received amount `received_amount`, fee `fee`, settlement amount `settlement_amount`, payout amount `amount`, total deducted `total_deducted`. * **The payer name is shown only in the back office**; neither the query API nor the Callback returns it. * Account numbers are masked by default (only the last 4 digits are shown). For collection orders, double-click the receiving account to show the full number; only one is shown at a time, and ST Pay keeps a record. Payee account numbers of payout orders are always masked in the merchant back office. * Click "Details" to see the rejection reason and the **Callback history**: the time, result (success or failure), and trigger (automatic or manual) of each send. Failed deliveries are not retried automatically; contact ST Pay if you need a resend (see [Callback](../api/callback.md)). ## Balance ledger The current available balance is shown at the top. Below it, every balance change is listed from newest to oldest, up to the latest 1000 entries at a time; filter by time and entry type (time defaults to today). Ledger entries cannot be edited or deleted. | Entry type | When it is created | Direction | | --- | --- | --- | | Collection credit | A collection order is credited (`success`); the amount is the settlement amount | Increase | | Late credit | A collection order changes to `late_credited`; the amount is the settlement amount. **No Callback is sent in this case**, so use this to reconcile | Increase | | Payout debit | A payout order is paid out (`paid`); the amount is the total deducted | Decrease | | Manual increase, Manual decrease | ST Pay adjusts the balance manually; the "Reason / Remark" column is ST Pay's explanation | Increase or decrease | Each entry shows the balance before and after, and the related order number, so you can reconcile it line by line with your own records. ## Staff accounts The merchant supervisor can manage merchant staff accounts of their own merchant: | Action | Details | | --- | --- | | Create account | Enter a username and an initial password (same rules as above). The staff member must change the password at first login. Usernames may contain only letters, digits, and `.` `_` `-`, must start with a letter or digit, and can be up to 64 characters. | | Disable | The account can no longer log in, and any active session ends immediately. **It cannot be set back to active.** | | Unlock | Use this when a staff account is locked after 5 consecutive wrong passwords. | | Reset password | Enter a new initial password. The old password stops working immediately, and the staff member must change it at the next login. | | Permissions | Turn off the "Order query" page or the "Show full account number" action for that staff member. Takes effect as soon as you save. | Merchant supervisor accounts are created, disabled, unlocked, and reset by ST Pay. For accounts created by ST Pay, the list shows "ST Pay" as the creator. --- --- url: /en/guide/conventions.md --- # Request and response format ## Requests * Always use HTTPS and `POST`, with `Content-Type: application/json`. The request body must be encoded in UTF-8. * The body is a JSON object. **Every field value must be a JSON string**, including amounts, for example `"amount": "1000.00"`. Sending the number `1000.00` will fail (with `invalid_signature` or `invalid_parameter`). * Every request must include `api_identity`, `timestamp`, and `sign` (see [Signature](./signature.md)). * `timestamp` is the time the request is sent, in the same format as [time](#data-formats) below (UTC+8, `yyyy-MM-dd HH:mm:ss`). A request that differs from the ST Pay server time by more than 5 minutes (in either direction) is rejected with `timestamp_expired`. Enable NTP time synchronization on your servers. * Sending an empty string `""` for an optional field is treated the same as not sending the field. ## Responses All APIs return the same structure: | Field | Description | | --- | --- | | `code` | `OK` on success; on failure, an English error code (see [Error codes](../reference/errors.md)). **Your program must use this field to determine the result.** | | `message` | A description in Traditional Chinese, intended for humans to read. Its wording may change, so do not use it to determine the result. | | `data` | The response data on success; `null` on failure. | HTTP status code: `200` on success; on failure, a 4xx or 5xx code depending on the type of error (see [Error codes](../reference/errors.md)). ```json { "code": "merchant_order_no_duplicate", "message": "商戶單號重複", "data": null } ``` ## Data formats | Type | Format | | --- | --- | | Amount | String. In requests: greater than 0, at most 2 decimal places (counted literally, so `"1000.500"` has 3), no more than `9999999999999999.99`, and only digits and a decimal point, for example `"1000"`, `"1000.5"`, `"1000.50"`. `"1,000.00"`, `"1000.001"`, `"-5"`, and `"0"` are all invalid.Amounts in responses and Callbacks always have 2 decimal places, for example `"1000.50"`. There is no currency field. | | Time | String in UTC+8, in the format `yyyy-MM-dd HH:mm:ss`, for example `"2026-10-13 10:30:05"`. | | ST Pay order number `order_no` | A string generated by ST Pay. Store it as an opaque string; do not parse its contents or rely on its format. | | Merchant order number `merchant_order_no` | Generated by you. 1 to 200 characters, containing only English letters, digits, underscores `_`, and hyphens `-`. | ## Data retention Orders are retained for 6 months from creation and are then deleted. After deletion, an order query returns `order_not_found`, and the merchant order number can be used again. --- --- url: /en/guide/security.md --- # Authentication and IP whitelist ## Order of checks Each request is checked in the following order. If any check fails, an error is returned: 1. **Signature**: `api_identity` must be an active credential, and `sign` must be correct. If not, `invalid_signature` (HTTP 401) is returned. 2. **Timestamp**: `timestamp` is required, in the format `yyyy-MM-dd HH:mm:ss` (UTC+8), and must not differ from the ST Pay server time by more than 5 minutes. If it is missing, `missing_parameter` is returned; if the format is invalid, `invalid_parameter`; if it differs by more than 5 minutes, `timestamp_expired` (HTTP 400). The timestamp is checked only after the signature passes, so a tampered `timestamp` returns `invalid_signature`. 3. **Source IP**: The source IP must be on the merchant's API whitelist. If the whitelist is empty, all requests are rejected. If not, `ip_not_allowed` (HTTP 403) is returned. 4. **Merchant status** (checked only when creating an order): A disabled merchant cannot create orders, and `merchant_disabled` (HTTP 403) is returned. You can still query orders while disabled. ## Keeping your secret key safe * Keep the secret key only on your server. Never put it in web pages, apps, or any program distributed to end users. * If your secret key is leaked or lost, contact ST Pay immediately to create a new credential and disable the old one. ## Key rotation A merchant can have multiple active credentials at the same time, and each of them can be used to call the API. Callbacks are always signed with the secret key of the **most recently created active** credential, so we recommend rotating your key in the following order: 1. First, make your Callback signature verification able to accept two keys at the same time. (You will only receive the new key after step 2, but you can prepare the place where it will be configured in advance.) 2. Ask ST Pay to create a new credential, and add the new key to your Callback signature verification. As soon as the new credential is created, Callbacks are signed with the new key. Callbacks received before you add the new key will fail signature verification; confirm those orders with an [order query](../api/query.md), or ask ST Pay to resend them. 3. Switch your API calls to the new `api_identity` and secret key. 4. After confirming that no requests use the old credential anymore, ask ST Pay to disable the old credential, and remove the old key from your Callback signature verification. --- --- url: /en/guide/signature.md --- # Signature API requests, API responses, and Callbacks use the same signature rules (HMAC-SHA256). ## How to compute the signature 1. Take **all** fields in the body except `sign`, and exclude fields whose value is `null`. Empty strings `""` are not excluded. 2. Sort the fields by **field name** in ascending order of character code (ASCII). 3. Write each field as `field_name=field_value` and join them with `&` into a single string (hereafter the "string to sign"). Insert field values exactly as they are: **do not URL-encode** them, and do not trim whitespace. 4. Compute HMAC-SHA256 over the UTF-8 bytes of the string to sign, using your API secret key as the key. 5. Convert the result to a **lowercase** hexadecimal string (64 characters) and put it in the `sign` field. ::: warning Common mistakes * Use the secret key directly as a UTF-8 string key. **Do not** Base64-decode it. (The key looks like Base64, but it does not need to be decoded.) * The values you sign must be exactly the same as the values actually sent in the JSON. For example, if you sign `1000.00` but send `"1000"`, the signature will not match. * Do not use functions such as `http_build_query`, `URLSearchParams`, or `urlencode` to build the string; they apply URL encoding. * Compute Chinese field values (such as `payer_name`) in UTF-8, not in Big5 or any other encoding. * The signature covers every field in the body, including fields not listed in this document. Do not add any fields after signing. * `callback_url` is also signed. Put it into the string to sign exactly as sent; do not encode it, even if its query string contains `?`, `&`, or `=`. * `timestamp` is also signed. Take the current time and sign right before sending; do not reuse old requests. A request that differs from the ST Pay server time by more than 5 minutes is rejected (`timestamp_expired`). ::: ## Example The following uses a sample credential. You can use it to check whether your code produces the same result (`timestamp` is a sample value; use the current time in real calls): | Item | Sample value | | --- | --- | | `api_identity` | `qp_Xk3vT9aLm2QwE7rB1nZc` | | API secret key | `Rk8vN2qXw5LpT9sJc3HmY7bF1dGz6AeU4oKi0Wn-QtE` | Fields to send (before signing): ```json { "api_identity": "qp_Xk3vT9aLm2QwE7rB1nZc", "merchant_order_no": "MO20261013-0001", "merchant_user_id": "U10086", "payer_name": "王小明", "amount": "1000.00", "channel_type": "fps", "response_mode": "url", "callback_url": "https://shop.example.com/qpay/callback", "timestamp": "2026-10-13 10:00:00" } ``` The string to sign after sorting and joining: ```text amount=1000.00&api_identity=qp_Xk3vT9aLm2QwE7rB1nZc&callback_url=https://shop.example.com/qpay/callback&channel_type=fps&merchant_order_no=MO20261013-0001&merchant_user_id=U10086&payer_name=王小明&response_mode=url×tamp=2026-10-13 10:00:00 ``` The HMAC-SHA256 result (that is, `sign`): ```text 566fac06060d54d7450b41b6858b1ddf33df9d37117ab8a8c495ce748469f2b2 ``` ## Code samples All of the following samples have been verified with the sample credential above and produce the same signature as shown above. When verifying a Callback, recompute the signature using all the fields you received, then compare the two values using a constant-time comparison. ::: code-tabs @tab Python 3 ```python import hmac, hashlib def qpay_sign(fields: dict, secret: str) -> str: items = sorted((k, v) for k, v in fields.items() if k != "sign" and v is not None) text = "&".join(f"{k}={v}" for k, v in items) return hmac.new(secret.encode("utf-8"), text.encode("utf-8"), hashlib.sha256).hexdigest() # Verify a Callback def qpay_verify(fields: dict, secret: str) -> bool: return hmac.compare_digest(qpay_sign(fields, secret), str(fields.get("sign", "")).lower()) ``` @tab Java 17+ ```java import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.util.HexFormat; import java.util.Map; import java.util.TreeMap; import java.util.stream.Collectors; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; public final class QpaySigner { public static String sign(Map fields, String secret) throws Exception { String text = new TreeMap<>(fields).entrySet().stream() .filter(e -> !"sign".equals(e.getKey()) && e.getValue() != null) .map(e -> e.getKey() + "=" + e.getValue()) .collect(Collectors.joining("&")); Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); return HexFormat.of().formatHex(mac.doFinal(text.getBytes(StandardCharsets.UTF_8))); } /** Verify a Callback */ public static boolean verify(Map fields, String secret) throws Exception { String sign = fields.getOrDefault("sign", ""); return MessageDigest.isEqual(sign(fields, secret).getBytes(StandardCharsets.UTF_8), sign.toLowerCase().getBytes(StandardCharsets.UTF_8)); } } ``` @tab PHP 7.4+ ```php $v) { $parts[] = $k . '=' . $v; // Do not use http_build_query; it applies URL encoding } return hash_hmac('sha256', implode('&', $parts), $secret); } // Verify a Callback function qpay_verify(array $fields, string $secret): bool { return hash_equals(qpay_sign($fields, $secret), strtolower((string) ($fields['sign'] ?? ''))); } ``` @tab Node.js ```js const crypto = require('crypto'); function qpaySign(fields, secret) { const text = Object.keys(fields) .filter((k) => k !== 'sign' && fields[k] !== null && fields[k] !== undefined) .sort() .map((k) => `${k}=${fields[k]}`) .join('&'); return crypto.createHmac('sha256', secret).update(text, 'utf8').digest('hex'); } // Verify a Callback function qpayVerify(fields, secret) { const expected = Buffer.from(qpaySign(fields, secret)); const actual = Buffer.from(String(fields.sign || '').toLowerCase()); return expected.length === actual.length && crypto.timingSafeEqual(expected, actual); } ``` @tab Go ```go import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "sort" "strings" ) // Also use map[string]any when decoding a response or Callback: null values become nil and are not signed func QpaySign(fields map[string]any, secret string) string { keys := make([]string, 0, len(fields)) for k, v := range fields { if k != "sign" && v != nil { keys = append(keys, k) } } sort.Strings(keys) parts := make([]string, len(keys)) for i, k := range keys { parts[i] = k + "=" + fields[k].(string) } mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(strings.Join(parts, "&"))) return hex.EncodeToString(mac.Sum(nil)) } // Verify a Callback func QpayVerify(fields map[string]any, secret string) bool { sign, _ := fields["sign"].(string) return hmac.Equal([]byte(QpaySign(fields, secret)), []byte(strings.ToLower(sign))) } ``` ::: ## Verifying responses Successful responses (`code` is `OK`) include `sign` in `data`. ST Pay computes it with **the API secret key used for that request**, using the same rules as above: all fields in `data` except `sign`, excluding fields whose value is `null`. * Verify the signature before using the response, especially `account_no` (the account the payer transfers to) and `payment_url` returned when creating a collection order. * If verification fails, the response may have been tampered with in transit. Do not give the account or URL to the payer; check the order with an [Order query](../api/query.md) instead. * You can use the verification functions above directly by passing in the response's `data`. * Error responses have `data` set to `null` and carry no signature. ## Full request example ```bash curl -X POST 'https://merchant.vincoterra.com/api/v1/collect/orders' \ -H 'Content-Type: application/json' \ -d '{ "api_identity": "qp_Xk3vT9aLm2QwE7rB1nZc", "merchant_order_no": "MO20261013-0001", "merchant_user_id": "U10086", "payer_name": "王小明", "amount": "1000.00", "channel_type": "fps", "response_mode": "url", "callback_url": "https://shop.example.com/qpay/callback", "timestamp": "2026-10-13 10:00:00", "sign": "566fac06060d54d7450b41b6858b1ddf33df9d37117ab8a8c495ce748469f2b2" }' ``` --- --- url: /en/reference/banks.md --- # Bank codes For bank transfer payouts, `bank_code` must be one of the following. ST Pay maintains this list and will notify you separately of any changes. | `bank_code` | Bank name | | --- | --- | | `003` | Standard Chartered Bank (Hong Kong) Limited | | `004` | The Hongkong and Shanghai Banking Corporation Limited | | `009` | China Construction Bank (Asia) Corporation Limited | | `012` | Bank of China (Hong Kong) Limited | | `015` | The Bank of East Asia, Limited | | `016` | DBS Bank (Hong Kong) Limited | | `018` | China CITIC Bank International Limited | | `020` | CMB Wing Lung Bank Limited | | `024` | Hang Seng Bank Limited | | `025` | Shanghai Commercial Bank Limited | | `027` | Bank of Communications Co., Ltd. Hong Kong Branch | | `028` | Public Bank (Hong Kong) Limited | | `035` | OCBC Bank (Hong Kong) Limited | | `039` | Chiyu Banking Corporation Limited | | `040` | Dah Sing Bank, Limited | | `041` | Chong Hing Bank Limited | | `043` | Nanyang Commercial Bank, Limited | | `072` | Industrial and Commercial Bank of China (Asia) Limited | | `128` | Fubon Bank (Hong Kong) Limited | | `250` | Citibank (Hong Kong) Limited | | `382` | Bank of Communications (Hong Kong) Limited | | `387` | ZA Bank Limited | | `388` | Livi Bank Limited | | `389` | Mox Bank Limited | | `390` | WeLab Bank Limited | | `391` | Fusion Bank Limited | | `392` | PAObank Limited | | `393` | Ant Bank (Hong Kong) Limited | | `395` | Airstar Bank Limited | --- --- url: /en/reference/checklist.md --- # Go-live checklist 1. The signature computed with the [sample credential](../guide/signature.md#example) matches the one in this documentation. 2. All field values are sent as strings, and amounts are not sent as a numeric type. 3. Collection order creation has been tested in both url and json modes (whichever you use) and on both the fps and bank_transfer channels. 4. The payment screen displays `payable_amount`, not the original amount. 5. Payout order creation has been tested with all three FPS account formats (whichever you use) and with bank transfer. 6. Callback endpoint: verifies signatures correctly, discards Callbacks whose signature does not match, handles duplicate notifications idempotently, and responds `ok` within 10 seconds. 7. Callbacks are matched to orders by `order_no`, and collections can be distinguished from payouts. 8. A periodic order query mechanism is in place to handle missing Callbacks and `late_credited` orders. 9. Order creation timeouts and `internal_error` are confirmed by querying with the merchant order number, instead of being treated as failures right away. 10. Your program determines results by `code` and does not rely on the `message` text. 11. The production credentials, domain, and IP whitelist have been requested separately, and the secret key is stored only on the server side. 12. Your server time is accurate (NTP time synchronization is recommended), and every request's `timestamp` is the time it is sent. 13. Every order creation request includes `callback_url`. The URL is a valid `http` or `https` URL (both are accepted), the domain resolves on the public internet to public IP addresses only, it does not redirect (3xx), and your server responds with HTTP 2xx. 14. A successful collection is credited with `received_amount` (or `settlement_amount`). If it differs from the expected amount, it goes to manual review instead of being credited automatically. 15. An order already in a final status is never rolled back by a later Callback with a different status; when in doubt, the [order query](../api/query.md) result prevails. --- --- url: /en/reference/errors.md --- # Error codes When you create an order, ST Pay performs its checks in the following order. If a request violates more than one rule, only the first error is returned: Signature → timestamp → source IP → merchant status → required fields and formats → `callback_url` environment and address rules → per-transaction minimum and maximum → duplicate merchant order number → (collection) pending order limit per member → (collection) receiving account and tail allocation / (payout) balance. Before all of these: if the body is not a JSON object or `Content-Type` is not `application/json`, `invalid_parameter` is returned immediately; a request without `api_identity` or `sign` returns `invalid_signature`. The `message` value is returned in Traditional Chinese and is intended for humans to read; its wording may change. Your program must use `code` to determine the result. In the table below, the last column starts with the English meaning of each `message` in quotation marks. | `code` | HTTP | `message` | Cause and what to do | | --- | --- | --- | --- | | `invalid_signature` | 401 | 簽章驗證失敗 | *"Signature verification failed."* `api_identity` is wrong, the credential has been disabled, or the signature does not match. Check your code against the [signature example](../guide/signature.md#example), and make sure you are using the credential for the correct environment. Sending a field value as a number or boolean returns this error or `invalid_parameter`, so send every value as a string. A request without `api_identity` or `sign` returns it too. | | `timestamp_expired` | 400 | 請求時間與伺服器時間相差超過 5 分鐘,請校正您的伺服器時間(伺服器目前時間:yyyy-MM-dd HH:mm:ss) | *"The request time differs from the server time by more than 5 minutes. Please correct your server time (current server time: yyyy-MM-dd HH:mm:ss)."* `timestamp` differs from the ST Pay server time by more than 5 minutes (in either direction). The message includes the current server time so you can see the difference. Enable NTP to keep your server time accurate, and take the time and sign right before sending. | | `ip_not_allowed` | 403 | 來源 IP 不在白名單 | *"Source IP is not on the whitelist."* Check your server's actual egress IP, and ask ST Pay to add it to the whitelist; it takes effect in about 1 minute. If the source IP is not on any whitelist, the request is blocked at the gateway: it returns HTTP 403 with an HTML body instead of JSON. The fix is the same. | | `merchant_disabled` | 403 | 商戶已停用 | *"Merchant is disabled."* Orders cannot be created while the merchant is disabled. Contact ST Pay. | | `permission_denied` | 403 | 無此操作權限 | *"No permission for this operation."* Usually caused by a misspelled path under `/api/v1/`. Using `GET`, or a path outside `/api/v1/`, gets HTTP 404 with a non-JSON body directly from the gateway. | | `missing_parameter` | 400 | 缺少必填欄位 | *"Required field missing."* A required field was not sent, is `null`, or is an empty string; `callback_url` is whitespace only; a bank transfer payout is missing a bank field; or an order query did not include any order number. | | `invalid_parameter` | 400 | 欄位值不合法 | *"Invalid field value."* A field value has an invalid format, length, characters, or enumerated value; a field value is not a string; the body is not a valid JSON object, or `Content-Type` is not `application/json`; `callback_url` has an invalid format (see [Callback URL](../api/callback.md#callback-url) for the rules); an FPS payout includes bank fields; or the bank code is not in the list. | | `invalid_amount` | 400 | 金額格式不正確 | *"Incorrect amount format."* The amount is 0, negative, has more than 2 decimal places (counted literally, so `1000.500` has 3), is greater than `9999999999999999.99`, or is not a number. | | `missing_member_identity` | 400 | 請提供會員唯一識別或真實姓名 | *"Please provide the member's unique identifier or real name."* Neither `merchant_user_id` nor `payer_name` was sent when creating a collection order. | | `invalid_callback_url` | 400 | Callback 網址不符合規定:不可為本機或內網位址 | *"The Callback URL is not allowed: it must not be a local or private network address."* The host of the `callback_url` sent when creating the order is local, a private network address, an internal domain, or a non-standard IP notation. See [Callback URL](../api/callback.md#callback-url). Format errors (a value that is not a URL, is longer than 500 characters, contains whitespace, or breaks another format rule in that section) return `invalid_parameter`. | | `amount_out_of_range` | 422 | 金額超出限額 | *"Amount exceeds the limits."* The original amount or payout amount is outside the per-transaction minimum and maximum. | | `merchant_order_no_duplicate` | 409 | 商戶單號重複 | *"Duplicate merchant order number."* See [Duplicate requests and confirming results](./idempotency.md). | | `user_active_orders_limit` | 422 | 有效訂單已達上限 | *"Active order limit reached."* The same member already has 10 pending collection orders. Wait until one of them completes or expires before creating another. | | `channel_full` | 422 | 通道已滿,請輸入其他金額再試 | *"Channel is full. Please enter a different amount and try again."* For the same channel and the same original amount, all identification tails are already taken by pending orders. Ask the payer to try again with a different amount. | | `no_available_card` | 422 | 通道已滿,請輸入其他金額再試 | *"Channel is full. Please enter a different amount and try again."* The channel currently has no available receiving account. Try again later or use the other channel. If this keeps happening, contact ST Pay. | | `insufficient_balance` | 422 | 商戶餘額不足 | *"Insufficient merchant balance."* When creating a payout order, the available balance is less than the payout amount plus the fee. | | `order_not_found` | 404 | 查無此單 | *"Order not found."* The order does not exist, does not belong to you, or has been deleted after the retention period. | | `query_conflict` | 400 | 訂單號與商戶單號指向不同訂單 | *"The order number and merchant order number refer to different orders."* Both order numbers were sent in an order query, but they do not refer to the same order. | | `internal_error` | 500 | 系統錯誤,請以商戶單號查單確認結果 | *"System error. Please query by merchant order number to confirm the result."* An internal ST Pay error; the order may or may not have been created. **Do not simply treat it as a failure.** Query by merchant order number to confirm. | `channel_full` and `no_available_card` have the same `message`; use `code` to tell them apart. If you get `invalid_parameter`, `invalid_signature`, or a similar error and cannot find the cause, give your ST Pay integration contact your `api_identity`, the merchant order number (or, for a query, the order number you queried), and the time (UTC+8). ST Pay can look up the API error log, which shows which field was rejected or why the signature failed. ST Pay only logs requests with a valid `api_identity`; if the `api_identity` itself is wrong, nothing is logged, so check it first. --- --- url: /en/reference/idempotency.md --- # Duplicate requests and confirming results | Situation | Result and recommended action | | --- | --- | | Creating an order again with a merchant order number that has already been used | Returns `merchant_order_no_duplicate`, regardless of the status of the original order and without comparing the contents (even if the amount is different). No second order is created. The response does not include the original order's data; use the merchant order number to [query the order](../api/query.md). | | The order creation request timed out, the connection was interrupted, or you received `internal_error` | **Do not simply treat it as a failure.** Query with the same merchant order number: if the order is found, rely on the query result (for an order that is pending and not yet expired, the query also returns the payment URL or receiving account, so the payer can still pay that order; see [Order query](../api/query.md)); if `order_not_found` is returned, ST Pay did not create the order, and you can create it again with the same merchant order number. | | Initiating the transaction again after the order was rejected | You must use a new merchant order number. | | Scope of merchant order number uniqueness | Within the same merchant, merchant order numbers must be unique among collection orders and, separately, among payout orders. After an order is deleted once it is older than 6 months, its merchant order number can be used again. | | Repeated order queries | Read-only. Each call returns the current status and changes nothing. | | Multiple order creation requests with the same merchant order number sent almost simultaneously | They are processed one at a time. Only the first succeeds, and the others return `merchant_order_no_duplicate`. | | Multiple Callbacks received for the same order | Use `order_no` plus `status` to determine whether it has already been processed, and respond `ok` directly to duplicates. | --- --- url: /en/reference/status.md --- # Order status and amounts "Back office label" is the status text shown in the [merchant back office](../guide/backoffice.md). ## Collection orders ```text pending ──┬── Payment credited ───────────────────────→ success (Callback sent) ├── Rejected manually by ST Pay ─────────────→ rejected (Callback sent; final) └── Rejected by system on expiry (30 min) ──→ rejected (Callback sent) │ └── Payment receipt confirmed later ──→ late_credited (no Callback) ``` | `status` | Back office label | Meaning | Final result? | | --- | --- | --- | --- | | `pending` | Pending | Waiting for the payer's transfer and ST Pay's confirmation | No | | `success` | Success | Payment credited; the settlement amount has been added to your balance | Yes | | `rejected` | Rejected | Nothing is credited and no fee is charged | Final if rejected manually; **an order rejected on expiry by the system can still change to `late_credited` afterwards** | | `late_credited` | Late credited | Late credit: the order was rejected on expiry by the system, but it was later confirmed that the payer did pay, and the settlement amount has been added to your balance | Yes | ::: warning No Callback is sent for late credits You first receive one Callback with `rejected`. If the same order later changes to `late_credited`, you will not receive another notification, but your balance has already increased. Recommendation: When a payer reports having paid but the order shows as rejected, confirm with an order query. Also reconcile regularly using order queries or the [balance ledger in the merchant back office](../guide/backoffice.md#balance-ledger) (entry type "Late credit"), checking whether orders whose `reject_reason` is 「訂單已逾時」 ("order expired") have changed to `late_credited`. ::: ## Payout orders ```text pending ──┬── Paid out by ST Pay ──→ paid (balance deducted; Callback sent) └── Rejected by ST Pay ──→ rejected (nothing deducted; Callback sent) ``` | `status` | Back office label | Meaning | Final result? | | --- | --- | --- | --- | | `pending` | Pending | Waiting for ST Pay to pay out | No | | `paid` | Paid | Paid out; the payout amount plus the fee has been deducted from your balance | Yes | | `rejected` | Rejected | Nothing is deducted and no fee is charged | Yes | Payout orders do not expire automatically. They remain `pending` until ST Pay processes them. ## Amounts and fees | Item | Calculation | | --- | --- | | Collection payable amount `payable_amount` | Original amount + identification tail (0.01–0.99) | | Collection fee `fee` | Received amount × collection fee rate, truncated (rounded down) to 2 decimal places | | Collection settlement amount `settlement_amount` | Received amount − fee | | Payout fee `fee` | Payout amount × payout fee rate, truncated (rounded down) to 2 decimal places | | Payout total deducted `total_deducted` | Payout amount + fee | * The fee rate is **locked in at the moment the order is created**. Later rate changes affect only newly created orders. * The received amount `received_amount` is the amount that ST Pay actually confirmed as credited, and the fee and settlement amount are calculated from it. If the amount the payer transferred differs from the payable amount, the received amount may differ from `payable_amount`. * The fee for a rejected order is `0.00`. * Each order's original amount or payout amount must be within the per-transaction minimum and maximum set by ST Pay (inclusive of the limits themselves). The limits are set separately by "collection or payout" and by "channel type". Check the current values with ST Pay. Example: With a collection fee rate of 2.5% and a received amount of 1000.37, the fee is 1000.37 × 2.5% = 25.00925, which is truncated to 25.00, and the settlement amount is 975.37.