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.

if (pm.execution.location.includes("Auth")) return;

const apiKey = pm.collectionVariables.get("apiKey");
console.log('api-key', apiKey);
const saltKey = pm.collectionVariables.get("saltKey");
const privateKeyPEM = pm.collectionVariables.get("privateKey")
const timestamp = Math.floor(Date.now() / 1000).toString();

pm.collectionVariables.set("X-Api-Timestamp", timestamp);


// ====== 1) EXACT SAME ENDPOINT AS FRONTEND ======
const segments = pm.request.url.path;
const lastSegment = segments[segments.length - 1];

const normalizedEndpoint = "/" + lastSegment;

// ====== 2) EXACT SAME BODY SERIALIZATION ======
let parsedBody = {};

if (pm.request.body && pm.request.body.mode) {
    const mode = pm.request.body.mode;

    if (mode === 'raw') {
        parsedBody = JSON.parse(pm.request.body.raw || "{}");
    } else if (mode === 'formdata') {
        parsedBody = {};
        pm.request.body.formdata.all().forEach(item => {
            if (item.type !== 'file' && !item.disabled) parsedBody[item.key] = item.value;
        });
    }
}

let normalizedBody = parsedBody || {};

const trimObject = obj => {
    if (typeof obj === "string") return obj.trim();
    if (Array.isArray(obj)) return obj.map(trimObject);
    if (obj && typeof obj === "object") {
        return Object.fromEntries(
            Object.entries(obj).map(([k, v]) => [k, trimObject(v)])
        );
    }
    return obj;
};

normalizedBody = trimObject(normalizedBody);

// ====== 3) Correct plain text ======
const plainText =  normalizedEndpoint + JSON.stringify(normalizedBody) + timestamp + saltKey;

console.log("plaintext :" , plainText)

// ====== 4) Generate HMAC ======
const hmac = CryptoJS.HmacSHA256(plainText, apiKey).toString(CryptoJS.enc.Hex);

// ====== 5) RSA sign like frontend ======
async function importPrivateKey(pemOrBase64) {
  // Accept either a full PEM (with headers) or just the base64 body.
  let b64 = pemOrBase64
    // remove header/footer lines if present
    .replace(/-----BEGIN [^-]+-----/g, "")
    .replace(/-----END [^-]+-----/g, "")
    // remove real whitespace (spaces, tabs, CRLF)
    .replace(/[\r\n\t ]+/g, "")
    // remove literal backslash-n sequences that sometimes appear when copying into envs ("\\n")
    .replace(/\\n/g, "");

  // Add padding if required (length % 4 must be 0)
  while (b64.length % 4 !== 0) b64 += "=";

  // Decode base64 -> binary string (works in browser or Node)
  let binary;
  if (typeof atob === "function") {
    try {
      binary = atob(b64);
    } catch (e) {
      throw new Error("Base64 decode failed in atob: " + e.message);
    }
  } else if (typeof Buffer !== "undefined") {
    // Node / Postman environment
    binary = Buffer.from(b64, "base64").toString("binary");
  } else {
    throw new Error("No base64 decoder available (atob or Buffer).");
  }

// async function importPrivateKey(pem) {
//   let b64 = pem
//     .replace(/-----BEGIN [^-]+-----/, "")
//     .replace(/-----END [^-]+-----/, "")
//     .replace(/\r?\n|\r/g, "")
//     .replace(/\\n/g, "");

//   const binary = Buffer.from(b64, "base64");
//   const bin = new Uint8Array(binary);

//   return await crypto.subtle.importKey(
//     "pkcs8",
//     bin.buffer,
//     { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
//     false,
//     ["sign"]
//   );
// }


  // Convert to Uint8Array
  const bin = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) {
    bin[i] = binary.charCodeAt(i);
  }

  // Try import; WebCrypto requires PKCS8 for private keys.
  try {
    return await crypto.subtle.importKey(
      "pkcs8",
      bin.buffer,
      { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
      false,
      ["sign"]
    );
  } catch (err) {
    // Common failure: key is PKCS#1 ("RSA PRIVATE KEY") not PKCS#8 ("PRIVATE KEY").
    // If that's the case, you'll see a DataError / format error. Tell user how to convert.
    throw new Error(
      "importKey failed: " + err.message +
      "\n\nIf this key is in PEM '-----BEGIN RSA PRIVATE KEY-----' (PKCS#1) format, WebCrypto expects PKCS#8. Convert with OpenSSL:\n" +
      "openssl pkcs8 -topk8 -nocrypt -in rsa_key.pem -out key_pkcs8.pem\n"
    );
  }
}

const privateKey = await importPrivateKey(privateKeyPEM);
const signatureBuffer = await crypto.subtle.sign(
    "RSASSA-PKCS1-v1_5",
    privateKey,
    new TextEncoder().encode(hmac)
);

