Python SDK
The Python SDK is a single file, anony_v2.py (shipped in the integration package under sdk/python/). Copy it into your project and you're done — there is no SDK package to install. It implements every cryptographic step of the v2 protocol: RSA-OAEP (MGF1 = SHA-1) encryption, SHA256withRSA signing, canonical string construction, and timestamp/nonce replay protection. See the protocol specification for the underlying details.
Unlike the PHP / Node SDKs, the Python SDK does not make HTTP calls for you: it only builds requests (encrypt + sign + headers) and processes responses (verify + decrypt), while the HTTP round trip is handled by whatever library you prefer. The examples below use requests.
Dependencies
cryptography: the SDK's only hard dependency, used for RSA encryption/decryption and signing.requests(or any HTTP library): used to make HTTP calls. Not an SDK dependency — feel free to usehttpx,urllib3, etc.
bash
pip install cryptography requestsbash
poetry add cryptography requestsImports (a typical integration only needs these two names):
python
from anony_v2 import AnonyV2Client, auth_headerConstructing the client takes 3 credentials (see Credentials & Keys for how to obtain them):
python
c = AnonyV2Client(
app_id, # ANONY-APP-ID (merchant ID)
server_pub_b64, # server public key public_t, base64(PEM)
merchant_priv_b64, # merchant private key private_u, base64(PEM)
# base_url defaults to "https://api.anonypay.io/api/merchant/" and can be omitted
)Key format is base64(PEM)
Both keys passed to the constructor are the full PEM text Base64-encoded one more time. The SDK Base64-decodes them back to PEM before loading. Passing a raw PEM string will fail to load.
Module-level functions vs. the AnonyV2Client class: which to use when
anony_v2.py exposes two API layers:
| Layer | Contents | When to use |
|---|---|---|
AnonyV2Client class | build_request / verify_response | Preferred for normal integrations. These two methods cover the entire "send a business request" and "process a response/callback" flow |
| Module-level functions | encrypt / decrypt / sign / verify / canonical / new_nonce / auth_header | auth_header is required for getToken; the rest are cryptographic primitives for custom flows or for comparing intermediate values step by step while debugging (e.g. the canonical string, ciphertext chunks) |
Module-level functions at a glance
| Function | Signature | Description |
|---|---|---|
auth_header | auth_header(app_id, secret) | Returns the Authorization header value for getToken: "auth " + md5(APP-ID + "&" + secret). The separator is & — the single most common hand-rolled mistake — so always use this function |
encrypt | encrypt(plaintext, server_public_key_b64) | RSA-OAEP(SHA-1) encryption; splits the plaintext into 214-byte chunks and returns a base64 string |
decrypt | decrypt(cipher_b64, merchant_private_key_b64) | Base64-decodes the ciphertext, decrypts it in 256-byte blocks, and returns bytes (call .decode("utf-8") yourself) |
sign | sign(canonical_str, merchant_private_key_b64) | SHA256withRSA signature, returns a base64 string |
verify | verify(canonical_str, server_public_key_b64, sign_b64) | SHA256withRSA verification, returns True / False (never raises) |
canonical | canonical(ts, nonce, payload) | Builds the canonical string: ts + "\n" + nonce + "\n" + payload |
new_nonce | new_nonce() | Generates 16 CSPRNG random bytes as a hex string (32 characters) |
Module-level constants: OAEP_MAX_PLAIN = 214 (OAEP plaintext chunk size), RSA_BLOCK = 256 (RSA-2048 ciphertext block), TS_WINDOW = 300 (±5-minute time window).
AnonyV2Client methods at a glance
| Method | Signature | Description |
|---|---|---|
build_request | build_request(data, token) | Encrypts and signs a business dict, returns {"body": <ciphertext>, "headers": <the 5 request headers>} |
verify_response | verify_response(encrypted_body, timestamp, nonce, sign_b64, is_fresh_nonce=None) | Checks the time window → deduplicates the nonce (when is_fresh_nonce is provided) → verifies the signature → decrypts; returns the business dict, and raises ValueError if any step fails |
Step 1: get a token (getToken)
The getToken request is neither encrypted nor signed — it only uses the md5 auth from auth_header. The response, however, is the same encrypted + signed envelope as every other endpoint, not a plaintext token: you must run it through verify_response and read the token field from the decrypted result. The full flow:
python
from anony_v2 import AnonyV2Client, auth_header
import requests
c = AnonyV2Client(app_id, server_pub_b64, merchant_priv_b64)
# Get a token first: the request is unencrypted (md5 auth only);
# the response still needs signature verification + decryption
tr = requests.post(c.base_url + "getToken",
headers={"ANONY-APP-ID": str(app_id), "Authorization": auth_header(app_id, secret)})
tb = tr.json()
token = c.verify_response(tb["data"], tr.headers["ANONY-TIMESTAMP"],
tr.headers["ANONY-NONCE"], tr.headers["ANONY-SIGN"])["token"]Three points to note:
auth_header(app_id, secret)produces theAuthorizationheader, in the formauth+md5(APP-ID + "&" + secret).- Parse the outer response with a standard JSON library (
tr.json()); thedatafield is base64 ciphertext. verify_responsetakes the ciphertext plus theANONY-TIMESTAMP/ANONY-NONCE/ANONY-SIGNresponse headers, verifies and decrypts, and returns a dict from which you readtoken.
Token lifetime (implementation detail)
Tokens use an 8-hour sliding expiry with a 24-hour absolute cap. When you receive a tokenExpired error, simply call getToken again — build automatic refresh into your integration. See getToken for endpoint details.
Step 2: send business requests (build_request)
build_request(data, token) handles encryption + timestamp/nonce + signing, and returns a body and headers you can hand straight to your HTTP library:
python
req = c.build_request({"userOrder": "A1001", "addressType": "trx"}, token)
r = requests.post(c.base_url + "createAddress", 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"])Internally, build_request performs, in order: json.dumps(data, ensure_ascii=False) serialization → OAEP encryption to base64 ciphertext → generation of a second-precision timestamp and a 16-byte nonce → a SHA256withRSA signature over the canonical string ts\nnonce\nciphertext. The returned headers include all 5 headers: ANONY-APP-ID / ANONY-TOKEN / ANONY-TIMESTAMP / ANONY-NONCE / ANONY-SIGN.
Note that requests.post uses data=req["body"] to send the raw ciphertext string (do not use json= — the body is not JSON).
A response with code != 10000 means failure: message holds the reason and data is empty, so just read body["message"] — no decryption needed. See the error codes reference.
TRON USDC deposits are closed
USDC sent to a platform TRON address will not be credited and will not trigger a callback — the funds cannot arrive automatically. Do not let your users deposit TRON-USDC to addresses created with addressType: "trx"; for TRON deposits use USDT or TRX. The withdrawal currency enum likewise does not include TRON-USDC.
For each endpoint's parameters and response fields, refer to the API docs: createAddress, createWithdrawOrder, merchantInfo, getDeposits, checkWithdrawOrder. Currency / address type enums are listed in the common API notes.
Step 3: process responses and callbacks (verify_response)
verify_response(encrypted_body, timestamp, nonce, sign_b64, is_fresh_nonce=None) is the single entry point for the downstream direction — used for both synchronous responses and asynchronous callbacks. It runs a fixed sequence internally:
- Checks that
timestampis numeric and within the ±300-second window; otherwise raisesValueError("timestamp out of window"); - If
is_fresh_noncewas provided, callsis_fresh_nonce(nonce); a falsy return raisesValueError("duplicate nonce (replay)"); - Verifies the signature over the canonical string with the platform public key; on failure raises
ValueError("signature verify failed"); - OAEP-decrypts with the merchant private key, then
json.loadsand returns the business dict.
is_fresh_nonce: optional for sync responses, mandatory for callbacks
is_fresh_nonce is a callback function: given a nonce, it returns True the first time it sees it and False on repeats. When omitted (the default None), verify_response skips nonce deduplication entirely:
- Processing a synchronous response (the reply to a request you just sent): safe to omit.
- Processing a platform callback: must be provided, and must be backed by an atomic store (e.g. Redis
SET key 1 NX EX 900, with an expiry of at least 15 minutes). Without it, an attacker who captures one legitimate callback can replay it repeatedly and have you credit the same deposit over and over.
Callback receiver examples
The callback HTTP body is the base64 ciphertext itself, with ANONY-TIMESTAMP / ANONY-NONCE / ANONY-SIGN in the request headers:
python
import redis
from flask import Flask, request
from anony_v2 import AnonyV2Client
app = Flask(__name__)
rds = redis.Redis()
c = AnonyV2Client(app_id, server_pub_b64, merchant_priv_b64)
def is_fresh_nonce(nonce):
# Atomic dedup: first write succeeds and returns True,
# repeats within 15 minutes return False
return bool(rds.set("anony:cb:nonce:" + nonce, 1, nx=True, ex=900))
@app.post("/anony/callback")
def anony_callback():
body = request.get_data(as_text=True) # the callback body is the ciphertext; do not parse it as JSON
try:
data = c.verify_response(
body,
request.headers["ANONY-TIMESTAMP"],
request.headers["ANONY-NONCE"],
request.headers["ANONY-SIGN"],
is_fresh_nonce=is_fresh_nonce, # mandatory for callbacks, or replays get through
)
except (ValueError, KeyError):
return "", 400 # verification failed: return a non-SUCCESS reply so the platform retries; never return FAIL
if data["businessType"] == "deposit":
handle_deposit(data) # credit idempotently, keyed on userOrder/apiOrder
elif data["businessType"] == "withdraw":
handle_withdraw(data) # update the order based on orderStatus
return "SUCCESS"python
import redis
from fastapi import FastAPI, Request
from fastapi.responses import PlainTextResponse
from anony_v2 import AnonyV2Client
app = FastAPI()
rds = redis.Redis()
c = AnonyV2Client(app_id, server_pub_b64, merchant_priv_b64)
def is_fresh_nonce(nonce):
# Atomic dedup: first write succeeds and returns True,
# repeats within 15 minutes return False
return bool(rds.set("anony:cb:nonce:" + nonce, 1, nx=True, ex=900))
@app.post("/anony/callback")
async def anony_callback(req: Request):
body = (await req.body()).decode("utf-8") # the callback body is the ciphertext; do not parse it as JSON
try:
data = c.verify_response(
body,
req.headers["ANONY-TIMESTAMP"],
req.headers["ANONY-NONCE"],
req.headers["ANONY-SIGN"],
is_fresh_nonce=is_fresh_nonce, # mandatory for callbacks, or replays get through
)
except (ValueError, KeyError):
# verification failed: return a non-SUCCESS reply so the platform retries; never return FAIL
return PlainTextResponse("", status_code=400)
if data["businessType"] == "deposit":
handle_deposit(data) # credit idempotently, keyed on userOrder/apiOrder
elif data["businessType"] == "withdraw":
handle_withdraw(data) # update the order based on orderStatus
return PlainTextResponse("SUCCESS")Callback reply semantics (implementation detail)
The platform treats a plain-text SUCCESS (uppercase) reply as callback success. For withdrawal callbacks, replying FAIL means you reject that withdrawal (the platform will process a refund); any other reply triggers retries with backoff (intervals of 2s / 30s / 2min / 5min / 15min / 60min, stopping after 5 failures). Consequently, never return FAIL when signature verification or decryption fails — return a non-SUCCESS reply such as HTTP 400 and let the platform retry.
For the business fields carried in callbacks (deposit credited / withdrawal status), see the callbacks guide.
Before going live: validate the full pipeline with selfcheck
Before enabling the integration for real, use the read-only v2/selfcheck endpoint to exercise the entire pipeline — encryption, signing, replay protection, verification, decryption. It can be called as many times as you like and never creates an order:
python
req = c.build_request({"hello": "anony"}, token)
r = requests.post(c.base_url + "v2/selfcheck", data=req["body"], headers=req["headers"])
body = r.json()
data = c.verify_response(body["data"], r.headers["ANONY-TIMESTAMP"],
r.headers["ANONY-NONCE"], r.headers["ANONY-SIGN"])
assert data["pong"] is True and data["echo"] == {"hello": "anony"}Getting echo back means both directions are ready: upstream (your encryption + signature accepted by the server) and downstream (the server's response verified + decrypted by you). See selfcheck for details. Once the self-check passes, notify the platform to complete activation.
Common issues
- Decryption fails / garbled output: the OAEP hash must be SHA-1. The SDK hardcodes
padding.OAEP(mgf=MGF1(SHA1()), algorithm=SHA1(), label=None); if you bypass the SDK and roll your own, this is the number-one interoperability pitfall. signatureFailed: the three canonical string segments must be joined with\n(LF), in the orderts, nonce, body; also make sure your server clock is NTP-synced (a timestamp outside the ±5-minute window produces this same error).- Plain-text
Oopsresponse: your source IP is not on the whitelist.
For more troubleshooting see Troubleshooting and error codes. For SDKs in other languages see the SDK overview.