Create a Withdrawal Order — createWithdrawOrder
Creates a withdrawal order. Once validation passes, the platform debits your merchant balance and creates the order in a single transaction, after which the order enters the withdrawal state machine. The final outcome is delivered asynchronously via the withdrawal status callback; you can also poll it at any time with checkWithdrawOrder.
| Item | Value |
|---|---|
| Endpoint | POST https://api.anonypay.io/api/merchant/createWithdrawOrder |
| Request body | Base64 ciphertext of the business JSON encrypted with RSA-OAEP |
| Request headers | ANONY-APP-ID / ANONY-TOKEN / ANONY-TIMESTAMP / ANONY-NONCE / ANONY-SIGN (see the protocol reference) |
| Response | { "code": 10000, "data": "<ciphertext>", "message": "success" }; verify the signature on data, then decrypt it |
Request Parameters
| Field | Type | Required | Description |
|---|---|---|---|
userOrder | string | Yes | Merchant order number (unique; resubmitting it returns the original order — see Idempotency) |
address | string | Yes | Withdrawal destination address |
amount | string | Yes | Withdrawal amount; pass it as a string to avoid precision issues; must be ≥ the currency's minimum withdrawal amount withdraw_min |
currency | string | Yes | Currency — see the enum below |
withdrawType | string | Yes | Withdrawal type: normal or financialConfirmation (finance review) |
message | string | No | Remarks |
callbackUrl | string | No | Callback URL for this order; if omitted, the merchant's default is used (an invalid URL fails with invalidCallbackUrl) |
currency enum for withdrawals (13 values):
trx trx.usdt eth eth.usdt eth.usdc solana solana.usdt solana.usdc bnb bnb.usdt bnb.usdc ton ton.usdt
Per-currency fee rates and minimum withdrawal amounts are available from the public endpoint getCurrencies (withdraw_fixed_fee / withdraw_percent_fee / withdraw_min_fee / withdraw_max_fee / withdraw_min).
USDC deposits on TRON are closed
USDC deposits on the TRON chain are closed: USDC sent to a platform TRON address will not be credited and will not trigger a callback. The withdrawal currency enum does not include trx.usdc either. On TRON, use only trx / trx.usdt; if you need USDC, use eth.usdc / solana.usdc / bnb.usdc instead.
Response Fields (new order created)
| Field | Type | Description |
|---|---|---|
apiOrder | string | Platform order number |
userOrder | string | Merchant order number |
amount | string | Amount |
currency | string | Currency |
fee | string | Fee |
address | string | Destination address |
message | string | Remarks |
Example of the decrypted data:
json
{
"apiOrder": "202607081234567890",
"userOrder": "W20260708-0001",
"amount": "100.5",
"currency": "trx.usdt",
"fee": "1",
"address": "TXYZa1b2c3d4e5f6g7h8i9j0kLmNoPqRsT",
"message": "玩家提现"
}This response only means the order was accepted (your balance has been debited and the order has entered processing) — it does not mean the funds have landed on-chain. The final result is determined by the withdrawal status callback (see Callbacks).
Idempotency: Resubmitting the Same userOrder
Idempotent hits return a different field set
If the userOrder already exists (a resubmission), the platform will not debit you again — it returns the current state of the original order, with the same fields as the withdrawal status callback (including orderStatus / txid / toAddress), not the creation field set shown above (for example, the destination address field is toAddress, not address).
Branch on apiOrder / orderStatus in your code and do not assume a fixed field set. This also means retrying with the same userOrder after a network timeout is safe: you either create a new order or get back the original order's state — you are never debited twice.
Server-Side Validation Chain (in execution order)
Each request passes through the following checks in order; the first failure returns code=9999 with the corresponding message:
- Platform-address guard: the destination address matches one of the platform's own hot-wallet/collection addresses →
withdrawAddressError - Idempotent return: an order already exists for
(userOrder, merchant ID)→ the original order's current state is returned directly (no debit — see the section above) - withdrawType validity: not
normal/financialConfirmation→withdrawTypeError - Finance-review precondition:
withdrawType=financialConfirmationbut the merchant has not yet bound an auditor Telegram →auditorTelegramNotBound - Merchant withdrawal-mode policy (the mode is configured on the platform side — see the next section): mode
offrejects everything →merchantWithdrawDisabled; modefinancialConfirmationforces finance review, so a non-finance-review request →onlySupportFinancialConfirmation; requestingnormalwhile the merchant's mode is notnormal→withdrawTypeNotAllowed - Currency: not in the supported list →
currencyNotSupported; currency does not exist →currencyNotExist - Amount: malformed or invalid value →
invalidAmount; below the currency's minimum withdrawal amount →withdrawMinAmount - Address format: the destination address is validated against the currency's rules →
invalidAddress - Balance: merchant balance <
amount + fee→insufficientBalance - Daily-limit recheck (merchants in
dailyLimitmode only): today's accumulated total + this order's amount > the daily limit →withdrawDailyLimitExceeded
Once every check passes, the debit and the order creation happen in a single transaction; if the transaction fails, everything rolls back (you will never end up "debited with no order") and the API returns operationFailed.
Implementation details
- The order above reflects the server implementation, so the returned
messagetells you which step a request failed at. A couple of checks actually sit in slightly different spots: in step 5, the "normalrequested but mode is notnormal" check actually runs after currency validation, and thecallbackUrlvalidity check (invalidCallbackUrl) sits between the mode policy and the amount checks. When troubleshooting, trust themessage. - The step-10 daily-limit recheck runs serialized under a lock together with the debit and order creation, so concurrent orders cannot slip past the daily limit. Under high concurrency, individual requests may time out waiting for the lock and receive
operationFailed— simply retry (the sameuserOrderis idempotent).
Merchant Withdrawal Modes vs. withdrawType
A merchant account has one of four withdrawal modes (configured on the platform side — request parameters cannot change it; contact the platform to adjust it). Whether a given withdrawType is accepted depends on the current mode:
| Merchant withdrawal mode | Meaning | withdrawType=normal | withdrawType=financialConfirmation |
|---|---|---|---|
off | Withdrawals disabled | ❌ merchantWithdrawDisabled | ❌ merchantWithdrawDisabled |
normal | Normal mode | ✅ Allowed | ✅ Allowed (auditor Telegram must be bound) |
financialConfirmation | Forced finance review | ❌ onlySupportFinancialConfirmation | ✅ Allowed |
dailyLimit | Daily-limit mode | ❌ withdrawTypeNotAllowed | ✅ Allowed (today's running total is rechecked against the daily limit at order time) |
Regardless of the merchant's mode, any request with
withdrawType=financialConfirmationrequires that the merchant has an auditor Telegram bound (otherwiseauditorTelegramNotBound).
Withdrawal State Machine
Status flow after the order is created (these values are the orderStatus seen in callbacks and in checkWithdrawOrder):
normal withdrawal:
pendingConfirm ──► pendingTransfer ──► pendingCallback ──► success
(awaiting confirm) (transferring) (awaiting callback) (done)
financialConfirmation withdrawal:
pendingConfirm ──► pendingFinancialConfirmation ──► pendingCallback ──► success
(awaiting confirm) (awaiting finance approval) (awaiting callback) (done)
Side exits (an order not yet in a final state may move to):
WithdrawRefund rejected and refunded fail withdrawal failed- While the order is in the pending-confirmation stage, the platform sends you a
businessType=withdrawalPendingConfirmcallback (notxid/orderStatus): respondSUCCESSto release the order, orFAILto reject and refund it (moving it toWithdrawRefund). - Once the order reaches a final state, the platform sends a
businessType=withdrawcallback (withtxid/orderStatus). See Callbacks for the payload and response contract of both callback types.
Error Codes
Failures uniformly return code=9999, with message set to one of the following identifiers:
| message | Meaning | Suggested handling |
|---|---|---|
withdrawAddressError | Destination is one of the platform's own addresses; rejected | Check whether address was filled in incorrectly |
withdrawTypeError | withdrawType is not normal / financialConfirmation | Fix the parameter |
auditorTelegramNotBound | Finance-review withdrawals require an auditor Telegram to be bound first | Bind an auditor Telegram in the merchant console, then retry |
merchantWithdrawDisabled | Withdrawals are disabled for this merchant (mode off) | Contact the platform to enable withdrawals |
onlySupportFinancialConfirmation | The merchant is forced into finance review; only withdrawType=financialConfirmation is accepted | Switch to finance-review withdrawals |
withdrawTypeNotAllowed | The current merchant mode does not allow normal withdrawals | Check the mode table above and use an allowed type |
currencyNotSupported | currency is not in the supported list (the 13-value enum) | Fix the currency |
currencyNotExist | The currency does not exist | Fix the currency |
invalidAmount | Malformed or invalid amount | Pass a valid positive amount as a string |
withdrawMinAmount | Below the minimum withdrawal amount (the message contains the literal template 最低提现金额为 :amount :symbol, i.e. "the minimum withdrawal amount is :amount :symbol") | Raise the amount to at least withdraw_min (query getCurrencies) |
invalidAddress | The address does not match the currency's format | Pre-validate with the public endpoint isAddress |
insufficientBalance | Insufficient balance (must be ≥ amount + fee) | Check your balance via merchantInfo and leave headroom for the fee |
withdrawDailyLimitExceeded | Merchant daily limit exceeded (dailyLimit mode) | Retry the next day, or contact the platform to adjust the limit |
operationFailed | Debit/order creation failed and was fully rolled back (nothing was debited) | Retry with the same userOrder (idempotent, therefore safe); contact the platform if it keeps failing |
For the generic authentication, signature and anti-replay errors (signatureFailed / tokenExpired, etc.), see Error Codes.
Code Examples
php
require 'AnonyV2Client.php';
$client = new AnonyV2Client($appId, $serverPublicKeyB64, $merchantPrivateKeyB64);
$token = $client->getToken($secret); // handles md5 auth + signature verification & decryption to extract the token
$res = $client->post('createWithdrawOrder', [
'userOrder' => 'W20260708-0001',
'address' => 'TXYZa1b2c3d4e5f6g7h8i9j0kLmNoPqRsT',
'amount' => '100.5',
'currency' => 'trx.usdt',
'withdrawType' => 'normal',
'message' => '玩家提现',
], $token);
// $res['data'] is already signature-verified and decrypted
// Note: on an idempotent hit, the field set is the callback shape (orderStatus/txid/toAddress)js
const { AnonyV2Client } = require('./anony_v2');
const c = new AnonyV2Client(appId, serverPubB64, merchantPrivB64);
const token = await c.getToken(secret); // Node ≥18: handles md5 auth + signature verification & decryption to extract the token
const req = c.buildRequest({
userOrder: 'W20260708-0001',
address: 'TXYZa1b2c3d4e5f6g7h8i9j0kLmNoPqRsT',
amount: '100.5',
currency: 'trx.usdt',
withdrawType: 'normal',
message: '玩家提现',
}, token);
const r = await fetch(c.baseUrl + 'createWithdrawOrder', {
method: 'POST', body: req.body, headers: req.headers,
});
const body = await r.json();
if (body.code === 10000) {
const data = c.verifyResponse(body.data, r.headers.get('anony-timestamp'),
r.headers.get('anony-nonce'), r.headers.get('anony-sign'));
// On an idempotent hit, data contains orderStatus/txid/toAddress — branch on apiOrder/orderStatus
}python
from anony_v2 import AnonyV2Client
import requests
c = AnonyV2Client(app_id, server_pub_b64, merchant_priv_b64)
# See the getToken page for obtaining a token (auth_header + signature verification & decryption)
req = c.build_request({
"userOrder": "W20260708-0001",
"address": "TXYZa1b2c3d4e5f6g7h8i9j0kLmNoPqRsT",
"amount": "100.5",
"currency": "trx.usdt",
"withdrawType": "normal",
"message": "玩家提现",
}, token)
r = requests.post(c.base_url + "createWithdrawOrder", data=req["body"], headers=req["headers"])
body = r.json()
if body["code"] == 10000:
data = c.verify_response(body["data"], r.headers["ANONY-TIMESTAMP"],
r.headers["ANONY-NONCE"], r.headers["ANONY-SIGN"])
# On an idempotent hit, data contains orderStatus/txid/toAddress — branch on apiOrder/orderStatusFor Go / Java (assemble the request yourself using the exported Encrypt/Sign/Canonical functions), see the SDK overview.
Related Pages
- getToken — prerequisite step before any business endpoint
- checkWithdrawOrder — actively query an order
- Callbacks — payload and response contract for the
withdraw/withdrawalPendingConfirmcallbacks - Common endpoints —
getCurrenciesfor fees/minimums,isAddressfor address pre-validation - Error Codes