Authentication

To start working with the Herald Exchange API, all clients must authenticate themselves.

Secure Request Signing (Pre-Request Script)

To ensure secure communication and prevent unauthorized access or tampered requests, all API requests (excluding the Authentication module) must include HMAC-based request signing with nonce-based replay attack prevention. The following Postman Pre-Request Script automatically generates the required signature and injects it into the request headers before execution.

This script performs the following steps:

  1. Loads Required Environment Keys
    • api_key — Public API key used for authentication.
    • salt_key — Secret key used to generate the cryptographic signature.
    • Both must be configured in the Postman Environment.
  2. Generates a Unix Timestamp and Nonce
    • A timestamp (in seconds) is added to each request to prevent replay attacks.
    • A unique nonce (random UUID) is generated for each request to prevent replay attacks.
  3. Extracts Request Metadata
    • Automatically identifies the request endpoint (last URL segment).
    • Parses the request body across supported formats (raw, form-data, urlencoded).
    • Removes excluded file fields to match backend signature rules.
  4. Builds the Signature Content
    • Constructs the string to sign using the format:
    {endpoint}{request_body}{timestamp}{nonce}{salt_key}
  5. Computes HMAC-SHA256 Signature
    • Uses the api_key to sign the content.
    • Produces a lowercase hexadecimal hash.
  6. RSA Signs the HMAC
    • Uses RSA private key to sign the HMAC hash.
    • Produces a base64-encoded signature.
  7. Automatically Appends Required Headers
    • X-Api-Timestamp
    • X-Api-Signature
    • X-Api-Nonce

Required Headers

Header NameDescription
X-Api-KeyA unique key assigned to each client. Identifies the calling application. Must be kept secret.
X-Api-SignatureRSA signature of HMAC-SHA256 hash computed using the endpoint, filtered request body, timestamp, nonce, and secret salt key. Ensures request integrity.
X-Api-TimestampUnix timestamp (UTC, seconds). Validates request freshness and prevents replay attacks.
X-Api-NonceUnique identifier (UUID) for each request. Prevents replay attacks by ensuring each request can only be used once.
AuthorizationThis header sends a Bearer token for authorization using the accessToken variable.

Note: Requests with invalid signatures, stale timestamps, or reused nonces will be rejected for security purposes.


Postman Pre-Request Script

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();

// Generate a unique nonce (UUID v4)
function generateUUID() {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        const r = Math.random() * 16 | 0;
        const v = c === 'x' ? r : (r & 0x3 | 0x8);
        return v.toString(16);
    });
}

const nonce = generateUUID();

pm.request.headers.upsert({ key: "X-Api-Timestamp", value: timestamp });
pm.request.headers.upsert({ key: "X-Api-Nonce", value: nonce });

// ====== 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) Serialize body (matching backend logic) ======
let bodyJson = JSON.stringify(normalizedBody, null, 0).replace(/\r\n/g, '\\n').replace(/:null(?=[,}])/g, ':""');

// ====== 4) Correct plain text with nonce ======
const plainText = normalizedEndpoint + bodyJson + timestamp + nonce + saltKey;

console.log("plaintext:", plainText);

// ====== 5) Generate HMAC ======
const hmac = CryptoJS.HmacSHA256(plainText, apiKey).toString(CryptoJS.enc.Hex);
console.log("HMAC:", hmac);

// ====== 6) RSA sign the HMAC ======
async function importPrivateKey(pemOrBase64) {
    // Accept either a full PEM (with headers) or just the base64 body.
    let b64 = pemOrBase64
        .replace(/-----BEGIN [^-]+-----/g, "")
        .replace(/-----END [^-]+-----/g, "")
        .replace(/[\r\n\t ]+/g, "")
        .replace(/\\n/g, "");

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

    // Decode base64 -> binary string
    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") {
        binary = Buffer.from(b64, "base64").toString("binary");
    } else {
        throw new Error("No base64 decoder available (atob or Buffer).");
    }

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

    try {
        return await crypto.subtle.importKey(
            "pkcs8",
            bin.buffer,
            { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
            false,
            ["sign"]
        );
    } catch (err) {
        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 });
