Authentication
All API requests must be authenticated using a combination of an API Key, an HMAC-SHA256 signature, and an RSA-signed token. This document walks you through the complete signing process with implementation examples in multiple languages.
Overview The authentication scheme uses a three-layer signing approach to verify both identity and request integrity. Every API call must carry three custom headers derived from your credentials and the request payload. Signing flow at a glance:
Build a plaintext string from the endpoint, request body, timestamp, and salt key Compute an HMAC-SHA256 hash of the plaintext using your API Key (as hex) RSA-sign the hex HMAC using your PKCS#8 private key Base64-encode the RSA signature and attach it — along with your API Key and timestamp — as request headers.
Credentials You need three values from your API dashboard before you can sign requests. Never expose these in client-side code or version control.
| Credential | Purpose |
|---|---|
| apiKey | Used as the HMAC key and sent as the x-api-key request header. |
| saltKey | Appended to the plaintext string before hashing. Acts as an additional shared secret. |
| privateKey | PKCS#8 RSA private key (full PEM or raw base64) used to produce the final RSA signature. |
Required Headers
Every authenticated request must include the following headers:
| Header Name | Description |
|---|---|
| Authorization | This header sends a Bearer token for authorization using the accessToken variable. |
Signing Steps (Detailed)
Step 1 — Build the Plaintext String Concatenate the following four components into a single string with no separator:
plainText = endpoint + JSON.stringify(trimmedBody) + timestamp + saltKey
Rules
- endpoint — only the last path segment, prefixed with /. Example: for POST /api/v1/login, use /login.
- body — the request body parsed and re-serialized as JSON after recursively trimming all string values (including nested objects and arrays). For requests with no body, use .
- timestamp — current Unix time in seconds (integer, as a string).
- saltKey — your Salt Key credential.
Example:
endpoint → /login
body → {"username":"alice","password":"secret"}
timestamp → 1718000000
saltKey → mySaltKey
plainText → /login{"username":"alice","password":"secret"}1718000000mySaltKey
Step 2 — Compute HMAC-SHA256
Run HMAC-SHA256 over the plaintext string, using your apiKey as the HMAC key. Encode the output as a lowercase hex string (64 characters).
hmac = HmacSHA256(plainText, apiKey) → hex string
This matches the output of CryptoJS.HmacSHA256(...).toString(CryptoJS.enc.Hex).
Step 3 — RSA Sign the HMAC
Sign the UTF-8 encoded hex HMAC string using your private key with algorithm RSASSA-PKCS1-v1_5 + SHA-256. Then Base64-encode the raw signature bytes.
signature = Base64( RSA_PKCS1_SHA256_Sign(privateKey, hmac) )
This is the value you send as the X-Api-Signature header.
Step 4 — Attach Headers
Add the following three headers to your request:
Authorization: Bearer <accessToken>
Postman Pre-Request Script
Paste this script into the Pre-request Script tab of your Postman collection. It reads credentials from collection variables, builds the signature, and injects the required headers automatically before every request.
// ===============================
// API Signature Generator
// ===============================
const CryptoJS = require('crypto-js');
const rs = pm.require('npm:jsrsasign');
// Collection Variables
const apiKey = pm.collectionVariables.get("apiKey");
const saltKey = pm.collectionVariables.get("saltKey");
const privateKeyPem = pm.collectionVariables.get("privateKey");
const accessToken = pm.collectionVariables.get("accessToken");
if (!apiKey) throw new Error("Missing collection variable: apiKey");
if (!saltKey) throw new Error("Missing collection variable: saltKey");
if (!privateKeyPem) throw new Error("Missing collection variable: privateKey");
if (!accessToken) throw new Error("Missing collection variable: accessToken");
// ===============================
// TIMESTAMP
// ===============================
const timestamp = Math.floor(Date.now() / 1000).toString();
// ===============================
// NONCE
// ===============================
// IMPORTANT:
//
// Your middleware uses:
//
// const nonce = (req as any).nonce || '';
//
// If antiReplayMiddleware is NOT setting req.nonce,
// then signature verification expects EMPTY nonce.
//
// Change USE_NONCE to true only if backend
// actually reads x-api-nonce and sets req.nonce.
// ===============================
const USE_NONCE = false;
let nonce = '';
if (USE_NONCE) {
nonce = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
}
pm.collectionVariables.set("nonce", nonce);
// ===============================
// ENDPOINT
// ===============================
// Backend signs ONLY last segment:
//
// /api/v1/trade/buy-by-wallet
// => /buy-by-wallet
// ===============================
const path = pm.request.url.getPath();
const segments = path.split('/').filter(Boolean);
const endpoint = '/' + (segments.pop() || '');
// ===============================
// BODY
// ===============================
let body = {};
if (
pm.request.body &&
pm.request.body.mode === 'raw' &&
pm.request.body.raw
) {
try {
body = JSON.parse(pm.request.body.raw);
} catch (e) {
body = {};
}
}
// Match backend exclusions
[
'picture',
'file',
'files',
'payment_picture',
'import_excel_file',
'cover',
'payment_file',
'document'
].forEach(key => delete body[key]);
// Match backend exactly
let bodyJson = JSON.stringify(body, null, 0)
.replace(/\r\n/g, '\\n')
.replace(/:null(?=[,}])/g, ':""');
// ===============================
// HMAC
// ===============================
const plainContent =
endpoint +
bodyJson +
timestamp +
nonce +
saltKey;
const expectedSignature = CryptoJS
.HmacSHA256(plainContent, apiKey)
.toString(CryptoJS.enc.Hex);
// ===============================
// RSA SIGN
// ===============================
const signer = new rs.KJUR.crypto.Signature({
alg: "SHA256withRSA"
});
signer.init(privateKeyPem);
signer.updateString(expectedSignature);
const signatureHex = signer.sign();
const signatureBase64 = rs.hextob64(signatureHex);
// ===============================
// HEADERS
// ===============================
pm.request.headers.upsert({
key: "Authorization",
value: `Bearer ${accessToken}`
});
pm.request.headers.upsert({
key: "x-api-key",
value: apiKey
});
pm.request.headers.upsert({
key: "x-api-timestamp",
value: timestamp
});
pm.request.headers.upsert({
key: "x-api-signature",
value: signatureBase64
});
if (USE_NONCE) {
pm.request.headers.upsert({
key: "x-api-nonce",
value: nonce
});
}
// ===============================
// DEBUG
// ===============================
console.log("========== SIGNATURE DEBUG ==========");
console.log("path:", path);
console.log("endpoint:", endpoint);
console.log("timestamp:", timestamp);
console.log("nonce:", nonce);
console.log("bodyJson:", bodyJson);
console.log("plainContent:", plainContent);
console.log("expectedSignature:", expectedSignature);
console.log("signatureBase64:", signatureBase64);
console.log("=====================================");import com.fasterxml.jackson.databind.ObjectMapper;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.time.Instant;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.Map;
public class SignatureGenerator {
public static void main(String[] args) throws Exception {
String apiKey = "YOUR_API_KEY";
String saltKey = "YOUR_SALT_KEY";
String accessToken = "YOUR_ACCESS_TOKEN";
String privateKeyPem = """
-----BEGIN PRIVATE KEY-----
YOUR_PRIVATE_KEY
-----END PRIVATE KEY-----
""";
String path = "/api/v1/buy-by-wallet";
// Timestamp (seconds)
String timestamp = String.valueOf(Instant.now().getEpochSecond());
// Empty nonce (same as your Postman script)
String nonce = "";
// Extract last path segment
String[] parts = path.split("/");
String endpoint = "/" + parts[parts.length - 1];
// Build body preserving order
Map<String, Object> body = new LinkedHashMap<>();
body.put("from", "USD");
body.put("to", "TRX");
body.put("value", 100);
ObjectMapper mapper = new ObjectMapper();
String bodyJson = mapper.writeValueAsString(body)
.replace("\r\n", "\\n")
.replaceAll(":null(?=[,}])", ":\"\"");
String plainContent =
endpoint +
bodyJson +
timestamp +
nonce +
saltKey;
String expectedSignature = hmacSha256Hex(
plainContent,
apiKey
);
String signatureBase64 = rsaSign(
expectedSignature,
privateKeyPem
);
System.out.println("endpoint: " + endpoint);
System.out.println("bodyJson: " + bodyJson);
System.out.println("plainContent: " + plainContent);
System.out.println("expectedSignature: " + expectedSignature);
System.out.println("signatureBase64: " + signatureBase64);
// Headers to send
System.out.println("\nHeaders:");
System.out.println("Authorization: Bearer " + accessToken);
System.out.println("x-api-key: " + apiKey);
System.out.println("x-api-timestamp: " + timestamp);
System.out.println("x-api-signature: " + signatureBase64);
}
private static String hmacSha256Hex(
String data,
String secret
) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(
new SecretKeySpec(
secret.getBytes(StandardCharsets.UTF_8),
"HmacSHA256"
)
);
byte[] result = mac.doFinal(
data.getBytes(StandardCharsets.UTF_8)
);
StringBuilder sb = new StringBuilder();
for (byte b : result) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
private static String rsaSign(
String message,
String privateKeyPem
) throws Exception {
String privateKeyContent = privateKeyPem
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s+", "");
byte[] keyBytes =
Base64.getDecoder().decode(privateKeyContent);
PKCS8EncodedKeySpec spec =
new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory =
KeyFactory.getInstance("RSA");
PrivateKey privateKey =
keyFactory.generatePrivate(spec);
Signature signature =
Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey);
signature.update(
message.getBytes(StandardCharsets.UTF_8)
);
byte[] signed =
signature.sign();
return Base64.getEncoder()
.encodeToString(signed);
}
}<?php
$apiKey = "YOUR_API_KEY";
$saltKey = "YOUR_SALT_KEY";
$accessToken = "YOUR_ACCESS_TOKEN";
$privateKeyPem = <<<PEM
-----BEGIN PRIVATE KEY-----
YOUR_PRIVATE_KEY
-----END PRIVATE KEY-----
PEM;
// Request path
$path = "/api/v1/buy-by-wallet";
// Timestamp (seconds)
$timestamp = (string) time();
// Empty nonce (matches your current Postman script)
$nonce = "";
// Backend signs only the last path segment
$segments = array_values(array_filter(explode('/', $path)));
$endpoint = '/' . end($segments);
// Body (preserve key order)
$body = [
"from" => "USD",
"to" => "TRX",
"value"=> 100
];
// Match Node.js JSON.stringify()
$bodyJson = json_encode(
$body,
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
);
$bodyJson = str_replace("\r\n", "\\n", $bodyJson);
// Build plainContent exactly like backend
$plainContent =
$endpoint .
$bodyJson .
$timestamp .
$nonce .
$saltKey;
// HMAC SHA256 (hex output)
$expectedSignature = hash_hmac(
'sha256',
$plainContent,
$apiKey
);
// RSA SHA256 sign the HMAC hex string
$privateKey = openssl_pkey_get_private($privateKeyPem);
if (!$privateKey) {
die("Invalid private key");
}
openssl_sign(
$expectedSignature,
$rawSignature,
$privateKey,
OPENSSL_ALGO_SHA256
);
$signatureBase64 = base64_encode($rawSignature);
// Debug
echo "endpoint: {$endpoint}\n";
echo "bodyJson: {$bodyJson}\n";
echo "plainContent: {$plainContent}\n";
echo "expectedSignature: {$expectedSignature}\n";
echo "signatureBase64: {$signatureBase64}\n";
// Headers
$headers = [
"Authorization: Bearer {$accessToken}",
"x-api-key: {$apiKey}",
"x-api-timestamp: {$timestamp}",
"x-api-signature: {$signatureBase64}"
];
print_r($headers);using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
string apiKey = "YOUR_API_KEY";
string saltKey = "YOUR_SALT_KEY";
string accessToken = "YOUR_ACCESS_TOKEN";
string privateKeyPem = """
-----BEGIN PRIVATE KEY-----
YOUR_PRIVATE_KEY
-----END PRIVATE KEY-----
""";
string path = "/api/v1/buy-by-wallet";
// Timestamp (seconds)
string timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
// Empty nonce (matches your current Postman script)
string nonce = "";
// Backend signs only the last path segment
string endpoint = "/" + path.Split('/', StringSplitOptions.RemoveEmptyEntries).Last();
// Body
var body = new Dictionary<string, object>
{
["from"] = "USD",
["to"] = "TRX",
["value"] = 100
};
// Match JSON.stringify()
string bodyJson = JsonSerializer.Serialize(body);
bodyJson = bodyJson
.Replace("\r\n", "\\n");
// Build plainContent
string plainContent =
endpoint +
bodyJson +
timestamp +
nonce +
saltKey;
// HMAC SHA256 -> hex
string expectedSignature;
using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(apiKey)))
{
byte[] hash = hmac.ComputeHash(
Encoding.UTF8.GetBytes(plainContent));
expectedSignature = Convert.ToHexString(hash)
.ToLowerInvariant();
}
// RSA SHA256 sign the HMAC hex string
string signatureBase64;
using (RSA rsa = RSA.Create())
{
rsa.ImportFromPem(privateKeyPem);
byte[] signature = rsa.SignData(
Encoding.UTF8.GetBytes(expectedSignature),
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1);
signatureBase64 = Convert.ToBase64String(signature);
}
// Debug
Console.WriteLine($"endpoint: {endpoint}");
Console.WriteLine($"bodyJson: {bodyJson}");
Console.WriteLine($"plainContent: {plainContent}");
Console.WriteLine($"expectedSignature: {expectedSignature}");
Console.WriteLine($"signatureBase64: {signatureBase64}");
Console.WriteLine("\nHeaders:");
Console.WriteLine($"Authorization: Bearer {accessToken}");
Console.WriteLine($"x-api-key: {apiKey}");
Console.WriteLine($"x-api-timestamp: {timestamp}");
Console.WriteLine($"x-api-signature: {signatureBase64}");