Create order
Call it from your server to create one platform payment order for one piece of business.
POST {API_BASE}/api/pay/open/createOrder
X-Pay-Api-Key: <your API KEY>
Content-Type: application/jsonThe caller of this endpoint is a merchant system holding an API KEY, not a browser. Merchant identity comes only from the KEY.
Request fields
| Field | Type | Required | Description |
|---|---|---|---|
merchantOrderNo | string | Yes | Your order number. The idempotency key; retries must reuse it. Max length 64 |
amount | string | Yes | Decimal string in token units, e.g. "100" / "100.5". See "Amount conventions" below |
chainId | number | Yes | Which chain to collect on (from your available-chain list) |
tokenId | number | Yes | Which token on that chain |
subject | string | No | A title, shown on the checkout page and in order lists |
notifyUrl | string | No | Per-order callback URL, overriding the default on your merchant profile |
returnUrl | string | No | Where to send the buyer's browser after payment |
On notifyUrl versus returnUrl: the former is server to server (your backend ships the goods), the latter is the jump in the buyer's browser (your "payment complete" page). Many merchants need both.
curl -X POST "$API_BASE/api/pay/open/createOrder" \
-H "X-Pay-Api-Key: $PAY_API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"merchantOrderNo": "SHOP-2026-0001",
"chainId": 2,
"tokenId": 1,
"amount": "100.5",
"subject": "Order SHOP-2026-0001",
"notifyUrl": "https://your-shop.com/pay/notify",
"returnUrl": "https://your-shop.com/pay/done"
}'Amount conventions
This is the most common mistake in the whole API. Read it carefully.
| Endpoint | Shape of amount | Example |
|---|---|---|
| Open API create order (this page) | Decimal string in token units | "100" / "100.5" |
Checkout online order createCashierOrder | Integer string in the smallest unit | For a 6-decimal token, 100 is "100000000" |
They are deliberately opposite: a merchant's server already knows a human-readable amount, while the checkout frontend works with the result of parseUnits. Send the wrong shape and it will be rejected or misinterpreted — amounts differ by 10 to the power of decimals, and the other side may not notice immediately.
decimals is a property of the token (commonly 6 for USDT) and is available in the chain and token list.
Idempotency: merchantOrderNo is the key
Calling again with the same merchantOrderNo does not create a new order. It returns the same payment order, with idempotentReplay set to true.
- A retry after a network blip or timeout is safe with the same
merchantOrderNo. - To genuinely create another order, use a new
merchantOrderNo. - Why this matters: without an idempotency key, a retry can create two orders and collect two payments — and the platform holds no private key, so the extra payment cannot be refunded.
Response
{
"code": 0,
"data": {
"order": {
"orderNo": "PO20261011XXXXXXXXXXXX",
"merchantOrderNo": "SHOP-2026-0001",
"merchantNo": "M00000XXXX",
"status": 1,
"statusText": "待支付",
"orderType": 1,
"amountMinimal": "100500000",
"payAmountMinimal": "100500123",
"payAmountDisplay": "100.500123",
"receivedAmountMinimal": "0",
"chainId": 2,
"chainKey": "tron",
"chainType": 2,
"tokenId": 1,
"tokenSymbol": "USDT",
"tokenContract": "TXxx...contract address",
"tokenDecimals": 6,
"payAddress": "TXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"subject": "Order SHOP-2026-0001",
"txHash": "",
"confirmations": 0,
"requiredConfirmations": 19,
"expiresAt": "2026-10-11T10:30:00Z",
"identify": {
"mode": 1,
"orderNo": "PO20261011XXXXXXXXXXXX",
"calldataSuffix": "0x...order identify code..."
},
"notifyUrl": "https://your-shop.com/pay/notify"
},
"idempotentReplay": false
},
"msg": "下单成功"
}Key fields
| Field | Description |
|---|---|
orderNo | Platform order number. Used by the query API and the checkout page to locate this order |
idempotentReplay | true means an existing payment order was reused, not created. Read it on every retry, or you will think you opened a second order |
amountMinimal | The amount you asked for, in the smallest unit |
payAmountMinimal | The amount that should actually be transferred (smallest unit). In amount-suffix mode it carries an extra suffix over amountMinimal — use this one |
payAmountDisplay | The same value formatted for humans, so you don't divide by decimals yourself |
receivedAmountMinimal | What actually arrived on-chain. For reconciliation; it may exceed the expected amount (the buyer overpaid) or still be 0 (not scanned yet) |
payAddress | Where this order should be sent (snapshotted at creation) |
tokenContract | The token contract address. Used to build the transaction — the transaction's to is the contract, not the collection address; get this backwards and no tokens move |
identify | The order identification data. Pass it through verbatim; change its case or add a prefix and the scanner will not recognise this order |
notifyUrl | The callback URL actually in effect for this order. It is echoed back so you can immediately see that the URL you passed did not take effect |
expiresAt | The payment validity window for this order |
Use payAmountMinimal, not amountMinimal
The amount the buyer should transfer is payAmountMinimal. When it differs from amountMinimal, that extra part is the identification suffix, not an error.
::: note Response text is in Chinese statusText and msg come back in Chinese from the API. Branch on status (numeric) and code, and treat the text as human-facing only. :::
Common rejection reasons
When an order is rejected, the message is written for a human and is passed through verbatim (for example "amount below the minimum"). These do not consume idempotency and can be retried after a fix. See Errors for the full troubleshooting table.
Related
- Auth and signatures — the API KEY and middleware order
- Query order — verify the state yourself
- Callbacks and verification — receiving the callback