pm.request.headers.add({ key: "X-Api-Nonce", value: nonce });
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.*;
import java.time.Instant;
import java.util.*;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import com.fasterxml.jackson.databind.ObjectMapper;

public class ApiSignature {

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

        // ====== 0) Setup your variables ======
        String apiKey = System.getenv("API_KEY");
        String saltKey = System.getenv("SALT_KEY");
        String privateKeyPEM = System.getenv("PRIVATE_KEY");

        // ====== 1) Timestamp and Nonce ======
        String timestamp = String.valueOf(Instant.now().getEpochSecond());
        String nonce = UUID.randomUUID().toString();

        // ====== 2) Normalize endpoint ======
        String fullPath = "/example/endpoint";
        String[] segments = fullPath.split("/");
        String lastSegment = segments[segments.length - 1];
        String normalizedEndpoint = "/" + lastSegment;

        // ====== 3) Parse & normalize body ======
        Map<String, Object> bodyMap = new HashMap<>();
        // Example: fill bodyMap with your request body, parsed as JSON
        // e.g., bodyMap.put("key", "value");

        // Trim strings recursively
        bodyMap = trimMap(bodyMap);

        ObjectMapper objectMapper = new ObjectMapper();
        String normalizedBody = objectMapper.writeValueAsString(bodyMap)
            .replace("\r\n", "\\n")
            .replaceAll(":null(?=[,}])", ":\"\"");

        // ====== 4) Plain text for HMAC with nonce ======
        String plainText = normalizedEndpoint + normalizedBody + timestamp + nonce + saltKey;
        System.out.println("Plaintext: " + plainText);

        // ====== 5) HMAC SHA256 ======
        Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
        SecretKeySpec secret_key = new SecretKeySpec(apiKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
        sha256_HMAC.init(secret_key);
        byte[] hmacBytes = sha256_HMAC.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
        String hmacHex = bytesToHex(hmacBytes);
        System.out.println("HMAC: " + hmacHex);

        // ====== 6) RSA sign ======
        PrivateKey privateKey = loadPrivateKey(privateKeyPEM);
        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initSign(privateKey);
        signature.update(hmacHex.getBytes(StandardCharsets.UTF_8));
        byte[] signatureBytes = signature.sign();
        String signatureBase64 = Base64.getEncoder().encodeToString(signatureBytes);

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

        // ====== 7) Headers (example usage) ======
        Map<String, String> headers = new HashMap<>();
        headers.put("x-api-key", apiKey);
        headers.put("X-Api-Signature", signatureBase64);
        headers.put("x-api-timestamp", timestamp);
        headers.put("X-Api-Nonce", nonce);

        System.out.println("Headers: " + headers);
    }

    // ====== Helper Methods ======
    private static Map<String, Object> trimMap(Map<String, Object> map) {
        Map<String, Object> trimmed = new HashMap<>();
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            Object value = entry.getValue();
            if (value instanceof String) {
                trimmed.put(entry.getKey(), ((String) value).trim());
            } else if (value instanceof Map) {
                trimmed.put(entry.getKey(), trimMap((Map<String, Object>) value));
            } else if (value instanceof List) {
                trimmed.put(entry.getKey(), trimList((List<Object>) value));
            } else {
                trimmed.put(entry.getKey(), value);
            }
        }
        return trimmed;
    }

    private static List<Object> trimList(List<Object> list) {
        List<Object> trimmed = new ArrayList<>();
        for (Object item : list) {
            if (item instanceof String) {
                trimmed.add(((String) item).trim());
            } else if (item instanceof Map) {
                trimmed.add(trimMap((Map<String, Object>) item));
            } else if (item instanceof List) {
                trimmed.add(trimList((List<Object>) item));
            } else {
                trimmed.add(item);
            }
        }
        return trimmed;
    }

    private static String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }

    private static PrivateKey loadPrivateKey(String pem) throws Exception {
        String privateKeyPEM = pem
                .replace("-----BEGIN PRIVATE KEY-----", "")
                .replace("-----END PRIVATE KEY-----", "")
                .replaceAll("\\s+", "");

        byte[] keyBytes = Base64.getDecoder().decode(privateKeyPEM);
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory kf = KeyFactory.getInstance("RSA");
        return kf.generatePrivate(spec);
    }
}
<?php

