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:
- 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.
- 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.
- 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.
- Builds the Signature Content
- Constructs the string to sign using the format:
{endpoint}{request_body}{timestamp}{nonce}{salt_key} - Computes HMAC-SHA256 Signature
- Uses the api_key to sign the content.
- Produces a lowercase hexadecimal hash.
- RSA Signs the HMAC
- Uses RSA private key to sign the HMAC hash.
- Produces a base64-encoded signature.
- Automatically Appends Required Headers
- X-Api-Timestamp
- X-Api-Signature
- X-Api-Nonce
Required Headers
| Header Name | Description |
|---|---|
| X-Api-Key | A unique key assigned to each client. Identifies the calling application. Must be kept secret. |
| X-Api-Signature | RSA signature of HMAC-SHA256 hash computed using the endpoint, filtered request body, timestamp, nonce, and secret salt key. Ensures request integrity. |
| X-Api-Timestamp | Unix timestamp (UTC, seconds). Validates request freshness and prevents replay attacks. |
| X-Api-Nonce | Unique identifier (UUID) for each request. Prevents replay attacks by ensuring each request can only be used once. |
| Authorization | This 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
- Added Nonce Generation: Each request now includes a unique UUID/GUID nonce to prevent replay attacks
- Updated Signature Content: The plaintext now includes the nonce:
{endpoint}{body}{timestamp}{nonce}{salt_key} - Added X-Api-Nonce Header: All requests must include this new header
- Body Serialization: Updated to match backend logic with null replacement:
:null→:""and\r\n→\\n - 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();
}
}
}Updated about 9 hours ago