const signatureBase64 = btoa(String.fromCharCode(...new Uint8Array(signatureBuffer)));

pm.collectionVariables.set("signature", signatureBase64);

pm.request.headers.add({ key: "x-api-key", value: apiKey });
pm.request.headers.add({ key: "X-Api-Signature", value: signatureBase64 });
pm.request.headers.add({ key: "x-api-timestamp", value: timestamp });
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.*;
import java.util.Base64;

public class ApiSigner {

    public static void main(String[] args) throws Exception {

        String apiKey = "YOUR_API_KEY";
        String saltKey = "YOUR_SALT_KEY";
        String privateKeyPEM = "YOUR_PRIVATE_KEY"; // can be full PEM or base64
        String endpoint = "/your-endpoint";
        String requestBodyJson = "{}"; // JSON string of trimmed body

        // ====== 1) Timestamp ======
        String timestamp = String.valueOf(System.currentTimeMillis() / 1000);

        // ====== 2) Plaintext (same as JS) ======
        String plainText = endpoint + requestBodyJson + timestamp + saltKey;
        System.out.println("plaintext: " + plainText);

        // ====== 3) HMAC SHA256 (same as CryptoJS.HmacSHA256) ======
        String hmacHex = generateHmacSHA256(plainText, apiKey);
        System.out.println("hmac: " + hmacHex);

        // ====== 4) RSA Sign (RSASSA-PKCS1-v1_5 + SHA256) ======
        PrivateKey privateKey = loadPrivateKey(privateKeyPEM);

        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initSign(privateKey);
        signature.update(hmacHex.getBytes(StandardCharsets.UTF_8));

        byte[] signedBytes = signature.sign();
        String signatureBase64 = Base64.getEncoder().encodeToString(signedBytes);

        System.out.println("signature: " + signatureBase64);

        // ====== 5) These values go into headers ======
        System.out.println("x-api-key: " + apiKey);
        System.out.println("x-api-timestamp: " + timestamp);
        System.out.println("X-Api-Signature: " + signatureBase64);
    }

    // ====== HMAC SHA256 ======
    private static String generateHmacSHA256(String data, String key) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        SecretKeySpec secretKeySpec =
                new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
        mac.init(secretKeySpec);

        byte[] hmacBytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
        return bytesToHex(hmacBytes);
    }

    // ====== Convert bytes to hex (same as CryptoJS.enc.Hex) ======
    private static String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }

    // ====== Load PKCS8 Private Key (same as WebCrypto importKey) ======
    private static PrivateKey loadPrivateKey(String pemOrBase64) throws Exception {

        String privateKeyContent = pemOrBase64
                .replaceAll("-----BEGIN [^-]+-----", "")
                .replaceAll("-----END [^-]+-----", "")
                .replaceAll("\\s", "")
                .replaceAll("\\\\n", "");

        // Fix padding
        while (privateKeyContent.length() % 4 != 0) {
            privateKeyContent += "=";
        }

        byte[] keyBytes = Base64.getDecoder().decode(privateKeyContent);

        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");

        try {
            return keyFactory.generatePrivate(keySpec);
        } catch (Exception e) {
            throw new Exception(
                "importKey failed: " + e.getMessage() +
                "\nIf this key is PKCS#1 (-----BEGIN RSA PRIVATE KEY-----), convert to PKCS#8:\n" +
                "openssl pkcs8 -topk8 -nocrypt -in rsa_key.pem -out key_pkcs8.pem"
            );
        }
    }
}
<?php

$apiKey = "YOUR_API_KEY";
$saltKey = "YOUR_SALT_KEY";
$privateKeyPEM = "YOUR_PRIVATE_KEY"; // full PEM or base64
$endpoint = "/your-endpoint";
$requestBodyJson = "{}"; // already trimmed JSON string

// ====== 1) Timestamp (same as JS) ======
$timestamp = (string) floor(time());

// ====== 2) Plaintext ======
$plainText = $endpoint . $requestBodyJson . $timestamp . $saltKey;
echo "plaintext: " . $plainText . PHP_EOL;

// ====== 3) HMAC SHA256 (HEX like CryptoJS) ======
$hmacHex = hash_hmac('sha256', $plainText, $apiKey);
echo "hmac: " . $hmacHex . PHP_EOL;

// ====== 4) Load Private Key ======
$privateKey = loadPrivateKey($privateKeyPEM);

// ====== 5) RSA Sign (RSASSA-PKCS1-v1_5 + SHA256) ======
openssl_sign(
    $hmacHex,
    $signatureBinary,
    $privateKey,
    OPENSSL_ALGO_SHA256
);

