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
| Item | Requirement |
|---|---|
| JDK | JDK 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 dependencies | None. The SDK itself is dependency-free |
| JSON library | Your choice (examples on this page use Jackson; Gson etc. work just as well) |
| HTTP client | Your 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
| Constant | Value | Meaning |
|---|---|---|
OAEP_MAX_PLAIN | 214 | Maximum plaintext bytes per OAEP-encrypted chunk |
RSA_BLOCK | 256 | Ciphertext chunk size in bytes when decrypting |
TS_WINDOW | 300 | Allowed timestamp skew in seconds (i.e. ±5 minutes) |
Static methods
| Method | Description |
|---|---|
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):
| Direction | Encrypt/decrypt | Sign/verify |
|---|---|---|
| Outbound request | Encrypt with platform public key public_t | Sign with merchant private key private_u |
| Response/callback | Decrypt with merchant private key private_u | Verify 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:
- Compute the
Authorizationheader withauthHeader(appId, secret)(auth+md5(APP-ID + "&" + secret)— note the space afterauth). - Use any HTTP library to
POST {baseUrl}getTokenwith theANONY-APP-IDandAuthorizationheaders. The request body is neither encrypted nor signed and may be empty. - 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 theANONY-TIMESTAMP / ANONY-NONCE / ANONY-SIGNresponse headers. - Check the time window with
verifyTimestamp→ verify the signature withverify(canonical(ts, nonce, data), platformPublicKey, sign)→ decrypt withdecrypt(data, merchantPrivateKey), then read thetokenfield 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 versionImplementation 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 → encrypt → buildHeaders 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):trxsolanaeth_bnbton;btcis reserved until Bitcoin deposits are opened. - Withdrawal currencies (
currency, 13 in total):trxtrx.usdtetheth.usdteth.usdcsolanasolana.usdtsolana.usdcbnbbnb.usdtbnb.usdctonton.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:
- Read the
ANONY-TIMESTAMP / ANONY-NONCE / ANONY-SIGNrequest headers; the request body is the Base64 ciphertext itself. - 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. RedisSET key 1 NX EX 900, TTL ≥ 15 minutes). Otherwise callbacks can be replayed. - Verify the signature:
verify(canonical(ts, nonce, body), platformPublicKey, sign). - Decrypt with
decrypt(body, merchantPrivateKey)to obtain the business JSON, then dispatch onbusinessType. - 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-Typeisapplication/jsonbut 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
SUCCESSto 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"(notxid/orderStatus): returnSUCCESSto approve,FAILto reject and refund. - The callback timeout is 3 seconds; failures are retried with
2s → 30s → 2min → 5min → 15min → 60minbackoff, 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
| Pitfall | Correct approach |
|---|---|
| Wrong OAEP hash | It must be SHA-1: Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding") (built into the SDK; take care if you roll your own) |
| Canonical string mismatch | The three parts are joined with LF (\n), in the fixed order ts, nonce, body, byte-for-byte identical |
| Key roles swapped | Outbound: 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 JSON | The response body may escape / as \/ — always use a proper JSON library (Jackson/Gson) rather than string splitting |
| Server clock drift | The time window is only ±5 minutes. Keep NTP sync enabled or you will get signatureFailed |
| Treating the getToken response as plaintext | The 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.