HMAC Authentication
HMAC (hash-based message authentication code) signs each request with a secret key shared between you and Tuned Global. The server recomputes the same signature independently and rejects the request if it does not match, so it can verify both that the request came from you and that nothing in it changed in transit.
If a request is altered in transit, whether by a malicious intermediary or a misbehaving proxy that drops headers, the signature will no longer match and Tuned Global rejects the call.
Our HMAC generator tool can be used to create API calls and and generate the OAuth signature using your API key and secret.
Generating the signature
- Generate a new GUID to use as the nonce, and take the current Unix timestamp in seconds (not milliseconds).
- Take the full request URI and URL-encode it using UTF-8. Match the exact casing of the URL you are calling, since the server signs whatever URI it actually receives. Lowercase is the safe default, since Tuned Global's routes are lowercase.
- If the request carries a JSON payload, serialize it. If it has no payload (a GET request, or a POST/PUT with an empty body), skip to step 6 and treat the payload as an empty string.
- Convert the serialized payload to bytes using UTF-8 encoding.
- Hash those bytes with MD5, then convert the resulting hash to a Base64 string. (MD5 is only used here as a content checksum folded into the final signature. The actual security guarantee comes from the HMAC-SHA256 step below, with your secret key.)
- Concatenate the following fields, in order, with nothing in between them, to build the raw signature string:
{access-key}{HTTP-method}{request-URI}{encoded-payload, or an empty string if there is none}{nonce}{timestamp} - Convert that concatenated string to bytes using UTF-8 encoding.
- Decode the secret key Tuned Global gave you from Base64 into a byte array. Do not UTF-8-encode the key string itself, it is already Base64.
- Compute the HMAC-SHA256 hash of the bytes from step 7, using the key bytes from step 8.
- Convert the resulting hash to a Base64 string. This is your request signature.
- Build the Authorization header value by joining the access key, the signature, the nonce, and the timestamp with colons:
{access-key}:{request-signature}:{nonce}:{timestamp} - Send it as the Authorization header, prefixed with the
Tuned-HMACscheme:Authorization: Tuned-HMAC {access-key}:{request-signature}:{nonce}:{timestamp}
What gets a request rejected
- A stale timestamp. If Tuned Global receives the request more than 5 minutes after the timestamp you signed it with, it treats the request as expired and rejects it. Sign and send close together, do not generate a signature far ahead of when you will send it.
- A reused nonce. Each request needs its own fresh nonce. Sending the same nonce twice, even with a valid timestamp, gets the second request rejected as a replay.
- Signing different bytes than you send. The payload hash must come from the exact bytes in the request body. If your serializer produces different output (field order, spacing, new line breaks) between the copy you hash and the copy you actually send, the signature will not match.

Code Samples for Generating HMAC

