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.


CredentialPurpose
apiKeyUsed as the HMAC key and sent as the x-api-key request header.
saltKeyAppended to the plaintext string before hashing. Acts as an additional shared secret.
privateKeyPKCS#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 NameDescription
AuthorizationThis 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}");