Skip to content

Java SDK

The Java SDK is a single file, AnonyV2Client.java, exposing everything as static methods. Copy it straight into your project — it has no third-party dependencies.

Its scope is deliberately narrow: cryptography and signing only — RSA-OAEP encryption/decryption, SHA256withRSA signing/verification, canonical string construction, the anti-replay pieces (nonce and timestamp), and building the outbound request headers. JSON serialization and HTTP transport are up to you — use whichever libraries you prefer (Jackson / Gson, java.net.http.HttpClient / OkHttp, etc.).

For the full protocol specification (encryption, signing, replay protection), see the Protocol Guide. For the 4 credentials you need (APP-ID, secret, platform public key public_t, merchant private key private_u), see Keys & Credentials.

Requirements

ItemRequirement
JDKJDK 8+. The SDK uses only the JDK standard library: javax.crypto.Cipher, java.security.* (KeyFactory / MessageDigest / Signature / SecureRandom), java.util.Base64, etc. java.util.Base64 was introduced in JDK 8
Third-party dependenciesNone. The SDK itself is dependency-free
JSON libraryYour choice (examples on this page use Jackson; Gson etc. work just as well)
HTTP clientYour choice (this page shows both a JDK 11+ java.net.http.HttpClient version and a JDK 8 HttpURLConnection version)

Installation: copy AnonyV2Client.java into your source tree (add your own package declaration as needed). There are no Maven/Gradle coordinates.

Keys are provided in base64(PEM) format: the entire PEM text is Base64-encoded one more time. The SDK decodes this automatically when loading — the private key must be PKCS#8 (BEGIN PRIVATE KEY), the public key SPKI (BEGIN PUBLIC KEY), both RSA-2048.

API Overview

Constants

ConstantValueMeaning
OAEP_MAX_PLAIN214Maximum plaintext bytes per OAEP-encrypted chunk
RSA_BLOCK256Ciphertext chunk size in bytes when decrypting
TS_WINDOW300Allowed timestamp skew in seconds (i.e. ±5 minutes)

Static methods

MethodDescription
PublicKey loadPublic(String pubB64)Load the platform public key (base64(PEM))
PrivateKey loadPrivate(String privB64)Load the merchant private key (base64(PEM))
String encrypt(byte[] plaintext, String serverPublicKeyB64)RSA-OAEP (SHA-1 + MGF1-SHA1) encryption with the platform public key, in 214-byte chunks; returns the Base64 ciphertext (this is the HTTP body)
byte[] decrypt(String cipherB64, String merchantPrivateKeyB64)Decrypt with the merchant private key: Base64-decode, then decrypt in 256-byte chunks and concatenate
String sign(String canonical, String merchantPrivateKeyB64)Sign the canonical string with the merchant private key using SHA256withRSA; returns Base64
boolean verify(String canonical, String serverPublicKeyB64, String signB64)Verify a downstream (response/callback) signature with the platform public key
String canonical(String ts, String nonce, String payload)Build the canonical string: ts + "\n" + nonce + "\n" + payload (LF-joined, byte-exact)
String newNonce()Generate 16 bytes of CSPRNG randomness, returned as a 32-character hex string
String authHeader(String appId, String secret)Compute the Authorization header value for getToken: "auth " + md5(APP-ID + "&" + secret)
boolean verifyTimestamp(String ts)Check that the timestamp is within 300 seconds of local time
Map<String,String> buildHeaders(String appId, String token, String encryptedBody, String merchantPrivateKeyB64)Build the 5 outbound request headers: internally generates a second-precision timestamp and a nonce, signs the canonical string, and returns ANONY-APP-ID / ANONY-TOKEN / ANONY-TIMESTAMP / ANONY-NONCE / ANONY-SIGN

Make absolutely sure the key roles line up (swapping them is guaranteed to fail):

DirectionEncrypt/decryptSign/verify
Outbound requestEncrypt with platform public key public_tSign with merchant private key private_u
Response/callbackDecrypt with merchant private key private_uVerify with platform public key public_t

Getting a token (getToken)

