Skip to content

Self-Check & Troubleshooting

When your integration isn't working, walk through this page in order. The vast majority of problems come down to three things: the OAEP hash isn't SHA-1, the canonical string doesn't match byte for byte, or the server clock is off.

Run selfcheck before anything else

v2/selfcheck is a read-only health-check endpoint — it creates no orders and changes no state, so you can call it as many times as you like. A single successful selfcheck proves both directions at once: upstream (the server accepts your encryption + signature) and downstream (you can verify and decrypt the server's response). It should be the first step in any investigation, and it is the fastest way to narrow down where the problem is.

1. Quick triage table

SymptomLikely causes
signatureFailedCanonical string mismatch (line endings, field order), signature algorithm isn't SHA256, wrong key, missing/wrong ts/nonce, timestamp outside the window, duplicate nonce
Decryption fails / garbage outputOAEP hash isn't SHA-1, chunk size isn't 214/256, wrong key
tokenExpiredToken has expired — call getToken again
OopsSource IP is not on the whitelist
Request rejected (body too large)Request body exceeds the platform limit (128KB by default)

General troubleshooting order:

  1. Confirm OAEP uses SHA-1 (see Section 3);
  2. Confirm the canonical string is joined with \n (LF) and the field order is ts, nonce, body;
  3. Confirm the key roles: upstream, encrypt with the platform public key and sign with the merchant private key; downstream, verify with the platform public key and decrypt with the merchant private key;
  4. Confirm your server clock is synchronized (NTP).

For the full list of error codes, see Error Codes.

2. Digging into signatureFailed

signatureFailed is the most common error — and the most commonly misread one. It does not only mean "the signature was computed wrong".

One error code, four distinct triggers

The server reports all four of the following as signatureFailed:

#TriggerDetails
1Signature verification failedCanonical string mismatch, signature algorithm isn't SHA256withRSA, wrong key
2Timestamp outside the windowANONY-TIMESTAMP deviates from server time by more than ±5 minutes (±300 seconds)
3Duplicate nonceThe same nonce reappears for the same merchant ID within 900 seconds (server-side Redis SET NX EX 900 dedup)
4Missing required headerAny of ANONY-TIMESTAMP / ANONY-NONCE / ANONY-SIGN is missing or empty

So when your signing code "looks correct", don't stare at the signature alone — clock drift and nonce reuse produce the exact same error.

Work through the checks below, cheapest first:

Step 1: Fix the clock (NTP)

The timestamp window is ±5 minutes, so a server clock that has drifted a few minutes will fail intermittently (working on and off around an NTP restart is the classic symptom).

  • Confirm NTP sync is enabled on your server (timedatectl / chronyc tracking).
  • Use selfcheck to compare clocks directly: its decrypted response contains a serverTime field. Compare it against your local time() — if the difference exceeds a few dozen seconds, fix the clock before anything else.

Step 2: Every retry needs a fresh nonce

The server deduplicates on (merchant ID, nonce) and rejects any repeat. In practice this means:

  • Every HTTP request — including timeout retries and failure retries — must generate a new nonce, take a fresh timestamp, and re-sign.
  • Caching the whole "ciphertext + headers" and resending it verbatim is guaranteed to hit the nonce dedup and return signatureFailed.
  • The nonce must be a CSPRNG random string of at least 16 bytes (hex or base64 both work; fixed-length hex is recommended). Millisecond timestamps or auto-increment counters do not qualify as nonces and collide easily.

Step 3: Verify the canonical string byte by byte

The input to signing/verification must be exactly this (this is the spec, not an example):

ANONY-TIMESTAMP + "\n" + ANONY-NONCE + "\n" + <body>

Check each point:

  • The three parts are joined with \n (LF, 0x0A) — not \r\n (CRLF), not spaces, and with no trailing newline. Windows environments and some template engines are prone to sneaking in a \r.
  • The field order is fixed: ts, nonce, body — it cannot be rearranged.
  • <body> is the ciphertext, not the plaintext: in upstream requests, <body> is the OAEP-encrypted Base64 ciphertext (i.e. the actual HTTP body you send); in downstream responses/callbacks, <body> is the Base64 ciphertext from the response body (the data field of the outer JSON / the raw callback body). Signing the plaintext business JSON is one of the most frequent mistakes.
  • The signature algorithm is SHA256withRSA (PKCS#1 v1.5 signature padding) — not MD5, not PSS.
  • Sign with the merchant private key; verify downstream signatures with the platform public key — don't swap the two.

Dump your assembled canonical string to a file and inspect it in hex with xxd: the separators must be 0a with no 0d anywhere (see the scripts in Section 4).

3. OAEP must use SHA-1 (all five languages)

Encryption is RSA-OAEP with MGF1 = SHA-1. Many languages default to OAEP-SHA256; if the two sides disagree, decryption simply fails (showing up as "decryption failed / garbage output"). The plaintext is encrypted in 214-byte chunks and the ciphertexts are concatenated then Base64-encoded as a whole; for decryption, Base64-decode first and split into 256-byte chunks.

Explicit settings per language (always specify explicitly — never rely on defaults):

LanguageSetting
PHPopenssl_public_encrypt($d,$o,$k, OPENSSL_PKCS1_OAEP_PADDING) (SHA-1 by default)
Pythonpadding.OAEP(mgf=MGF1(SHA1()), algorithm=SHA1(), label=None)
Node{ padding: RSA_PKCS1_OAEP_PADDING, oaepHash: 'sha1' }
Gorsa.EncryptOAEP(sha1.New(), rand.Reader, pub, msg, nil)
JavaCipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding")
php
// PHP's OPENSSL_PKCS1_OAEP_PADDING defaults to SHA-1; no extra parameters needed
openssl_public_encrypt($d, $o, $k, OPENSSL_PKCS1_OAEP_PADDING);
python
# cryptography library: both OAEP and MGF1 must explicitly specify SHA1
padding.OAEP(mgf=MGF1(SHA1()), algorithm=SHA1(), label=None)
js
// oaepHash must be set to 'sha1' explicitly — it's Node's default, but declare it anyway
{ padding: RSA_PKCS1_OAEP_PADDING, oaepHash: 'sha1' }
go
// The first argument determines the OAEP hash — it must be sha1.New()
rsa.EncryptOAEP(sha1.New(), rand.Reader, pub, msg, nil)
java
// Hard-code SHA-1 + MGF1 in the transformation string
Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");

Using the official SDKs (PHP / Python / Node / Go / Java) sidesteps this pitfall entirely.

4. Manual debugging: reproducing signatures and decryption with openssl

When you're not sure whether to trust the SDK, you can reproduce the entire chain independently on the openssl command line, with no application code involved. The scripts below run as-is on any Linux box with openssl / jq.

Setup: restore the PEM keys (your credentials are in base64(PEM) format — strip one layer of Base64 first):

bash
# private_u = merchant private key (PKCS#8), public_t = platform public key (SPKI)
echo "$MERCHANT_PRIVATE_U_B64" | base64 -d > private_u.pem
echo "$SERVER_PUBLIC_T_B64"    | base64 -d > public_t.pem

# If these parse cleanly, the keys were restored correctly
openssl pkey -in private_u.pem -noout -text | head -2
openssl pkey -pubin -in public_t.pem -noout -text | head -2

Reproduce the upstream signature (SHA256withRSA over the canonical string):

bash
TS=$(date +%s)                                # Unix timestamp in seconds
NONCE=$(openssl rand -hex 16)                 # CSPRNG, at least 16 bytes
BODY='<the actual HTTP body you send, i.e. the Base64 ciphertext, verbatim on one line>'

# Assemble the canonical string with printf: three parts joined by LF, no trailing newline.
# Never use echo — it appends a trailing newline and guarantees a bad signature.
printf '%s\n%s\n%s' "$TS" "$NONCE" "$BODY" > canonical.txt

# Hex self-check: separators must be 0a (LF); 0d (CR) must not appear
xxd canonical.txt | head -3

# Sign with the merchant private key; the Base64 output is the value of the ANONY-SIGN header
openssl dgst -sha256 -sign private_u.pem canonical.txt | base64 -w0

Compare this signature against the ANONY-SIGN your SDK produces: given the same ts/nonce/body, the two must be identical. If they differ, your canonical assembly or signature algorithm is wrong.

Verify a downstream signature (same logic for responses and callbacks):

bash
# Save the full response: curl -sD headers.txt -o response.json ...
RESP_TS=$(grep -i '^ANONY-TIMESTAMP' headers.txt | tr -d '\r' | awk '{print $2}')
RESP_NONCE=$(grep -i '^ANONY-NONCE' headers.txt | tr -d '\r' | awk '{print $2}')
grep -i '^ANONY-SIGN' headers.txt | tr -d '\r' | awk '{print $2}' | base64 -d > resp_sign.bin

# Extract data with a proper JSON tool (jq unescapes \/ automatically — never slice the string by hand)
DATA=$(jq -r '.data' response.json)

printf '%s\n%s\n%s' "$RESP_TS" "$RESP_NONCE" "$DATA" > resp_canonical.txt

# Verify with the platform public key; "Verified OK" means it passed
openssl dgst -sha256 -verify public_t.pem -signature resp_sign.bin resp_canonical.txt

Decrypt the downstream data (merchant private key, OAEP SHA-1, 256-byte chunks):

bash
printf '%s' "$DATA" | base64 -d > cipher.bin
split -b 256 cipher.bin seg_
for f in seg_*; do
  openssl pkeyutl -decrypt -inkey private_u.pem -in "$f" \
    -pkeyopt rsa_padding_mode:oaep \
    -pkeyopt rsa_oaep_md:sha1 -pkeyopt rsa_mgf1_md:sha1
done > plain.json
cat plain.json   # Should be the plaintext business JSON

If openssl can decrypt it but your code cannot, the problem is in your code's OAEP parameters (go back to Section 3). If openssl can't decrypt it either, you're most likely using the wrong key (downstream must be decrypted with the merchant private key private_u, not the platform public key).

5. Deposit not credited / callback never arrived

First run through the usual items on the callbacks side: is callback_url configured and reachable from the public internet, does your endpoint return the expected success response, and are your time-window + nonce dedup checks correct (a buggy dedup will reject legitimate callbacks as replays)? Then note these special cases:

USDC deposits on TRON are closed

The TRON-USDC deposit channel is closed: USDC sent to a TRON deposit address is neither credited nor called back. Do not direct users to deposit USDC on the TRON chain. Live deposit address types (addressType) are trx / solana / eth_bnb / ton; btc is reserved while Bitcoin integration is still coming soon. For withdrawals, the authoritative list is the 13 currency enums in the API reference. If you need USDC, use eth.usdc / solana.usdc / bnb.usdc.

Implementation detail: small deposits don't trigger callbacks

A deposit below the currency's deposit_min (the minimum deposit amount, queryable via the public getCurrencies endpoint) will not trigger a callback. When investigating "funds arrived but no callback", check the amount against this threshold first.

Implementation detail: callback timeout and retries

When the platform calls your callback_url, the timeout is 3 seconds; on failure it retries with a 2s → 30s → 2min → 5min → 15min → 60min backoff, stopping after at most 5 retries. Make sure your callback handler returns quickly (persist first, process asynchronously) — otherwise it will be treated as a timeout and enter the retry cycle. Deposit callbacks must return the plain-text string SUCCESS (uppercase) to count as delivered.

6. Still stuck? Contact the platform

If you've been through everything above and still can't pin it down, contact @anonypay on Telegram and include all of the following (each missing item slows down the diagnosis):

  • APP-ID (merchant ID);
  • The exact time the problem occurred (to the second, with timezone) so the platform can search its logs;
  • The complete request headers: paste ANONY-APP-ID / ANONY-TIMESTAMP / ANONY-NONCE / ANONY-SIGN verbatim, but ANONY-TOKEN must be redacted (e.g. keep only the first 4 characters + ****);
  • The first few characters of the request body ciphertext, and the code / message you received in the response.

Never send your keys

Under no circumstances should you send your merchant private key private_u, your secret, or a complete token to anyone — including people claiming to be platform staff. The platform never needs your private key to investigate an issue.

  • Go-live selfcheck — where every investigation starts and ends
  • Protocol details — the full spec for the canonical string, OAEP, and replay protection
  • Error Codes — every error code and what it means
  • Callbacks — callback signature verification, dedup, and response conventions
  • FAQ