// ====== 0) Setup your variables ======
$apiKey = getenv('API_KEY');
$saltKey = getenv('SALT_KEY');
$privateKeyPEM = getenv('PRIVATE_KEY');

// ====== 1) Timestamp and Nonce ======
$timestamp = (string) time();
$nonce = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
    mt_rand(0, 0xffff), mt_rand(0, 0xffff),
    mt_rand(0, 0xffff),
    mt_rand(0, 0x0fff) | 0x4000,
    mt_rand(0, 0x3fff) | 0x8000,
    mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);

// ====== 2) Normalize endpoint ======
$fullPath = '/example/endpoint';
$segments = explode('/', trim($fullPath, '/'));
$lastSegment = end($segments);
$normalizedEndpoint = '/' . $lastSegment;

// ====== 3) Parse & normalize body ======
$bodyArray = [];

// Example body:
// $bodyArray = [
//     "name" => " John ",
//     "details" => [
//         "email" => " [email protected] ",
//         "tags" => [" tag1 ", " tag2 "]
//     ]
// ];

// Trim strings recursively
function trimArray($arr) {
    foreach ($arr as $key => $value) {
        if (is_string($value)) {
            $arr[$key] = trim($value);
        } elseif (is_array($value)) {
            $arr[$key] = trimArray($value);
        }
    }
    return $arr;
}

$bodyArray = trimArray($bodyArray);
$normalizedBody = json_encode($bodyArray, JSON_UNESCAPED_SLASHES);
$normalizedBody = str_replace(["\r\n"], ["\\n"], $normalizedBody);
$normalizedBody = preg_replace('/:null(?=[,}])/', ':""', $normalizedBody);

// ====== 4) Plain text for HMAC with nonce ======
$plainText = $normalizedEndpoint . $normalizedBody . $timestamp . $nonce . $saltKey;
echo "Plaintext: $plainText\n";

// ====== 5) HMAC SHA256 ======
$hmac = hash_hmac('sha256', $plainText, $apiKey, false);
echo "HMAC: $hmac\n";

// ====== 6) RSA Sign ======
function loadPrivateKey($pem) {
    $pem = str_replace(
        ["-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----", "\r", "\n", " "],
        "",
        $pem
    );
    $keyBytes = base64_decode($pem);
    $privateKey = openssl_pkey_get_private("-----BEGIN PRIVATE KEY-----\n" .
                                           chunk_split(base64_encode($keyBytes), 64, "\n") .
                                           "-----END PRIVATE KEY-----");
    if (!$privateKey) {
        throw new Exception("Failed to load private key. Make sure it is PKCS#8 format.");
    }
    return $privateKey;
}

$privateKey = loadPrivateKey($privateKeyPEM);

if (!openssl_sign($hmac, $signature, $privateKey, OPENSSL_ALGO_SHA256)) {
    throw new Exception("Failed to sign HMAC with RSA private key");
}

$signatureBase64 = base64_encode($signature);

echo "Signature: $signatureBase64\n";

// ====== 7) Headers ======
$headers = [
    'x-api-key: ' . $apiKey,
    'X-Api-Signature: ' . $signatureBase64,
    'x-api-timestamp: ' . $timestamp,
    'X-Api-Nonce: ' . $nonce,
];

print_r($headers);

// ====== 8) Example cURL request ======
/*
$ch = curl_init('https://your.api.endpoint' . $fullPath);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $normalizedBody);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
*/
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;