The Java SDK does not make HTTP calls itself, so fetching a token takes four steps:

  1. Compute the Authorization header with authHeader(appId, secret) (auth + md5(APP-ID + "&" + secret) — note the space after auth).
  2. Use any HTTP library to POST {baseUrl}getToken with the ANONY-APP-ID and Authorization headers. The request body is neither encrypted nor signed and may be empty.
  3. The response is not a plaintext token — it is the same encrypted-and-signed envelope as every other endpoint: an outer JSON {code, data, message} plus the ANONY-TIMESTAMP / ANONY-NONCE / ANONY-SIGN response headers.
  4. Check the time window with verifyTimestamp → verify the signature with verify(canonical(ts, nonce, data), platformPublicKey, sign) → decrypt with decrypt(data, merchantPrivateKey), then read the token field from the decrypted JSON.

Field-level details are in the getToken API reference.

java
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

String baseUrl = "https://api.anonypay.io/api/merchant/";
String appId   = "<your merchant ID>";
String secret  = "<your secret>";
String serverPubB64    = "<platform public key public_t (base64(PEM))>";
String merchantPrivB64 = "<merchant private key private_u (base64(PEM))>";

// 1. Compute the Authorization header
String auth = AnonyV2Client.authHeader(appId, secret);

// 2. POST getToken (empty body, no encryption, no signature)
HttpClient http = HttpClient.newHttpClient();
HttpRequest req = HttpRequest.newBuilder()
        .uri(URI.create(baseUrl + "getToken"))
        .header("ANONY-APP-ID", appId)
        .header("Authorization", auth)
        .POST(HttpRequest.BodyPublishers.noBody())
        .build();
HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());

// 3. Parse the outer envelope {code, data, message} with a proper JSON library
ObjectMapper om = new ObjectMapper();
JsonNode envelope = om.readTree(resp.body());
if (envelope.get("code").asInt() != 10000) {
    throw new IllegalStateException("getToken failed: " + envelope.get("message").asText());
}
String data  = envelope.get("data").asText();
String ts    = resp.headers().firstValue("ANONY-TIMESTAMP").orElseThrow();
String nonce = resp.headers().firstValue("ANONY-NONCE").orElseThrow();
String sig   = resp.headers().firstValue("ANONY-SIGN").orElseThrow();

// 4. Check time window -> verify signature (platform public key)
//    -> decrypt (merchant private key) -> extract token
if (!AnonyV2Client.verifyTimestamp(ts)) throw new SecurityException("response timestamp outside window");
if (!AnonyV2Client.verify(AnonyV2Client.canonical(ts, nonce, data), serverPubB64, sig)) {
    throw new SecurityException("response signature verification failed");
}
byte[] plain = AnonyV2Client.decrypt(data, merchantPrivB64);
String token = om.readTree(plain).get("token").asText();
java
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;

String baseUrl = "https://api.anonypay.io/api/merchant/";
String auth = AnonyV2Client.authHeader(appId, secret);

// 2. POST getToken (empty body, no encryption, no signature)
HttpURLConnection conn = (HttpURLConnection) new URL(baseUrl + "getToken").openConnection();
conn.setRequestMethod("POST");
conn.setDoOutput(true);
conn.setRequestProperty("ANONY-APP-ID", appId);
conn.setRequestProperty("Authorization", auth);
conn.getOutputStream().close(); // empty body

String body;
try (InputStream in = conn.getInputStream();
     ByteArrayOutputStream buf = new ByteArrayOutputStream()) {
    byte[] tmp = new byte[4096];
    int n;
    while ((n = in.read(tmp)) > 0) buf.write(tmp, 0, n);
    body = buf.toString("UTF-8");
}
String ts    = conn.getHeaderField("ANONY-TIMESTAMP");
String nonce = conn.getHeaderField("ANONY-NONCE");
String sig   = conn.getHeaderField("ANONY-SIGN");

// Steps 3-4: envelope handling (parse JSON -> verifyTimestamp -> verify
// -> decrypt -> extract token) is identical to the HttpClient version

Implementation detail: token lifetime

