Skip to content

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 use httpx, urllib3, etc.
bash
pip install cryptography requests
bash
poetry add cryptography requests

Imports (a typical integration only needs these two names):

python
from anony_v2 import AnonyV2Client, auth_header

Constructing 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:

LayerContentsWhen to use
AnonyV2Client classbuild_request / verify_responsePreferred for normal integrations. These two methods cover the entire "send a business request" and "process a response/callback" flow
Module-level functionsencrypt / decrypt / sign / verify / canonical / new_nonce / auth_headerauth_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

FunctionSignatureDescription
auth_headerauth_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
encryptencrypt(plaintext, server_public_key_b64)RSA-OAEP(SHA-1) encryption; splits the plaintext into 214-byte chunks and returns a base64 string
decryptdecrypt(cipher_b64, merchant_private_key_b64)Base64-decodes the ciphertext, decrypts it in 256-byte blocks, and returns bytes (call .decode("utf-8") yourself)
signsign(canonical_str, merchant_private_key_b64)SHA256withRSA signature, returns a base64 string
verifyverify(canonical_str, server_public_key_b64, sign_b64)SHA256withRSA verification, returns True / False (never raises)
canonicalcanonical(ts, nonce, payload)Builds the canonical string: ts + "\n" + nonce + "\n" + payload
new_noncenew_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

MethodSignatureDescription
build_requestbuild_request(data, token)Encrypts and signs a business dict, returns {"body": <ciphertext>, "headers": <the 5 request headers>}
verify_responseverify_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:

  1. auth_header(app_id, secret) produces the Authorization header, in the form auth + md5(APP-ID + "&" + secret).
  2. Parse the outer response with a standard JSON library (tr.json()); the data field is base64 ciphertext.
  3. verify_response takes the ciphertext plus the ANONY-TIMESTAMP / ANONY-NONCE / ANONY-SIGN response headers, verifies and decrypts, and returns a dict from which you read token.

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:

  1. Checks that timestamp is numeric and within the ±300-second window; otherwise raises ValueError("timestamp out of window");
  2. If is_fresh_nonce was provided, calls is_fresh_nonce(nonce); a falsy return raises ValueError("duplicate nonce (replay)");
  3. Verifies the signature over the canonical string with the platform public key; on failure raises ValueError("signature verify failed");
  4. OAEP-decrypts with the merchant private key, then json.loads and 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 order ts, nonce, body; also make sure your server clock is NTP-synced (a timestamp outside the ±5-minute window produces this same error).
  • Plain-text Oops response: 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.