PHP SDK
The PHP SDK is a self-contained, single-file class, AnonyV2Client.php (see the SDK overview for how to get it). Just require it and go — no Composer dependencies. It encapsulates all of the v2 protocol's security logic:
- Encryption: RSA-OAEP (MGF1 = SHA-1), plaintext chunked at 214 bytes, ciphertext chunked at 256 bytes;
- Signing: SHA256withRSA over the canonical string
timestamp + "\n" + nonce + "\n" + <base64 ciphertext>; - Replay protection: automatically generates a second-precision
ANONY-TIMESTAMPand a 16-byte CSPRNGANONY-NONCE, and enforces a ±5-minute time window on inbound responses.
For protocol details see Encryption & Signing Protocol — this page focuses on usage.
Requirements
| Dependency | Purpose |
|---|---|
openssl extension | Encrypt / decrypt / sign / verify (the only dependency at the crypto layer) |
curl extension | Only used by the built-in getToken() / post() for HTTP; not needed if you bring your own HTTP client (Guzzle etc.) via manual mode |
Keys are in base64(PEM) format — the entire standard PEM text Base64-encoded once more; the SDK decodes and loads them internally. See Credentials for how to obtain and safeguard your keys.
Initialization
Constructor (matches the source):
php
public function __construct(
$appId,
$serverPublicKeyB64,
$merchantPrivateKeyB64,
$baseUrl = 'https://api.anonypay.io/api/merchant/'
)| Parameter | Description |
|---|---|
$appId | Merchant ID, i.e. ANONY-APP-ID |
$serverPublicKeyB64 | Platform public key public_t, base64(PEM); encrypts outbound requests and verifies inbound signatures |
$merchantPrivateKeyB64 | Merchant private key private_u, base64(PEM); signs outbound requests and decrypts inbound responses |
$baseUrl | API base URL, defaults to https://api.anonypay.io/api/merchant/ |
php
require 'AnonyV2Client.php';
$client = new AnonyV2Client(
getenv('ANONY_APP_ID'),
getenv('ANONY_SERVER_PUB_B64'), // platform public key public_t
getenv('ANONY_MERCHANT_PRIV_B64') // merchant private key private_u
);Keep the private key on your server only — never ship it to the frontend, write it to logs, or hand it to third parties.
Getting a token: getToken()
php
public function getToken($secret): stringThe getToken request body is neither encrypted nor signed; it authenticates solely with Authorization: auth + md5(APP-ID + "&" + secret) (the separator is & — this is the single easiest thing to get wrong, and the SDK wraps it in authHeader()). The response, however, is still an encrypted and signed envelope, not a plaintext token — getToken() verifies the signature, decrypts, extracts the token field and returns it; on failure it throws a RuntimeException.
php
$token = $client->getToken(getenv('ANONY_SECRET'));The token is reusable for its lifetime, so cache it; when a business endpoint returns tokenExpired, simply fetch a new one. See the getToken endpoint for details.
One-shot business calls: post()
php
public function post($path, array $data, $token): arraypost() runs the whole pipeline for you: OAEP-encrypt the business JSON → generate timestamp / nonce → sign the canonical string → POST with the five headers ANONY-APP-ID / TOKEN / TIMESTAMP / NONCE / SIGN → (on success) verify and decrypt the response. You just pass business parameters and consume business data:
php
$res = $client->post('createAddress', [
'userOrder' => 'A1001',
'addressType' => 'trx',
], $token);
if (($res['code'] ?? null) == 10000) {
$address = $res['data']['address']; // data has already been verified + decrypted into an array
$apiOrder = $res['data']['apiOrder'];
} else {
// failure: message holds the reason, data is empty
error_log('createAddress failed: ' . ($res['message'] ?? ''));
}- The return value is
['code', 'data', 'message']: whencode == 10000,datais already decrypted into an array; otherwise the outer JSON is returned as-is. - Signature-verification or decryption failures throw a
RuntimeException— try/catch and treat them as exceptions; do not retry them as business failures. $pathis written relative to the base URL, e.g.createAddress,createWithdrawOrder,merchantInfo,getDeposits,checkWithdrawOrder,v2/selfcheck; see the API reference for parameters and response fields.
TRON-chain USDC deposits are disabled
TRON-USDC sent to a trx-type deposit address will not be credited and will not trigger a callback. Do not direct your users to deposit TRON-USDC to platform addresses. The supported currencies are exactly the enum in the API reference (13 withdrawal currencies).
Before going live, exercise the full round trip with v2/selfcheck (read-only, safe to call repeatedly; the request payload is echoed back verbatim):
php
$res = $client->post('v2/selfcheck', ['hello' => 'anony'], $token);
// $res['data'] looks like { "pong": true, "apiVersion": 2, "serverTime": ..., "echo": {"hello":"anony"} }A passing self-check proves both directions are ready: outbound (your encryption + signature are accepted by the server) and inbound (you can verify + decrypt the response). See Go-live self-check.
Manual mode: buildRequest + verifyResponse
If you prefer your own HTTP layer — Guzzle, Laravel HTTP Client, etc. — these two methods split the flow into "build the request" and "verify the response".
buildRequest
php
public function buildRequest(array $data, $token): arrayReturn structure:
php
[
'body' => '<base64 ciphertext of the business JSON>', // send as the HTTP body, verbatim
'headers' => [
'ANONY-APP-ID' => '...',
'ANONY-TOKEN' => '...',
'ANONY-TIMESTAMP' => '...',
'ANONY-NONCE' => '...',
'ANONY-SIGN' => '...',
],
]verifyResponse
php
public function verifyResponse(
$encryptedBody, // base64 ciphertext (the response's data field, or the entire callback body)
$timestamp, // ANONY-TIMESTAMP header
$nonce, // ANONY-NONCE header
$sign, // ANONY-SIGN header
callable $isFreshNonce = null // nonce dedup callback; optional for synchronous responses, required for callbacks
): arrayExecutes in order: time-window check (±5 minutes) → nonce dedup (when $isFreshNonce is provided) → signature verification with the platform public key → decryption with the merchant private key, returning the business data array. Any failing step throws a RuntimeException.
Guzzle example
php
use GuzzleHttp\Client;
$http = new Client(['base_uri' => 'https://api.anonypay.io/api/merchant/', 'timeout' => 15]);
$req = $client->buildRequest(['userOrder' => 'A1001'], $token);
$r = $http->post('checkWithdrawOrder', [
'body' => $req['body'],
'headers' => $req['headers'],
]);
$body = json_decode((string) $r->getBody(), true); // always parse the outer envelope with a standard JSON library
if (($body['code'] ?? null) == 10000) {
$data = $client->verifyResponse(
$body['data'],
$r->getHeaderLine('ANONY-TIMESTAMP'),
$r->getHeaderLine('ANONY-NONCE'),
$r->getHeaderLine('ANONY-SIGN')
);
}In the outer JSON, / may be escaped as \/; standard JSON libraries un-escape it automatically — do not hand-roll string slicing.
Receiving callbacks: reusing verifyResponse
Platform callbacks use the same canonical signature verification and OAEP decryption as inbound responses, so you can reuse verifyResponse directly. Two differences from synchronous responses:
- The callback body is not a
{code,data}JSON envelope — it is the raw base64 ciphertext. Pass the entire HTTP body in as-is. $isFreshNonceis mandatory. When omitted,verifyResponseskips nonce dedup — fine for synchronous responses, but a callback without it can be replayed. Implement it with atomic storage (e.g. RedisSET key 1 NX EX 900, TTL ≥ 15 minutes): returntruethe first time a nonce is seen,falseon repeats.
php
<?php
// callback.php — platform callback entry point
require 'AnonyV2Client.php';
$client = new AnonyV2Client(
getenv('ANONY_APP_ID'),
getenv('ANONY_SERVER_PUB_B64'),
getenv('ANONY_MERCHANT_PRIV_B64')
);
$encryptedBody = file_get_contents('php://input'); // callback body = base64 ciphertext
$timestamp = $_SERVER['HTTP_ANONY_TIMESTAMP'] ?? '';
$nonce = $_SERVER['HTTP_ANONY_NONCE'] ?? '';
$sign = $_SERVER['HTTP_ANONY_SIGN'] ?? '';
try {
$data = $client->verifyResponse($encryptedBody, $timestamp, $nonce, $sign, 'isFreshNonce');
} catch (\RuntimeException $e) {
http_response_code(400); // time window / duplicate nonce / bad signature: reject, do not process
exit;
}
if ($data['businessType'] === 'deposit') {
// Deposit credited: record it by userOrder (make your own side idempotent too)
// $data: txid, address, addressType, fromAddress, amount, currency, fee, apiOrder, userOrder
} elseif ($data['businessType'] === 'withdraw') {
// Withdrawal status: update the order by orderStatus
// $data: toAddress, amount, currency, fee, apiOrder, userOrder, withdrawType, message, txid, orderStatus
}
echo 'SUCCESS'; // on success, return the agreed plain-text acknowledgmentTwo implementations of the nonce dedup function isFreshNonce:
php
function isFreshNonce($nonce)
{
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// Note: 'nx' must be a positional (array-value) option; 'ex' is a key-value pair
return $redis->set('anony:cb:nonce:' . $nonce, '1', ['nx', 'ex' => 900]) === true;
}php
function isFreshNonce($nonce)
{
$redis = new Predis\Client();
// SET key 1 EX 900 NX: returns OK on success, null if the nonce already exists
return (string) $redis->set('anony:cb:nonce:' . $nonce, '1', 'EX', 900, 'NX') === 'OK';
}Acknowledgment contract (implementation detail)
Deposit callbacks must be answered with the plain text SUCCESS (uppercase) to count as delivered. For withdrawal callbacks, SUCCESS = confirm and proceed, FAIL = reject and refund, and any other response triggers platform retries (backoff 2s → 60min, up to 5 attempts). Withdrawals also have a pending-confirmation callback with businessType = withdrawalPendingConfirm (no txid/orderStatus). See Callbacks for the full set of callback types and acknowledgment semantics.
Low-level utilities (static, callable standalone)
If you want to assemble the flow yourself or port it to another framework, the following static methods are available directly:
| Method | Description |
|---|---|
encrypt($plaintext, $publicKeyB64) | RSA-OAEP(SHA-1) encryption, 214-byte chunks, returns base64 ciphertext |
decrypt($cipherB64, $privateKeyB64) | Decryption; base64-decodes then splits into 256-byte chunks |
sign($canonical, $privateKeyB64) | SHA256withRSA signature, returns base64 |
verify($canonical, $publicKeyB64, $signB64) | Signature verification, returns bool |
canonical($ts, $nonce, $payload) | Builds the canonical string ts . "\n" . nonce . "\n" . payload |
newNonce() | 16 bytes of CSPRNG, returned as fixed-length 32-char hex |
authHeader($appId, $secret) | 'auth ' . md5($appId . '&' . $secret), the Authorization header value for getToken |
Related constants: OAEP_MAX_PLAIN = 214, RSA_BLOCK = 256, TS_WINDOW = 300 (±5 minutes).
Common exceptions and troubleshooting
| Exception message thrown by the SDK | Cause |
|---|---|
timestamp out of window | Inbound timestamp fell outside the ±5-minute window; check your server's NTP sync |
duplicate nonce (replay) | $isFreshNonce reported a duplicate nonce — the callback was replayed (or your dedup key is not scoped per nonce) |
signature verify failed | Verification failed: canonical string mismatch or misconfigured platform public key |
getToken failed: ... | md5 auth failed etc.; message carries the server-side reason |
invalid public key / invalid private key | Key is not in base64(PEM) format, or public and private keys are swapped |
decrypt failed: ... | Wrong private key, or the ciphertext was tampered with |
For business error codes returned by the server (signatureFailed, tokenExpired, the Oops whitelist block, etc.) see Error Codes and the Troubleshooting Guide.