Tokens use a sliding 8-hour expiry with an absolute 24-hour cap. When you receive a tokenExpired error, simply call getToken again — there is no need to refresh on a fixed schedule.

Making business requests

Every business endpoint follows the same pattern: business JSON → encryptbuildHeaders for the 5 headers → POST the ciphertext → verify and decrypt the response. Using createAddress as an example:

java
// 1. Serialize the business parameters to JSON bytes (any JSON library; Jackson here)
Map<String, Object> params = new LinkedHashMap<>();
params.put("userOrder", "A1001");
params.put("addressType", "trx");
byte[] plaintext = om.writeValueAsBytes(params);

// 2. OAEP-encrypt with the platform public key -> Base64 ciphertext, used as the HTTP body
String encryptedBody = AnonyV2Client.encrypt(plaintext, serverPubB64);

// 3. Build the 5 request headers (generates ts/nonce internally and signs
//    the canonical string with the merchant private key)
Map<String, String> headers =
        AnonyV2Client.buildHeaders(appId, token, encryptedBody, merchantPrivB64);

// 4. POST the ciphertext
HttpRequest.Builder rb = HttpRequest.newBuilder()
        .uri(URI.create(baseUrl + "createAddress"))
        .POST(HttpRequest.BodyPublishers.ofString(encryptedBody));
headers.forEach(rb::header);
HttpResponse<String> r = http.send(rb.build(), HttpResponse.BodyHandlers.ofString());

// 5. Handle the response envelope: parse -> check time window -> verify -> decrypt
//    (same as steps 3-4 of getToken)
JsonNode env = om.readTree(r.body());
if (env.get("code").asInt() != 10000) {
    throw new IllegalStateException("request failed: " + env.get("message").asText());
}
String data   = env.get("data").asText();
String rts    = r.headers().firstValue("ANONY-TIMESTAMP").orElseThrow();
String rnonce = r.headers().firstValue("ANONY-NONCE").orElseThrow();
String rsig   = r.headers().firstValue("ANONY-SIGN").orElseThrow();

if (!AnonyV2Client.verifyTimestamp(rts)) throw new SecurityException("response timestamp outside window");
if (!AnonyV2Client.verify(AnonyV2Client.canonical(rts, rnonce, data), serverPubB64, rsig)) {
    throw new SecurityException("response signature verification failed");
}
JsonNode result = om.readTree(AnonyV2Client.decrypt(data, merchantPrivB64));
// result is the business payload: { "address": "...", "apiOrder": "...", "addressDatas": [...] }

The remaining endpoints (createWithdrawOrder, merchantInfo, getDeposits, checkWithdrawOrder) only differ in the URL and business parameters — the envelope handling is identical. See the API Reference for parameters and response fields.

Currencies and a TRON-USDC caveat

  • Live address types (addressType): trx solana eth_bnb ton; btc is reserved until Bitcoin deposits are opened.
  • Withdrawal currencies (currency, 13 in total): trx trx.usdt eth eth.usdt eth.usdc solana solana.usdt solana.usdc bnb bnb.usdt bnb.usdc ton ton.usdt.

USDC deposits on TRON are closed

Funds sent as USDC to a TRON deposit address will not be credited and will not trigger a callback. Do not direct your users to deposit USDC on the TRON chain — use TRX or USDT for TRON deposits instead.

Handling callbacks

The platform calls your callback_url when a deposit is credited or a withdrawal changes status. Callbacks use the same canonical-signature and OAEP-decryption logic as responses. The steps:

  1. Read the ANONY-TIMESTAMP / ANONY-NONCE / ANONY-SIGN request headers; the request body is the Base64 ciphertext itself.
  2. Check the time window (±5 minutes) and deduplicate the nonce — the Java SDK ships no dedup store, so you must check for replays yourself with an atomic operation before calling verify (e.g. Redis SET key 1 NX EX 900, TTL ≥ 15 minutes). Otherwise callbacks can be replayed.
  3. Verify the signature: verify(canonical(ts, nonce, body), platformPublicKey, sign).
  4. Decrypt with decrypt(body, merchantPrivateKey) to obtain the business JSON, then dispatch on businessType.
  5. On success, respond as agreed with the platform (e.g. the plain-text success value).