class ApiSignature
{
    static void Main()
    {
        // ====== 0) Setup your variables ======
        string apiKey = Environment.GetEnvironmentVariable("API_KEY");
        string saltKey = Environment.GetEnvironmentVariable("SALT_KEY");
        string privateKeyPEM = Environment.GetEnvironmentVariable("PRIVATE_KEY");

        // ====== 1) Timestamp and Nonce ======
        string timestamp = ((DateTimeOffset)DateTime.UtcNow).ToUnixTimeSeconds().ToString();
        string nonce = Guid.NewGuid().ToString();

        // ====== 2) Normalize endpoint ======
        string fullPath = "/example/endpoint";
        string[] segments = fullPath.Trim('/').Split('/');
        string lastSegment = segments[^1];
        string normalizedEndpoint = "/" + lastSegment;

        // ====== 3) Parse & normalize body ======
        var body = new Dictionary<string, object>();

        // Example body:
        // body["name"] = " John ";
        // body["details"] = new Dictionary<string, object> {
        //     { "email", " [email protected] " },
        //     { "tags", new List<string> { " tag1 ", " tag2 " } }
        // };

        TrimObject(body);
        string normalizedBody = JsonSerializer.Serialize(body);
        normalizedBody = normalizedBody.Replace("\r\n", "\\n");
        normalizedBody = Regex.Replace(normalizedBody, ":null(?=[,}])", ":\"\"");

        // ====== 4) Plain text for HMAC with nonce ======
        string plainText = normalizedEndpoint + normalizedBody + timestamp + nonce + saltKey;
        Console.WriteLine("Plaintext: " + plainText);

        // ====== 5) HMAC SHA256 ======
        byte[] hmacBytes;
        using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(apiKey)))
        {
            hmacBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(plainText));
        }
        string hmacHex = BitConverter.ToString(hmacBytes).Replace("-", "").ToLower();
        Console.WriteLine("HMAC: " + hmacHex);

        // ====== 6) RSA Sign ======
        using var rsa = RSA.Create();
        rsa.ImportFromPem(privateKeyPEM.ToCharArray());
        byte[] signatureBytes = rsa.SignData(Encoding.UTF8.GetBytes(hmacHex), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
        string signatureBase64 = Convert.ToBase64String(signatureBytes);

        Console.WriteLine("Signature: " + signatureBase64);

        // ====== 7) Headers ======
        var headers = new Dictionary<string, string>
        {
            { "x-api-key", apiKey },
            { "X-Api-Signature", signatureBase64 },
            { "x-api-timestamp", timestamp },
            { "X-Api-Nonce", nonce }
        };

        foreach (var kv in headers)
        {
            Console.WriteLine($"{kv.Key}: {kv.Value}");
        }
    }

    // ====== Helper Methods ======
    static void TrimObject(object obj)
    {
        if (obj is Dictionary<string, object> dict)
        {
            var keys = new List<string>(dict.Keys);
            foreach (var key in keys)
            {
                var value = dict[key];
                if (value is string strVal)
                    dict[key] = strVal.Trim();
                else
                    TrimObject(value);
            }
        }
        else if (obj is List<object> list)
        {
            for (int i = 0; i < list.Count; i++)
            {
                if (list[i] is string s)
                    list[i] = s.Trim();
                else
                    TrimObject(list[i]);
            }
        }
    }
}

Key Changes from Previous Version

  1. Added Nonce Generation: Each request now includes a unique UUID/GUID nonce to prevent replay attacks
  2. Updated Signature Content: The plaintext now includes the nonce: {endpoint}{body}{timestamp}{nonce}{salt_key}
  3. Added X-Api-Nonce Header: All requests must include this new header
  4. Body Serialization: Updated to match backend logic with null replacement: :null:"" and \r\n\\n
  5. Security Enhancement: Combines timestamp-based expiry with nonce-based single-use validation for maximum security

Security Features

  • HMAC-SHA256: Ensures data integrity
  • RSA Signature: Provides non-repudiation
  • Timestamp Validation: Prevents old request replay
  • Nonce Validation: Prevents duplicate request replay
  • Null Normalization: Consistent body serialization
// Ensure CryptoJS is available
if (typeof CryptoJS === "undefined") {
    console.log("CryptoJS is not available in Postman.");
}
 
