Error Codes
This page lists every business error code returned by the Merchant API. Start with the core convention:
- Success:
code == 10000,datacontains the encrypted payload (verify the signature, then decrypt), andmessageissuccess. - Failure:
code == 9999(a single code shared by all business errors),messageis a machine-readable English error key, anddatais 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
- 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 isOops, first verify that the egress IP of the requesting server has been registered on the whitelist. - 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 key | Meaning | Recommended action |
|---|---|---|
merchantNotExist | Merchant does not exist | Check that the ANONY-APP-ID header carries the merchant ID assigned by the platform, and that the merchant account is active. See Credentials |
authFailed | Authentication failed | Only returned by getToken: verify that the Authorization header is auth + md5(APP-ID + "&" + secret) — note the & separator and the space after auth. See getToken |
tokenExpired | Token has expired | Call getToken again for a fresh token, then retry the request. We recommend wrapping this in your client as "catch tokenExpired → auto-refresh → retry once" |
signatureFailed | Signature verification failed | One key, many causes — v2 timestamp/nonce problems also report this key. See the dedicated section below and the Troubleshooting guide |
publicKeyNotExist | Merchant public key not configured | The platform has not yet registered your merchant public key (public_u). Contact the platform to complete key setup before testing |
paramError | Invalid parameters | Check required fields, field types, and spelling; send amount fields as strings. Note: an oversized request body (>128KB) also triggers this error |
invalidCallbackUrl | Invalid callback URL | callbackUrl must be a well-formed, publicly reachable URL; do not pass internal or loopback addresses. See Callbacks |
addressTypeError | Invalid address type | addressType must be one of the supported live values: trx / solana / eth_bnb / ton. btc is reserved until Bitcoin deposits are opened. See createAddress |
createAddressFailed | Address creation failed | The 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 |
withdrawAddressError | Withdrawal address not accepted | The withdrawal target address is not accepted (e.g. one of the platform's own deposit addresses). Use a genuine external receiving address instead |
withdrawTypeError | Invalid withdrawal type | withdrawType must be either normal or financialConfirmation. See createWithdrawOrder |
auditorTelegramNotBound | Auditor Telegram not bound | Withdrawals of type financialConfirmation require the merchant to bind a finance auditor's Telegram first; complete the binding in the merchant dashboard and retry |
merchantWithdrawDisabled | Withdrawals disabled for this merchant | Withdrawals are switched off for this merchant account. Contact the platform to find out why and to re-enable them |
onlySupportFinancialConfirmation | Only financial-confirmation withdrawals allowed | Your merchant account is configured to enforce financial confirmation: set withdrawType to financialConfirmation and resubmit |
withdrawTypeNotAllowed | Withdrawal type not allowed | The requested withdrawType does not match your merchant account's withdrawal policy. Check the merchant configuration or ask the platform to adjust it |
currencyNotSupported | Currency not supported | currency is not among the 13 supported withdrawal currencies (e.g. trx.usdt, eth.usdc). Check spelling and case; see the currency enum |
currencyNotExist | Currency does not exist | The platform does not recognize the currency symbol you passed. Treat the symbol values returned by the public getCurrencies endpoint as authoritative |
invalidAmount | Invalid amount | amount must be a valid positive number; send it as a string to avoid precision issues |
withdrawMinAmount | Minimum withdrawal amount is :amount :symbol | The placeholders are replaced with the actual floor for that currency. Query each currency's withdraw_min via getCurrencies and validate before submitting |
invalidAddress | Invalid address | The withdrawal target address failed validation. Pre-validate the address/currency pair with the public isAddress endpoint before submitting |
insufficientBalance | Insufficient balance | The 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 |
withdrawDailyLimitExceeded | Daily withdrawal limit exceeded | The 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 |
limitError | Pagination limit out of range | limit on getDeposits must be between 1 and 100 (default 10). See getDeposits |
withdrawOrderNotExist | Withdrawal order not found | checkWithdrawOrder found no order for that userOrder. Check that the order number is correct and belongs to the current merchant account. See checkWithdrawOrder |
operationFailed | Operation failed | Generic 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 ints, nonce, bodyorder); - 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-NONCEheaders 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 referencejs
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
}
}Related pages
- Troubleshooting guide: step-by-step diagnosis of
signatureFailed, decryption failures, and more. - Encryption & Signing Protocol: the full spec for the canonical string, time window, and nonce deduplication.
- Go-live selfcheck: run the full flow with real keys before going live to catch protocol-level errors early.
- FAQ