Skip to content

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 native fetch. On Node < 18 you will get ReferenceError: fetch is not defined.
  • No npm packages to install — just drop anony_v2.js into 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/'
ParameterDescription
appIdMerchant ID (ANONY-APP-ID)
serverPubB64Server public key public_t, encoded as base64(PEM); used to encrypt outbound requests and verify inbound signatures
merchantPrivB64Merchant private key private_u, encoded as base64(PEM); used to sign outbound requests and decrypt inbound data
baseUrlOptional 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 getTokenverify and decrypt the encrypted response envelope → extract and return the token.

js
const token = await c.getToken(secret); // Node ≥18
  • The getToken request 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:

FieldDescription
req.bodyThe business JSON, OAEP-encrypted into a Base64 ciphertext (this is the HTTP body)
req.headersThe 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:

  1. Validates that timestamp is purely numeric and within the ±5 minute window, otherwise throws timestamp out of window;
  2. If isFreshNonce was provided, calls it for deduplication — a falsy return throws duplicate nonce (replay);
  3. Verifies the signature over canonical(ts, nonce, body) with the platform public key, throwing signature verify failed on failure;
  4. 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

PitfallDetails
Lowercase response headers like anony-timestampNative 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 definedNode version < 18. Upgrade Node, or switch to another HTTP client (still using the SDK functions for encryption/signing)
Passing an async function as isFreshNonceA Promise is always truthy, so deduplication is silently skipped. await the dedup first, then pass a synchronous closure (see above)
Callback parsing fails / 400The 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" projectsanony_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 / signatureFailedServer clock drift exceeds ±5 minutes — make sure NTP is synced
Changing the crypto parametersThe 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):

MethodReturnsDescription
new AnonyV2Client(appId, serverPubB64, merchantPrivB64, baseUrl?)InstanceInitialize 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?)ObjectValidate 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):

FunctionDescription
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 &)