// Extract environment variables
const apiKey = pm.environment.get("api_key");
const saltKey = pm.environment.get("salt_key");
 
// Validate required variables
if (!apiKey || !saltKey) {
    console.error("api_key or salt_key is missing in environment variables.");
    return;
}
 
// Get current timestamp in seconds
const timestamp = Math.floor(Date.now() / 1000).toString();
// pm.request.headers.upsert({ key: "X-Api-Key", value: apiKey });
pm.request.headers.upsert({ key: "X-Api-Timestamp", value: timestamp });
 
// Extract last segment of path as endpoint
const urlPathArray = pm.request.url.path;
const lastSegment = urlPathArray.filter(Boolean).pop();
const endpoint = `/${lastSegment}`; // Same as backend logic
 
console.log("Endpoint for Signature:", endpoint);
 
// Parse the request body
let parsedBody = {};
try {
    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.disabled && item.type !== 'file') {
                parsedBody[item.key] = item.value;
            }
        });
    } else if (mode === 'urlencoded') {
        parsedBody = {};
        pm.request.body.urlencoded.all().forEach(item => {
            if (!item.disabled) {
                parsedBody[item.key] = item.value;
            }
        });
    }
} catch (err) {
    console.error("Error parsing body:", err);
}
 
// Filter out excluded fields (must match backend exactly)
const excludedFields = [
    'picture', 'file', 'files', 'payment_picture',
    'import_excel_file', 'cover', 'payment_file', 'document'
];
 
excludedFields.forEach(field => {
    delete parsedBody[field];
});
 
console.log("Parsed & Filtered Body:", parsedBody);
 
// Prepare body JSON
let bodyJson = JSON.stringify(parsedBody, null, 0)
    .replace(/\r\n/g, "\\n")
    .replace(/:null(?=[,}])/g, ':""');
 
// Construct plainContent for signing
const plainContent = `${endpoint}${bodyJson}${timestamp}${saltKey}`;
console.log("Content to be signed:", plainContent);
 
// Generate HMAC-SHA256 signature
const signature = CryptoJS.HmacSHA256(plainContent, apiKey).toString(CryptoJS.enc.Hex);
console.log("Generated Signature:", signature);
 
// Attach signature header
pm.request.headers.upsert({ key: "X-Api-Signature", value: signature });
 
// Optional: Save signature if needed later
pm.environment.set("signature", signature);
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.*;
import com.fasterxml.jackson.databind.ObjectMapper;

public class SignatureGenerator {

    public static String generateSignature(
            String apiKey,
            String saltKey,
            String endpoint,
            Map<String, Object> body
    ) throws Exception {

        // Convert map to JSON string
        ObjectMapper objectMapper = new ObjectMapper();
        String bodyJson = objectMapper.writeValueAsString(body);

        // Current timestamp (seconds)
        String timestamp = String.valueOf(System.currentTimeMillis() / 1000);

        // Data format: endpoint + bodyJson + timestamp + saltKey
        String plainContent = endpoint + bodyJson + timestamp + saltKey;
        System.out.println("Content to be signed: " + plainContent);

        // HMAC-SHA256
        Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
        SecretKeySpec secret_key = new SecretKeySpec(apiKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
        sha256_HMAC.init(secret_key);

        byte[] hash = sha256_HMAC.doFinal(plainContent.getBytes(StandardCharsets.UTF_8));

        // Convert to Hex
        StringBuilder hexString = new StringBuilder();
        for (byte b : hash) {
            hexString.append(String.format("%02x", b));
        }

        return hexString.toString();
    }

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

        String apiKey = "your_api_key_here";
        String saltKey = "your_salt_key_here";

        // Example endpoint
        String endpoint = "/add_remitter";

        // Example body
        Map<String, Object> body = new HashMap<>();
        body.put("first_name", "John");
        body.put("last_name", "Doe");
        body.put("postal_code", "123456");

        // Compute signature
        String signature = generateSignature(apiKey, saltKey, endpoint, body);

        System.out.println("Generated Signature: " + signature);
        System.out.println("Timestamp: " + (System.currentTimeMillis() / 1000));
    }
}
<?php