Code Samples for Generating HMAC
JavaScript
// -----------------------------------------------------------------------------
// SECTION 1: Credential Loading
// -----------------------------------------------------------------------------
// Reads AccessKey and SecretKey from collection variables.
// Never hardcode credentials here — always use collection variables.
var accessKey = pm.variables.get("AccessKey") || "";
var secretKey = pm.variables.get("SecretKey") || "";
if (!accessKey || !secretKey) {
throw new Error("Missing credentials. Please set AccessKey and SecretKey in your Collection Variables.");
}
// -----------------------------------------------------------------------------
// SECTION 2: Helper Functions
// -----------------------------------------------------------------------------
/**
* Encodes a URL using encodeURIComponent but with lowercase % escapes.
* This matches the encoding format expected by the API signature algorithm.
*/
function encodeUriLowercase(url) {
return encodeURIComponent(url).replace(/%\w\w/g, function (m) {
return m.toLowerCase();
});
}
/**
* Returns the raw request body string for POST/PUT/PATCH requests.
* Returns an empty string for GET requests or requests with no body.
*/
function getRawBody() {
if (!pm.request.body) { return ""; }
if (pm.request.body.mode === "raw") {
return pm.request.body.raw || "";
}
return "";
}
/**
* Computes an MD5 hash of the request body, returned as a Base64 string.
* This is included in the signature for non-GET requests to ensure
* the payload has not been tampered with.
*/
function computePayloadMd5Base64(raw) {
if (!raw || !raw.trim()) { return ""; }
try {
var md5 = CryptoJS.MD5(CryptoJS.enc.Utf8.parse(raw));
return CryptoJS.enc.Base64.stringify(md5);
} catch (e) {
return "";
}
}
/**
* Resolves the full request URL by expanding Postman variables ({{...}})
* and path variables (:param), then normalizes duplicate slashes.
* Throws an error if any variables remain unresolved.
*/
function resolveFinalUrl() {
var raw = pm.request.url.toString();
var resolved = pm.variables.replaceIn(raw);
// Substitute :pathVariable style params
if (pm.request.url.variable && typeof pm.request.url.variable.each === "function") {
pm.request.url.variable.each(function (v) {
resolved = resolved.replace(":" + v.key, v.value);
});
}
// Normalize accidental double slashes (preserves https://)
resolved = resolved.replace(/(?<!:)\/{2,}/g, "/");
// Guard against unresolved {{variables}}
if (/\{\{[^}]+\}\}/.test(resolved)) {
throw new Error("Unresolved variable in URL: " + resolved + ". Check that all variables are set in Collection Variables.");
}
return resolved;
}
/**
* Determines whether this request should include a nonce and timestamp.
* Certain internal API paths (e.g. /api/v2/tconnect/) use a simplified
* signature without a nonce.
*/
function useNonce() {
return !resolveFinalUrl().includes("/api/v2/tconnect/");
}
// -----------------------------------------------------------------------------
// SECTION 3: Build the Signature
// -----------------------------------------------------------------------------
var method = (pm.request.method || "GET").toUpperCase();
var resolvedUrl = resolveFinalUrl();
var encodedUri = encodeUriLowercase(resolvedUrl);
// Generate a unique nonce (UUID) and Unix timestamp for replay protection.
// These are omitted for endpoints that don't require them.
var nonce = useNonce() ? pm.variables.replaceIn("{{$guid}}") : "";
var timestamp = useNonce() ? String(Math.floor(Date.now() / 1000)) : "";
// Construct the raw signature string based on the HTTP method.
// Format (GET): AccessKey + Method + EncodedURI [+ Nonce + Timestamp]
// Format (POST): AccessKey + Method + EncodedURI + BodyMD5 [+ Nonce + Timestamp]
var signatureRawData = "";
if (method === "GET") {
signatureRawData = accessKey + method + encodedUri + (useNonce() ? (nonce + timestamp) : "");
} else {
var rawBody = getRawBody();
var bodyMd5 = computePayloadMd5Base64(rawBody);
signatureRawData = accessKey + method + encodedUri + bodyMd5 + (useNonce() ? (nonce + timestamp) : "");
}
// Sign the raw data using HMAC-SHA256 with the Base64-decoded SecretKey.
var signatureBytes = CryptoJS.enc.Utf8.parse(signatureRawData);
var secretKeyBytes = CryptoJS.enc.Base64.parse(secretKey);
var signatureHash = CryptoJS.HmacSHA256(signatureBytes, secretKeyBytes).toString(CryptoJS.enc.Base64);
// -----------------------------------------------------------------------------
// SECTION 4: Inject Authorization Header
// -----------------------------------------------------------------------------
// Composes and injects the Authorization header into the outgoing request.
// Format: Tuned-HMAC {AccessKey}:{Signature}[:{Nonce}:{Timestamp}]
var authHeaderValue = useNonce()
? "Tuned-HMAC " + accessKey + ":" + signatureHash + ":" + nonce + ":" + timestamp
: "Tuned-HMAC " + accessKey + ":" + signatureHash;
pm.request.headers.upsert({ key: "Authorization", value: authHeaderValue });
// -----------------------------------------------------------------------------
// SECTION 5: Debug Logging (visible in Postman Console)
// -----------------------------------------------------------------------------
// Open View → Postman Console to inspect these values when troubleshooting.
console.log("=== Tuned HMAC Auth Debug ===");
console.log("Method: ", method);
console.log("Resolved URL: ", resolvedUrl);
console.log("Encoded URI: ", encodedUri);
console.log("Use Nonce: ", useNonce());
console.log("Nonce: ", nonce || "(none)");
console.log("Timestamp: ", timestamp || "(none)");
console.log("Signature Raw: ", signatureRawData);
console.log("Signature Hash: ", signatureHash);
console.log("Authorization: ", authHeaderValue);
Python
"""
Tuned HMAC request signing — ported from a Postman pre-request script.
Computes the "Tuned-HMAC" Authorization header used by TunedConnect APIs.
"""
import base64
import hashlib
import hmac
import re
import time
import uuid
from urllib.parse import quote
# -----------------------------------------------------------------------------
# SECTION 1: Credential Loading
# -----------------------------------------------------------------------------
# Replace these with your own AccessKey and SecretKey before running this sample.
access_key = "YourAccessKeyHere"
secret_key = "YourSecretKeyHere"
# -----------------------------------------------------------------------------
# SECTION 2: Helper Functions
# -----------------------------------------------------------------------------
def encode_uri_lowercase(url):
"""
Encodes a URL the way JS encodeURIComponent does, but with lowercase % escapes.
This matches the encoding format expected by the API signature algorithm.
"""
encoded = quote(url, safe="!*'()") # matches encodeURIComponent's unreserved set
return re.sub(r"%[0-9A-Fa-f]{2}", lambda m: m.group(0).lower(), encoded)
def get_raw_body(body):
"""
Returns the raw request body string for POST/PUT/PATCH requests.
Returns an empty string for GET requests or requests with no body.
Assumes `body` is already the raw payload string (Postman's "raw" mode).
"""
return body or ""
def compute_payload_md5_base64(raw):
"""
Computes an MD5 hash of the request body, returned as a Base64 string.
This is included in the signature for non-GET requests to ensure
the payload has not been tampered with.
"""
if not raw or not raw.strip():
return ""
try:
digest = hashlib.md5(raw.encode("utf-8")).digest()
return base64.b64encode(digest).decode("utf-8")
except Exception:
return ""
def resolve_final_url(url):
"""
Normalizes accidental double slashes in the URL (preserves scheme://).
Assumes {{variables}} and :pathVariables have already been substituted.
"""
resolved = re.sub(r"(?<!:)/{2,}", "/", url)
if re.search(r"\{\{[^}]+\}\}", resolved):
raise RuntimeError("Unresolved variable in URL: " + resolved + ". Check that all variables are set.")
return resolved
def use_nonce(url):
"""
Determines whether this request should include a nonce and timestamp.
Certain internal API paths (e.g. /api/v2/tconnect/) use a simplified
signature without a nonce.
"""
return "/api/v2/tconnect/" not in resolve_final_url(url)
# -----------------------------------------------------------------------------
# SECTION 3: Build the Signature
# -----------------------------------------------------------------------------
# `method`, `url`, and `raw_body` represent the outgoing request being signed
# (Postman reads these from pm.request; wire these up to your actual request).
method = "GET"
url = "https://api.example.com/v2/some/endpoint"
raw_body = ""
method = (method or "GET").upper()
resolved_url = resolve_final_url(url)
encoded_uri = encode_uri_lowercase(resolved_url)
nonce_needed = use_nonce(url)
nonce = str(uuid.uuid4()) if nonce_needed else ""
timestamp = str(int(time.time())) if nonce_needed else ""
if method == "GET":
signature_raw_data = access_key + method + encoded_uri + (nonce + timestamp if nonce_needed else "")
else:
body = get_raw_body(raw_body)
body_md5 = compute_payload_md5_base64(body)
signature_raw_data = access_key + method + encoded_uri + body_md5 + (nonce + timestamp if nonce_needed else "")
secret_key_bytes = base64.b64decode(secret_key)
signature_hash = base64.b64encode(
hmac.new(secret_key_bytes, signature_raw_data.encode("utf-8"), hashlib.sha256).digest()
).decode("utf-8")
# -----------------------------------------------------------------------------
# SECTION 4: Build Authorization Header
# -----------------------------------------------------------------------------
# Format: Tuned-HMAC {AccessKey}:{Signature}[:{Nonce}:{Timestamp}]
if nonce_needed:
auth_header_value = f"Tuned-HMAC {access_key}:{signature_hash}:{nonce}:{timestamp}"
else:
auth_header_value = f"Tuned-HMAC {access_key}:{signature_hash}"
# headers["Authorization"] = auth_header_value # attach to your outgoing request
# -----------------------------------------------------------------------------
# SECTION 5: Debug Logging
# -----------------------------------------------------------------------------
print("=== Tuned HMAC Auth Debug ===")
print("Method: ", method)
print("Resolved URL: ", resolved_url)
print("Encoded URI: ", encoded_uri)
print("Use Nonce: ", nonce_needed)
print("Nonce: ", nonce or "(none)")
print("Timestamp: ", timestamp or "(none)")
print("Signature Raw: ", signature_raw_data)
print("Signature Hash: ", signature_hash)
print("Authorization: ", auth_header_value)
C#
using System;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
namespace TunedGlobal.Tools.HmacSigning
{
/// <summary>
/// Computes the "Tuned-HMAC" Authorization header used by TunedConnect APIs.
/// Ported from a Postman pre-request script.
/// </summary>
public static class TunedHmacRequestSigner
{
// -----------------------------------------------------------------------
// SECTION 1: Credential Loading
// -----------------------------------------------------------------------
// Replace these with your own AccessKey and SecretKey before running this sample.
private const string AccessKey = "YourAccessKeyHere";
private const string SecretKey = "YourSecretKeyHere";
// -----------------------------------------------------------------------
// SECTION 2: Helper Functions
// -----------------------------------------------------------------------
/// <summary>
/// Encodes a URL the way JS encodeURIComponent does, but with lowercase % escapes.
/// This matches the encoding format expected by the API signature algorithm.
/// </summary>
public static string EncodeUriLowercase(string url)
{
string encoded = Uri.EscapeDataString(url);
return Regex.Replace(encoded, "%[0-9A-Fa-f]{2}", m => m.Value.ToLowerInvariant());
}
/// <summary>
/// Returns the raw request body string for POST/PUT/PATCH requests.
/// Returns an empty string for GET requests or requests with no body.
/// Assumes <paramref name="body"/> is already the raw payload string.
/// </summary>
public static string GetRawBody(string body)
{
return body ?? "";
}
/// <summary>
/// Computes an MD5 hash of the request body, returned as a Base64 string.
/// This is included in the signature for non-GET requests to ensure
/// the payload has not been tampered with.
/// </summary>
public static string ComputePayloadMd5Base64(string raw)
{
if (string.IsNullOrWhiteSpace(raw))
{
return "";
}
try
{
using (var md5 = MD5.Create())
{
byte[] hash = md5.ComputeHash(Encoding.UTF8.GetBytes(raw));
return Convert.ToBase64String(hash);
}
}
catch
{
return "";
}
}
/// <summary>
/// Normalizes accidental double slashes in the URL (preserves scheme://).
/// Assumes {{variables}} and :pathVariables have already been substituted.
/// </summary>
public static string ResolveFinalUrl(string url)
{
string resolved = Regex.Replace(url, "(?<!:)/{2,}", "/");
if (Regex.IsMatch(resolved, @"\{\{[^}]+\}\}"))
{
throw new InvalidOperationException("Unresolved variable in URL: " + resolved + ". Check that all variables are set.");
}
return resolved;
}
/// <summary>
/// Determines whether this request should include a nonce and timestamp.
/// Certain internal API paths (e.g. /api/v2/tconnect/) use a simplified
/// signature without a nonce.
/// </summary>
public static bool UseNonce(string url)
{
return !ResolveFinalUrl(url).Contains("/api/v2/tconnect/");
}
// -----------------------------------------------------------------------
// SECTION 3 + 4: Build the Signature and Authorization Header
// -----------------------------------------------------------------------
/// <summary>
/// Builds the "Tuned-HMAC" Authorization header value for the given request.
/// </summary>
public static string BuildAuthorizationHeader(string method, string url, string rawBody = null)
{
method = (method ?? "GET").ToUpperInvariant();
string resolvedUrl = ResolveFinalUrl(url);
string encodedUri = EncodeUriLowercase(resolvedUrl);
bool nonceNeeded = UseNonce(url);
string nonce = nonceNeeded ? Guid.NewGuid().ToString() : "";
string timestamp = nonceNeeded ? DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString() : "";
string signatureRawData;
if (method == "GET")
{
signatureRawData = AccessKey + method + encodedUri + (nonceNeeded ? nonce + timestamp : "");
}
else
{
string body = GetRawBody(rawBody);
string bodyMd5 = ComputePayloadMd5Base64(body);
signatureRawData = AccessKey + method + encodedUri + bodyMd5 + (nonceNeeded ? nonce + timestamp : "");
}
byte[] secretKeyBytes = Convert.FromBase64String(SecretKey);
string signatureHash;
using (var hmac = new HMACSHA256(secretKeyBytes))
{
byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(signatureRawData));
signatureHash = Convert.ToBase64String(hash);
}
string authHeaderValue = nonceNeeded
? $"Tuned-HMAC {AccessKey}:{signatureHash}:{nonce}:{timestamp}"
: $"Tuned-HMAC {AccessKey}:{signatureHash}";
// -------------------------------------------------------------------
// SECTION 5: Debug Logging
// -------------------------------------------------------------------
Console.WriteLine("=== Tuned HMAC Auth Debug ===");
Console.WriteLine("Method: " + method);
Console.WriteLine("Resolved URL: " + resolvedUrl);
Console.WriteLine("Encoded URI: " + encodedUri);
Console.WriteLine("Use Nonce: " + nonceNeeded);
Console.WriteLine("Nonce: " + (string.IsNullOrEmpty(nonce) ? "(none)" : nonce));
Console.WriteLine("Timestamp: " + (string.IsNullOrEmpty(timestamp) ? "(none)" : timestamp));
Console.WriteLine("Signature Raw: " + signatureRawData);
Console.WriteLine("Signature Hash: " + signatureHash);
Console.WriteLine("Authorization: " + authHeaderValue);
return authHeaderValue;
}
}
}
Was this section helpful?
On this page
- HMAC Authentication