ST Pay Merchant APIST Pay Merchant API
Guide
API
Error codes
Back office
AI-assisted integration
  • 繁體中文
  • 简体中文
  • English
Guide
API
Error codes
Back office
AI-assisted integration
  • 繁體中文
  • 简体中文
  • English
  • Getting started

    • Overview
    • Request and response format
    • Authentication and IP whitelist
    • Signature
    • Merchant back office
    • AI-assisted integration
  • API

    • Create a collection order
    • Create a payout order
    • Order query
    • Callback notifications
  • Reference

    • Order status and amounts
    • Duplicate requests and confirming results
    • Error codes
    • Bank codes
    • Go-live checklist

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

EventstatusSent?
Collection succeededsuccessSent
Collection rejected (rejected manually by ST Pay, or rejected by the system when the 30 minutes expire)rejectedSent
Collection late creditlate_creditedNot sent. Confirm with an order query; see Order status
Payout paidpaidSent
Payout rejectedrejectedSent

Callback URL

  • Send it as callback_url each time you create an order (collection, payout). 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 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 rules are the same as for API requests and use your API secret key (see 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/1.1 200 OK
Content-Type: text/plain

ok

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

FieldCollection succeededCollection rejectedPayout paidPayout rejectedDescription
order_no✓✓✓✓ST Pay order number
merchant_order_no✓✓✓✓Merchant order number
statussuccessrejectedpaidrejectedOrder 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 result prevails.

Examples

All signatures below are actual values computed with the sample key.

Collection succeeded
{
  "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"
}
Collection rejected
{
  "order_no": "C202610131001178820000",
  "merchant_order_no": "MO20261013-0002",
  "status": "rejected",
  "payable_amount": "2000.58",
  "finished_at": "2026-10-13 10:31:17",
  "sign": "cd85bf732776bb2897f6e767dda873ce580330f979be3f812c390ec09af0d2f4"
}
Payout paid
{
  "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"
}
Payout rejected
{
  "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:

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
Prev
Order query