Skip to content

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-TIMESTAMP and a 16-byte CSPRNG ANONY-NONCE, and enforces a ±5-minute time window on inbound responses.

For protocol details see Encryption & Signing Protocol — this page focuses on usage.

Requirements

DependencyPurpose
openssl extensionEncrypt / decrypt / sign / verify (the only dependency at the crypto layer)
curl extensionOnly 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/'
)
ParameterDescription
$appIdMerchant ID, i.e. ANONY-APP-ID
$serverPublicKeyB64Platform public key public_t, base64(PEM); encrypts outbound requests and verifies inbound signatures
$merchantPrivateKeyB64Merchant private key private_u, base64(PEM); signs outbound requests and decrypts inbound responses
$baseUrlAPI 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): string

The 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 tokengetToken() 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): array

post() 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']: when code == 10000, data is 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.
  • $path is 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): array

Return 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
): array

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

  1. The callback body is not a {code,data} JSON envelope — it is the raw base64 ciphertext. Pass the entire HTTP body in as-is.
  2. $isFreshNonce is mandatory. When omitted, verifyResponse skips nonce dedup — fine for synchronous responses, but a callback without it can be replayed. Implement it with atomic storage (e.g. Redis SET key 1 NX EX 900, TTL ≥ 15 minutes): return true the first time a nonce is seen, false on 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 acknowledgment

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

MethodDescription
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 SDKCause
timestamp out of windowInbound 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 failedVerification 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 keyKey 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.