Node.js SDK
The Node.js SDK is a single file, anony_v2.js, with zero third-party dependencies — it only uses Node's built-in crypto module. All protocol details (chunked RSA-OAEP SHA-1 encryption/decryption, SHA256withRSA signatures, the canonical string, timestamp/nonce replay protection) are built in, so there is nothing to implement yourself. For how the protocol works, see Encryption & Signature Protocol.
Requirements
- Node ≥ 18:
getToken()and every example on this page rely on the nativefetch. On Node < 18 you will getReferenceError: fetch is not defined. - No npm packages to install — just drop
anony_v2.jsinto your project directory (the file is shipped with your integration package, see SDK Overview).
Importing
js
const { AnonyV2Client } = require('./anony_v2');js
// anony_v2.js is a CommonJS module. If your package.json declares "type": "module",
// rename the file to anony_v2.cjs first, then import it like this:
import { AnonyV2Client } from './anony_v2.cjs';Initialization
js
const c = new AnonyV2Client(appId, serverPubB64, merchantPrivB64);
// Optional 4th argument baseUrl, defaults to 'https://api.anonypay.io/api/merchant/'| Parameter | Description |
|---|---|
appId | Merchant ID (ANONY-APP-ID) |
serverPubB64 | Server public key public_t, encoded as base64(PEM); used to encrypt outbound requests and verify inbound signatures |
merchantPrivB64 | Merchant private key private_u, encoded as base64(PEM); used to sign outbound requests and decrypt inbound data |
baseUrl | Optional API base URL, defaults to https://api.anonypay.io/api/merchant/ (the SDK normalizes the trailing slash for you) |
Keys are expected in
base64(PEM)format (the entire PEM text Base64-encoded once more). The SDK decodes this automatically — pass the values as-is. For how to obtain and safeguard the four credentials, see Preparing Credentials.
Getting a token: getToken(secret) (fully automatic)
The Node SDK ships with a ready-to-use getToken() that handles the whole flow internally: Authorization: auth + md5(APP-ID & secret) authentication → POST getToken → verify and decrypt the encrypted response envelope → extract and return the token.
js
const token = await c.getToken(secret); // Node ≥18- The
getTokenrequest body is neither encrypted nor signed — it only uses md5 authentication. The response, however, is still an encrypted + signed envelope; the SDK handles that automatically, so what you get back is the token string itself. - On failure it throws
Error('getToken failed: <message>').
Token lifetime (implementation detail)
Tokens use a sliding 8-hour expiry with a hard 24-hour cap. When a business request returns tokenExpired, simply call getToken() again for a fresh token — do not blindly refresh on a fixed schedule. See Error Codes for what each code means.
Making a business request: buildRequest(data, token)
buildRequest does everything an outbound request needs: JSON serialization → OAEP encryption into a Base64 ciphertext → generating a second-precision timestamp and a 16-byte CSPRNG nonce → signing the canonical string with SHA256withRSA. It returns { body, headers }, ready to hand straight to fetch.
Full example (matches §5.3 of the integration docs):
js
const { AnonyV2Client } = require('./anony_v2');
const c = new AnonyV2Client(appId, serverPubB64, merchantPrivB64);
const token = await c.getToken(secret); // Node ≥18: automatic md5 auth + signature verification + decryption
const req = c.buildRequest({ userOrder: 'A1001', addressType: 'trx' }, token);
const r = await fetch(c.baseUrl + 'createAddress', { method: 'POST', body: req.body, headers: req.headers });
const body = await r.json();
if (body.code === 10000) {
const data = c.verifyResponse(body.data, r.headers.get('anony-timestamp'),
r.headers.get('anony-nonce'), r.headers.get('anony-sign'));
}USDC deposits on TRON are disabled
USDC sent to a trx-type address on the TRON chain is neither credited nor called back — the funds cannot be booked automatically. For TRON deposits, only use USDT (trx.usdt) or TRX. See the API Reference for the authoritative list of supported currencies.
Return value:
| Field | Description |
|---|---|
req.body | The business JSON, OAEP-encrypted into a Base64 ciphertext (this is the HTTP body) |
req.headers | The complete set of 5 request headers: ANONY-APP-ID / ANONY-TOKEN / ANONY-TIMESTAMP / ANONY-NONCE / ANONY-SIGN |
All business endpoints are POST — just replace 'createAddress' in the example with the desired path (createWithdrawOrder / merchantInfo / getDeposits / checkWithdrawOrder; parameters are documented in the API Reference). Pass amount fields as strings to avoid precision issues.
Handling responses and callbacks: verifyResponse(...)
js
verifyResponse(encryptedBody, timestamp, nonce, signB64, isFreshNonce?)One method covers both scenarios: synchronous responses (body.data + response headers) and platform callbacks (request-body ciphertext + request headers) — both use the same canonical signature verification and OAEP decryption logic. Internally it runs, in order:
- Validates that
timestampis purely numeric and within the ±5 minute window, otherwise throwstimestamp out of window; - If
isFreshNoncewas provided, calls it for deduplication — a falsy return throwsduplicate nonce (replay); - Verifies the signature over
canonical(ts, nonce, body)with the platform public key, throwingsignature verify failedon failure; - OAEP-decrypts with the merchant private key, runs
JSON.parse, and returns the business data object.
Handling synchronous responses
See the example in the previous section: parse the outer { code, data, message } with a standard JSON library first, then when code === 10000, hand data and the three response headers to verifyResponse. For synchronous responses, isFreshNonce can be omitted.
Handling platform callbacks (isFreshNonce is required)
The platform calls your callback_url when a deposit lands or a withdrawal changes status. The HTTP request body is the Base64 ciphertext itself (not wrapped in JSON), and the request carries the ANONY-TIMESTAMP / ANONY-NONCE / ANONY-SIGN headers. In the callback direction you must deduplicate nonces — if isFreshNonce is omitted, the SDK skips deduplication and your callbacks become replayable.
js
const express = require('express');
const Redis = require('ioredis');
const { AnonyV2Client } = require('./anony_v2');
const app = express();
const redis = new Redis();
const c = new AnonyV2Client(appId, serverPubB64, merchantPrivB64);
// The callback body is the raw Base64 ciphertext — read it as raw text with express.text (do NOT use express.json)
app.post('/anony/callback', express.text({ type: '*/*' }), async (req, res) => {
// In Node, request header keys are always lowercase
const ts = req.headers['anony-timestamp'];
const nonce = req.headers['anony-nonce'];
const sig = req.headers['anony-sign'];
try {
// Deduplicate atomically with Redis SET NX first (expiry ≥ 15 minutes), then pass the result in via a synchronous closure
const fresh = await redis.set(`anony:cb:nonce:${nonce}`, 1, 'EX', 900, 'NX');
const data = c.verifyResponse(req.body, ts, nonce, sig, () => fresh === 'OK');
if (data.businessType === 'deposit') {
// Deposit credited: book funds idempotently keyed on userOrder / apiOrder
} else if (data.businessType === 'withdraw') {
// Withdrawal status change: update the order based on orderStatus
}
res.send('SUCCESS'); // On success, reply with the response agreed upon with the platform
} catch (e) {
res.status(400).send('fail'); // Signature/dedup/decryption failure: reject and let the platform retry
}
});isFreshNonce must be a synchronous function
The SDK calls it synchronously: if (isFreshNonce && !isFreshNonce(nonce)) throw .... If you pass an async function directly, its return value is a Promise (always truthy), and deduplication is silently skipped. Do it as shown above: await the atomic Redis dedup first, then pass the result in via the synchronous closure () => fresh === 'OK'.
Callback acknowledgment format (implementation detail)
The documentation example replies with plain-text success, but the current server implementation strictly matches uppercase SUCCESS: a deposit callback only counts as acknowledged when you return SUCCESS; for withdrawal callbacks, SUCCESS = confirm and proceed, FAIL = reject and refund, and any other reply triggers retries (with backoff, up to 5 attempts). Always reply with SUCCESS/FAIL. Callback fields and the retry mechanism are covered in Callbacks.
Go-live self-check (required)
Before going live, call the read-only v2/selfcheck endpoint with your production keys to validate the full request/response path (safe to call repeatedly — it never creates orders or addresses):
js
const req = c.buildRequest({ ping: 1 }, token);
const r = await fetch(c.baseUrl + 'v2/selfcheck', { method: 'POST', body: req.body, headers: req.headers });
const body = await r.json();
const data = c.verifyResponse(body.data, r.headers.get('anony-timestamp'),
r.headers.get('anony-nonce'), r.headers.get('anony-sign'));
// data => { pong: true, apiVersion: 2, serverTime: ..., echo: { ping: 1 } }If you can verify the signature, decrypt, and read back echo, both directions are working — notify the platform to complete activation. See Go-live Self-check for details.
Common pitfalls
| Pitfall | Details |
|---|---|
Lowercase response headers like anony-timestamp | Native fetch's r.headers.get() is case-insensitive, so the examples work as-is; but with axios (r.headers['anony-timestamp']) and Node's native http (req.headers), header keys are always all-lowercase. Reading them as lowercase everywhere is the safest habit |
fetch is not defined | Node version < 18. Upgrade Node, or switch to another HTTP client (still using the SDK functions for encryption/signing) |
Passing an async function as isFreshNonce | A Promise is always truthy, so deduplication is silently skipped. await the dedup first, then pass a synchronous closure (see above) |
| Callback parsing fails / 400 | The callback body is the raw Base64 ciphertext, not JSON. In Express, read it as raw text with express.text({ type: '*/*' }) — do not mount express.json() |
Import errors in "type": "module" projects | anony_v2.js is CommonJS; rename it to anony_v2.cjs before you import it |
| Hand-slicing the outer response string | / in the response body may be escaped as \/. Always use a standard JSON parser like r.json() / JSON.parse — never hand-written string slicing |
timestamp out of window / signatureFailed | Server clock drift exceeds ±5 minutes — make sure NTP is synced |
| Changing the crypto parameters | The OAEP hash must be SHA-1, chunk sizes must be 214/256 bytes, and the three canonical segments are joined with \n — the SDK is byte-for-byte aligned already, do not modify it. More troubleshooting in Troubleshooting |
Method reference
Instance methods (AnonyV2Client):
| Method | Returns | Description |
|---|---|---|
new AnonyV2Client(appId, serverPubB64, merchantPrivB64, baseUrl?) | Instance | Initialize the client |
getToken(secret) | Promise<string> | Fully automatic token retrieval (including signature verification + decryption); requires Node ≥ 18 |
buildRequest(data, token) | { body, headers } | Encrypt the business JSON and generate the full set of signed request headers |
verifyResponse(encryptedBody, timestamp, nonce, signB64, isFreshNonce?) | Object | Validate the time window / dedup / verify signature / decrypt inbound data (shared by responses and callbacks); throws on failure |
Low-level functions (also exported via module.exports, for custom wrappers / unit tests):
| Function | Description |
|---|---|
encrypt(plaintext, serverPublicKeyB64) | Chunked OAEP-SHA1 encryption, returns a Base64 ciphertext |
decrypt(cipherB64, merchantPrivateKeyB64) | Chunked decryption, returns a Buffer |
sign(canonicalStr, merchantPrivateKeyB64) | SHA256withRSA signature, returns Base64 |
verify(canonicalStr, serverPublicKeyB64, signB64) | Signature verification, returns a boolean |
canonical(ts, nonce, payload) | Builds the canonical signing string (joined with \n) |
newNonce() | 16-byte CSPRNG, returns a 32-character hex string |
authHeader(appId, secret) | Computes auth + md5(APP-ID & secret) (the separator is &) |