A Spring Boot example:

java
@RestController
public class AnonyCallbackController {

    @Autowired
    private StringRedisTemplate redis;

    private static final ObjectMapper OM = new ObjectMapper();

    @PostMapping("/anony/callback")
    public String callback(@RequestHeader("ANONY-TIMESTAMP") String ts,
                           @RequestHeader("ANONY-NONCE") String nonce,
                           @RequestHeader("ANONY-SIGN") String sig,
                           @RequestBody String cipherBody) throws Exception {
        // 1. Time window of +/- 300 seconds
        if (!AnonyV2Client.verifyTimestamp(ts)) return "FAIL";

        // 2. Atomic nonce dedup (a duplicate means a replay -- reject outright)
        Boolean fresh = redis.opsForValue()
                .setIfAbsent("anony:cb:nonce:" + nonce, "1", Duration.ofSeconds(900));
        if (!Boolean.TRUE.equals(fresh)) return "FAIL";

        // 3. Verify with the platform public key
        //    (canonical string = ts + "\n" + nonce + "\n" + ciphertext body)
        if (!AnonyV2Client.verify(
                AnonyV2Client.canonical(ts, nonce, cipherBody), serverPubB64, sig)) {
            return "FAIL";
        }

        // 4. Decrypt with the merchant private key, dispatch on businessType
        JsonNode data = OM.readTree(AnonyV2Client.decrypt(cipherBody, merchantPrivB64));
        String businessType = data.get("businessType").asText();
        if ("deposit".equals(businessType)) {
            // Deposit credited: txid / address / amount / currency / apiOrder / userOrder ...
            // Recommended: credit idempotently, keyed on apiOrder
        } else if ("withdraw".equals(businessType)) {
            // Withdrawal status: orderStatus / txid / toAddress ...
        }

        // 5. Return the agreed value once processing succeeds
        return "SUCCESS";
    }
}

The full callback payload reference is in Callbacks.

Implementation details: callback body and reply contract

  • The callback HTTP body is the raw ciphertext (no {code,data} outer JSON). Content-Type is application/json but the content is a Base64 ciphertext — do not try to parse the callback body as JSON.
  • The platform judges the outcome by your reply: deposit callbacks must return the plain text SUCCESS to count as delivered; for withdrawal callbacks, SUCCESS = confirm and proceed, FAIL = reject (order refunded), anything else = retry later.
  • Withdrawals also have a pending-confirmation callback with businessType = "withdrawalPendingConfirm" (no txid/orderStatus): return SUCCESS to approve, FAIL to reject and refund.
  • The callback timeout is 3 seconds; failures are retried with 2s → 30s → 2min → 5min → 15min → 60min backoff, stopping after 5 attempts. Keep your handler fast and push heavy work to async processing.

Pre-launch self-check (selfcheck)

Before going live, always hit the read-only probe endpoint POST /api/merchant/v2/selfcheck with your real keys. The request goes through the full encrypt-and-sign flow (any business JSON you send is echoed back verbatim); if you can verify the signature, decrypt, and read back the echo, both directions of the pipeline are proven working. See Self-check.

Common pitfalls

PitfallCorrect approach
Wrong OAEP hashIt must be SHA-1: Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding") (built into the SDK; take care if you roll your own)
Canonical string mismatchThe three parts are joined with LF (\n), in the fixed order ts, nonce, body, byte-for-byte identical
Key roles swappedOutbound: encrypt with the platform public key, sign with the merchant private key. Inbound: verify with the platform public key, decrypt with the merchant private key
Hand-parsing the outer JSONThe response body may escape / as \/ — always use a proper JSON library (Jackson/Gson) rather than string splitting
Server clock driftThe time window is only ±5 minutes. Keep NTP sync enabled or you will get signatureFailed
Treating the getToken response as plaintextThe getToken response is the same encrypted-and-signed envelope — verify and decrypt it before reading token

For more debugging guidance see Troubleshooting; error codes are listed in Error Codes. For SDKs in other languages, see the SDK Overview.