Skip to content

Error Codes

This page lists every business error code returned by the Merchant API. Start with the core convention:

  • Success: code == 10000, data contains the encrypted payload (verify the signature, then decrypt), and message is success.
  • Failure: code == 9999 (a single code shared by all business errors), message is a machine-readable English error key, and data is empty — failure responses contain nothing to decrypt.

Your error handling must therefore dispatch on the message key, not on code, and never string-match against display text.

Two cases where you won't get JSON back

  1. Source IP not on the whitelist: the server returns the plain-text string Oops, not a JSON envelope. If JSON parsing fails and the response body is Oops, first verify that the egress IP of the requesting server has been registered on the whitelist.
  2. Request bodies exceeding the platform limit (128KB by default) are rejected; in practice an oversized body also surfaces as paramError.

Always add a fallback for "response is not valid JSON" in your code — see the examples at the bottom of this page.

Error code reference

All errors share code=9999 and are distinguished by the message key:

message keyMeaningRecommended action
merchantNotExistMerchant does not existCheck that the ANONY-APP-ID header carries the merchant ID assigned by the platform, and that the merchant account is active. See Credentials
authFailedAuthentication failedOnly returned by getToken: verify that the Authorization header is auth + md5(APP-ID + "&" + secret) — note the & separator and the space after auth. See getToken
tokenExpiredToken has expiredCall getToken again for a fresh token, then retry the request. We recommend wrapping this in your client as "catch tokenExpired → auto-refresh → retry once"
signatureFailedSignature verification failedOne key, many causes — v2 timestamp/nonce problems also report this key. See the dedicated section below and the Troubleshooting guide
publicKeyNotExistMerchant public key not configuredThe platform has not yet registered your merchant public key (public_u). Contact the platform to complete key setup before testing
paramErrorInvalid parametersCheck required fields, field types, and spelling; send amount fields as strings. Note: an oversized request body (>128KB) also triggers this error
invalidCallbackUrlInvalid callback URLcallbackUrl must be a well-formed, publicly reachable URL; do not pass internal or loopback addresses. See Callbacks
addressTypeErrorInvalid address typeaddressType must be one of the supported live values: trx / solana / eth_bnb / ton. btc is reserved until Bitcoin deposits are opened. See createAddress
createAddressFailedAddress creation failedThe platform is temporarily unable to allocate an address. Retry later with the same userOrder (idempotent — no duplicate orders will be created); if the failure persists, contact the platform
withdrawAddressErrorWithdrawal address not acceptedThe withdrawal target address is not accepted (e.g. one of the platform's own deposit addresses). Use a genuine external receiving address instead
withdrawTypeErrorInvalid withdrawal typewithdrawType must be either normal or financialConfirmation. See createWithdrawOrder
auditorTelegramNotBoundAuditor Telegram not boundWithdrawals of type financialConfirmation require the merchant to bind a finance auditor's Telegram first; complete the binding in the merchant dashboard and retry
merchantWithdrawDisabledWithdrawals disabled for this merchantWithdrawals are switched off for this merchant account. Contact the platform to find out why and to re-enable them
onlySupportFinancialConfirmationOnly financial-confirmation withdrawals allowedYour merchant account is configured to enforce financial confirmation: set withdrawType to financialConfirmation and resubmit
withdrawTypeNotAllowedWithdrawal type not allowedThe requested withdrawType does not match your merchant account's withdrawal policy. Check the merchant configuration or ask the platform to adjust it
currencyNotSupportedCurrency not supportedcurrency is not among the 13 supported withdrawal currencies (e.g. trx.usdt, eth.usdc). Check spelling and case; see the currency enum
currencyNotExistCurrency does not existThe platform does not recognize the currency symbol you passed. Treat the symbol values returned by the public getCurrencies endpoint as authoritative
invalidAmountInvalid amountamount must be a valid positive number; send it as a string to avoid precision issues
withdrawMinAmountMinimum withdrawal amount is :amount :symbolThe placeholders are replaced with the actual floor for that currency. Query each currency's withdraw_min via getCurrencies and validate before submitting
invalidAddressInvalid addressThe withdrawal target address failed validation. Pre-validate the address/currency pair with the public isAddress endpoint before submitting
insufficientBalanceInsufficient balanceThe balance must cover amount + fee (the fee is charged on top). Call merchantInfo to check the balance first, and include the fee in your calculation
withdrawDailyLimitExceededDaily withdrawal limit exceededThe day's cumulative withdrawals have reached the merchant's daily limit. Try again the next day, or ask the platform to adjust the limit; do not auto-retry on this error
limitErrorPagination limit out of rangelimit on getDeposits must be between 1 and 100 (default 10). See getDeposits
withdrawOrderNotExistWithdrawal order not foundcheckWithdrawOrder found no order for that userOrder. Check that the order number is correct and belongs to the current merchant account. See checkWithdrawOrder
operationFailedOperation failedGeneric catch-all error. Retry later; if it persists, contact the platform with the request time, endpoint name, and userOrder

USDC deposits on TRON are closed

The deposit channel for USDC on the TRON chain is closed: USDC sent to a TRON address will not be credited and will not trigger a callback — do not direct users to deposit USDC via TRON. The withdrawal currency enum is limited to the 13 currencies above (trx.usdc is not included). For USDC, use eth.usdc / solana.usdc / bnb.usdc.

signatureFailed: one key, many causes

signatureFailed is the most common error during integration — and the one most likely to send you down the wrong path, because under the v2 protocol several entirely different failure causes all return this same key:

  • the canonical string doesn't match (line separator is not \n, or the three segments are not in ts, nonce, body order);
  • the signature algorithm is not SHA256withRSA, or the wrong key was used (requests must be signed with the merchant private key);
  • the ANONY-TIMESTAMP / ANONY-NONCE headers are missing or misspelled;
  • the timestamp falls outside the ±5 minute window (usually a server clock without NTP sync);
  • duplicate nonce (a nonce reused under the same merchant ID — typically retry logic that resends the original request as-is).

So when you see signatureFailed, don't fixate on the signing code. Work through the Troubleshooting guide in order: OAEP hash is SHA-1 → canonical string matches byte for byte → key roles are correct → server clock is in sync → retries regenerate ts/nonce and re-sign. Protocol details are in Encryption & Signing Protocol.

Retries must be re-signed

Any automatic retry must generate a fresh timestamp and nonce and re-sign the canonical string before sending. Replaying the previous request verbatim gets rejected by nonce deduplication and reports the same signatureFailed.

Error handling examples

The examples below show the recommended dispatch skeleton: first handle non-JSON responses (Oops), then dispatch on the message key.

php
$res = $client->post('createWithdrawOrder', $params, $token);

if (($res['code'] ?? 0) !== 10000) {
    switch ($res['message'] ?? 'operationFailed') {
        case 'tokenExpired':
            $token = $client->getToken($secret); // refresh the token, then retry this request
            break;
        case 'signatureFailed':
            // work through the troubleshooting guide: canonical string / clock sync / nonce reuse
            break;
        case 'insufficientBalance':
        case 'withdrawDailyLimitExceeded':
            // business failure: log it and escalate to a human / defer — do not auto-retry
            break;
        default:
            // handle the rest per the error code reference
    }
}
python
r = requests.post(c.base_url + "createWithdrawOrder",
                  data=req["body"], headers=req["headers"])
try:
    body = r.json()
except ValueError:
    # non-JSON response: most likely the plain-text "Oops" from an IP whitelist mismatch
    raise RuntimeError(f"non-JSON response: {r.text[:50]!r}")

if body["code"] != 10000:
    key = body.get("message", "operationFailed")
    if key == "tokenExpired":
        pass  # call getToken again, then retry this request
    elif key == "signatureFailed":
        pass  # work through the troubleshooting guide; regenerate ts/nonce and re-sign before retrying
    else:
        pass  # handle the rest per the error code reference
js
const r = await fetch(c.baseUrl + 'createWithdrawOrder',
  { method: 'POST', body: req.body, headers: req.headers });

const text = await r.text();
let body;
try {
  body = JSON.parse(text);
} catch {
  // non-JSON response: most likely the plain-text "Oops" from an IP whitelist mismatch
  throw new Error('non-JSON response: ' + text.slice(0, 50));
}

if (body.code !== 10000) {
  switch (body.message) {
    case 'tokenExpired':
      // call getToken again, then retry this request
      break;
    case 'signatureFailed':
      // work through the troubleshooting guide; regenerate ts/nonce and re-sign before retrying
      break;
    default:
      // handle the rest per the error code reference
  }
}