$signatureBase64 = base64_encode($signatureBinary);
echo "signature: " . $signatureBase64 . PHP_EOL;

// ====== 6) Headers to send ======
$headers = [
    "x-api-key: $apiKey",
    "x-api-timestamp: $timestamp",
    "X-Api-Signature: $signatureBase64"
];

print_r($headers);



/**
 * Load PKCS8 private key (same logic as WebCrypto importKey)
 */
function loadPrivateKey($pemOrBase64)
{
    // Remove header/footer if present
    $clean = preg_replace('/-----BEGIN [^-]+-----/', '', $pemOrBase64);
    $clean = preg_replace('/-----END [^-]+-----/', '', $clean);
    $clean = preg_replace('/\s+/', '', $clean);
    $clean = str_replace("\\n", "", $clean);

    // Fix padding
    while (strlen($clean) % 4 !== 0) {
        $clean .= "=";
    }

    $decoded = base64_decode($clean);

    // Rebuild proper PEM (required by OpenSSL)
    $pem = "-----BEGIN PRIVATE KEY-----\n" .
        chunk_split(base64_encode($decoded), 64, "\n") .
        "-----END PRIVATE KEY-----";

    $privateKey = openssl_pkey_get_private($pem);

    if (!$privateKey) {
        throw new Exception(
            "importKey failed. If key is PKCS#1 (-----BEGIN RSA PRIVATE KEY-----), convert to PKCS#8:\n" .
            "openssl pkcs8 -topk8 -nocrypt -in rsa_key.pem -out key_pkcs8.pem"
        );
    }

    return $privateKey;
}
using System;
using System.Security.Cryptography;
using System.Text;

class ApiSigner
{
    static void Main()
    {
        string apiKey = "YOUR_API_KEY";
        string saltKey = "YOUR_SALT_KEY";
        string privateKeyPEM = "YOUR_PRIVATE_KEY"; // Full PEM or base64
        string endpoint = "/your-endpoint";
        string requestBodyJson = "{}"; // already trimmed JSON string

        // ====== 1) Timestamp ======
        string timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();

        // ====== 2) Plaintext ======
        string plainText = endpoint + requestBodyJson + timestamp + saltKey;
        Console.WriteLine("plaintext: " + plainText);

        // ====== 3) HMAC SHA256 (HEX like CryptoJS) ======
        string hmacHex = GenerateHmacSha256(plainText, apiKey);
        Console.WriteLine("hmac: " + hmacHex);

        // ====== 4) Load Private Key ======
        using RSA rsa = LoadPrivateKey(privateKeyPEM);

        // ====== 5) RSA Sign (RSASSA-PKCS1-v1_5 + SHA256) ======
        byte[] signatureBytes = rsa.SignData(
            Encoding.UTF8.GetBytes(hmacHex),
            HashAlgorithmName.SHA256,
            RSASignaturePadding.Pkcs1
        );

        string signatureBase64 = Convert.ToBase64String(signatureBytes);
        Console.WriteLine("signature: " + signatureBase64);

        // ====== 6) Headers ======
        Console.WriteLine("x-api-key: " + apiKey);
        Console.WriteLine("x-api-timestamp: " + timestamp);
        Console.WriteLine("X-Api-Signature: " + signatureBase64);
    }

    // ====== HMAC SHA256 ======
    static string GenerateHmacSha256(string data, string key)
    {
        using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(key));
        byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(data));

        StringBuilder sb = new StringBuilder();
        foreach (byte b in hash)
            sb.Append(b.ToString("x2")); // hex lowercase (same as CryptoJS)

        return sb.ToString();
    }

    // ====== Load PKCS8 Private Key ======
    static RSA LoadPrivateKey(string pemOrBase64)
    {
        string clean = pemOrBase64
            .Replace("-----BEGIN PRIVATE KEY-----", "")
            .Replace("-----END PRIVATE KEY-----", "")
            .Replace("-----BEGIN RSA PRIVATE KEY-----", "")
            .Replace("-----END RSA PRIVATE KEY-----", "")
            .Replace("\r", "")
            .Replace("\n", "")
            .Replace("\\n", "")
            .Trim();

        while (clean.Length % 4 != 0)
            clean += "=";

        byte[] keyBytes = Convert.FromBase64String(clean);

        RSA rsa = RSA.Create();

        try
        {
            rsa.ImportPkcs8PrivateKey(keyBytes, out _);
        }
        catch
        {
            throw new Exception(
                "importKey failed. If this key is PKCS#1 (BEGIN RSA PRIVATE KEY), convert to PKCS#8:\n" +
                "openssl pkcs8 -topk8 -nocrypt -in rsa_key.pem -out key_pkcs8.pem"
            );
        }

        return rsa;
    }
}