Skip to content

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.

ItemValue
EndpointPOST https://api.anonypay.io/api/merchant/createWithdrawOrder
Request bodyBase64 ciphertext of the business JSON encrypted with RSA-OAEP
Request headersANONY-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

FieldTypeRequiredDescription
userOrderstringYesMerchant order number (unique; resubmitting it returns the original order — see Idempotency)
addressstringYesWithdrawal destination address
amountstringYesWithdrawal amount; pass it as a string to avoid precision issues; must be ≥ the currency's minimum withdrawal amount withdraw_min
currencystringYesCurrency — see the enum below
withdrawTypestringYesWithdrawal type: normal or financialConfirmation (finance review)
messagestringNoRemarks
callbackUrlstringNoCallback 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)

FieldTypeDescription
apiOrderstringPlatform order number
userOrderstringMerchant order number
amountstringAmount
currencystringCurrency
feestringFee
addressstringDestination address
messagestringRemarks

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:

  1. Platform-address guard: the destination address matches one of the platform's own hot-wallet/collection addresses → withdrawAddressError
  2. 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)
  3. withdrawType validity: not normal / financialConfirmationwithdrawTypeError
  4. Finance-review precondition: withdrawType=financialConfirmation but the merchant has not yet bound an auditor Telegram → auditorTelegramNotBound
  5. Merchant withdrawal-mode policy (the mode is configured on the platform side — see the next section): mode off rejects everything → merchantWithdrawDisabled; mode financialConfirmation forces finance review, so a non-finance-review request → onlySupportFinancialConfirmation; requesting normal while the merchant's mode is not normalwithdrawTypeNotAllowed
  6. Currency: not in the supported list → currencyNotSupported; currency does not exist → currencyNotExist
  7. Amount: malformed or invalid value → invalidAmount; below the currency's minimum withdrawal amount → withdrawMinAmount
  8. Address format: the destination address is validated against the currency's rules → invalidAddress
  9. Balance: merchant balance < amount + feeinsufficientBalance
  10. Daily-limit recheck (merchants in dailyLimit mode 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 message tells you which step a request failed at. A couple of checks actually sit in slightly different spots: in step 5, the "normal requested but mode is not normal" check actually runs after currency validation, and the callbackUrl validity check (invalidCallbackUrl) sits between the mode policy and the amount checks. When troubleshooting, trust the message.
  • 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 same userOrder is 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 modeMeaningwithdrawType=normalwithdrawType=financialConfirmation
offWithdrawals disabledmerchantWithdrawDisabledmerchantWithdrawDisabled
normalNormal mode✅ Allowed✅ Allowed (auditor Telegram must be bound)
financialConfirmationForced finance reviewonlySupportFinancialConfirmation✅ Allowed
dailyLimitDaily-limit modewithdrawTypeNotAllowed✅ Allowed (today's running total is rechecked against the daily limit at order time)

Regardless of the merchant's mode, any request with withdrawType=financialConfirmation requires that the merchant has an auditor Telegram bound (otherwise auditorTelegramNotBound).

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=withdrawalPendingConfirm callback (no txid / orderStatus): respond SUCCESS to release the order, or FAIL to reject and refund it (moving it to WithdrawRefund).
  • Once the order reaches a final state, the platform sends a businessType=withdraw callback (with txid / 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:

messageMeaningSuggested handling
withdrawAddressErrorDestination is one of the platform's own addresses; rejectedCheck whether address was filled in incorrectly
withdrawTypeErrorwithdrawType is not normal / financialConfirmationFix the parameter
auditorTelegramNotBoundFinance-review withdrawals require an auditor Telegram to be bound firstBind an auditor Telegram in the merchant console, then retry
merchantWithdrawDisabledWithdrawals are disabled for this merchant (mode off)Contact the platform to enable withdrawals
onlySupportFinancialConfirmationThe merchant is forced into finance review; only withdrawType=financialConfirmation is acceptedSwitch to finance-review withdrawals
withdrawTypeNotAllowedThe current merchant mode does not allow normal withdrawalsCheck the mode table above and use an allowed type
currencyNotSupportedcurrency is not in the supported list (the 13-value enum)Fix the currency
currencyNotExistThe currency does not existFix the currency
invalidAmountMalformed or invalid amountPass a valid positive amount as a string
withdrawMinAmountBelow 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)
invalidAddressThe address does not match the currency's formatPre-validate with the public endpoint isAddress
insufficientBalanceInsufficient balance (must be ≥ amount + fee)Check your balance via merchantInfo and leave headroom for the fee
withdrawDailyLimitExceededMerchant daily limit exceeded (dailyLimit mode)Retry the next day, or contact the platform to adjust the limit
operationFailedDebit/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/orderStatus

For Go / Java (assemble the request yourself using the exported Encrypt/Sign/Canonical functions), see the SDK overview.