// Environment or config values
$apiKey  = getenv("API_KEY");   // or from config
$saltKey = getenv("SALT_KEY");  // or from config

if (!$apiKey || !$saltKey) {
    die("API_KEY or SALT_KEY is missing.");
}

// Current timestamp (seconds)
$timestamp = time();

// Endpoint (last segment of URL path)
$requestUri = $_SERVER['REQUEST_URI'];
$pathParts = explode("/", trim($requestUri, "/"));
$lastSegment = end($pathParts);
$endpoint = "/" . $lastSegment; // Same as backend logic

// Read request body
$rawBody = file_get_contents("php://input");
$parsedBody = json_decode($rawBody, true);

// If body is null, set to empty array
if (!is_array($parsedBody)) {
    $parsedBody = [];
}

// Fields to exclude
$excludedFields = [
    'picture', 'file', 'files', 'payment_picture',
    'import_excel_file', 'cover', 'payment_file', 'document'
];

// Remove excluded fields
foreach ($excludedFields as $field) {
    if (isset($parsedBody[$field])) {
        unset($parsedBody[$field]);
    }
}

// Convert body to JSON
$bodyJson = json_encode($parsedBody, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);

// Replace null -> ""
$bodyJson = preg_replace('/:null(?=[,}])/', ':""', $bodyJson);

// Construct signature base string
$plainContent = $endpoint . $bodyJson . $timestamp . $saltKey;

// Generate HMAC-SHA256 signature in lowercase hex
$signature = strtolower(hash_hmac('sha256', $plainContent, $apiKey));

// Debug
error_log("Endpoint: $endpoint");
error_log("Body JSON: $bodyJson");
error_log("Signature Base String: $plainContent");
error_log("Generated Signature: $signature");

// Set headers (example for response or forwarding request)
header("X-Api-Key: $apiKey");
header("X-Api-Timestamp: $timestamp");
header("X-Api-Signature: $signature");
using System;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

public static class ApiRequestHelper
{
    public static HttpRequestMessage PrepareSignedRequest(
        HttpMethod method,
        string url,
        object body,
        string apiKey,
        string saltKey)
    {
        var request = new HttpRequestMessage(method, url);

        // Get timestamp
        var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();

        // Extract last path segment as endpoint
        var uri = new Uri(url);
        var lastSegment = uri.AbsolutePath.TrimEnd('/').Split('/').Last();
        var endpoint = "/" + lastSegment;

        // Convert body to JObject
        JObject jsonBody = JObject.FromObject(body ?? new { });

        // Fields to exclude
        string[] excludedFields =
        {
            "picture", "file", "files", "payment_picture",
            "import_excel_file", "cover", "payment_file", "document"
        };

        foreach (var field in excludedFields)
        {
            jsonBody.Remove(field);
        }

        // Convert to clean JSON
        string bodyJson = JsonConvert.SerializeObject(jsonBody, Formatting.None)
            .Replace("\r\n", "\\n")
            .Replace(":null", ":\"\"");

        // Prepare content string for signing
        string plainContent = $"{endpoint}{bodyJson}{timestamp}{saltKey}";
        Console.WriteLine("Content to Sign: " + plainContent);

        // Generate signature (HMAC SHA256 hex lowercase)
        string signature = GenerateHmacSha256(plainContent, apiKey);

        Console.WriteLine("Generated Signature: " + signature);

        // Attach headers
        request.Headers.Add("X-Api-Timestamp", timestamp);
        request.Headers.Add("X-Api-Signature", signature);

        // Add JSON body if exists
        request.Content = new StringContent(bodyJson, Encoding.UTF8, "application/json");

        return request;
    }

    private static string GenerateHmacSha256(string data, string key)
    {
        using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(key)))
        {
            var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(data));
            return BitConverter.ToString(hash).Replace("-", "").ToLower();
        }
    }
}