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.
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;
}
}Updated about 2 months ago
