Authentication
Authentication: step-by-step
Mercury uses HMAC-SHA256 request signing to authenticate every API call. This guide walks through the signing process from first principles.
How it works
Every request includes a cryptographic signature that proves you hold the Secret Key without ever transmitting it. Mercury verifies the signature server-side. If it doesn't match, the request is rejected with 401 Unauthorized and no response body.
Your credentials
Sent in the x-api-key header and included in the signature. Identifies your distributor account. Not secret, but don't share publicly.
Used to generate the HMAC signature. Never transmitted. Kept server-side only. Treat like a password. Rotate immediately if compromised.
Building the string to sign
The string to sign joins these values with a newline character (\n), in this exact order. GET requests leave out the body.
Worked example
Signing a GET /api/v1/client request with these values. Use them to test your own implementation: if your code produces the same signature, your signing is correct.
(each value on its own line, joined with \n, no trailing newline):
iOJeZxEZ/zRZDiATS/+a8YDZsbSwyKrTXCGAxCzlvOI=
Code examples
Complete signing implementations. Each example builds the signature and makes an authenticated GET /api/v1/client request.
API_KEY="your-api-key"
SECRET_KEY="your-secret-key"
NONCE=$(uuidgen)
METHOD="GET"
REQUEST_PATH="/api/v1/client"
# Join with newlines (GET requests have no body line) and sign
STRING_TO_SIGN=$(printf '%s\n%s\n%s\n%s' "$API_KEY" "$METHOD" "$REQUEST_PATH" "$NONCE")
SIGNATURE=$(printf '%s' "$STRING_TO_SIGN" | openssl dgst -sha256 -hmac "$SECRET_KEY" -binary | base64)
curl -s -X GET \
"https://www.api.external.test.mercury.global${REQUEST_PATH}" \
-H "x-api-key: ${API_KEY}" \
-H "x-nonce: ${NONCE}" \
-H "Authorization: HMACSHA256 ${SIGNATURE}" \
-H "Content-Type: application/json" | jq .import base64, hashlib, hmac, uuid
import requests
API_KEY = "your-api-key"
SECRET_KEY = "your-secret-key"
BASE_URL = "https://www.api.external.test.mercury.global"
method = "GET"
path = "/api/v1/client"
nonce = str(uuid.uuid4())
# GET requests have no body line
string_to_sign = "\n".join([API_KEY, method, path, nonce])
signature = base64.b64encode(
hmac.new(SECRET_KEY.encode(), string_to_sign.encode(), hashlib.sha256).digest()
).decode()
response = requests.get(
f"{BASE_URL}{path}",
headers={
"x-api-key": API_KEY,
"x-nonce": nonce,
"Authorization": f"HMACSHA256 {signature}",
"Content-Type": "application/json",
},
)
print(response.json())const crypto = require("crypto");
const API_KEY = "your-api-key";
const SECRET_KEY = "your-secret-key";
const BASE_URL = "https://www.api.external.test.mercury.global";
const method = "GET";
const path = "/api/v1/client";
const nonce = crypto.randomUUID();
// GET requests have no body line
const stringToSign = [API_KEY, method, path, nonce].join("\n");
const signature = crypto
.createHmac("sha256", SECRET_KEY)
.update(stringToSign, "utf8")
.digest("base64");
fetch(`${BASE_URL}${path}`, {
headers: {
"x-api-key": API_KEY,
"x-nonce": nonce,
Authorization: `HMACSHA256 ${signature}`,
"Content-Type": "application/json",
},
})
.then((res) => res.json())
.then((data) => console.log(JSON.stringify(data, null, 2)));import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URI;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.UUID;
public class MercuryAuth {
public static void main(String[] args) throws Exception {
String apiKey = "your-api-key";
String secretKey = "your-secret-key";
String baseUrl = "https://www.api.external.test.mercury.global";
String method = "GET";
String path = "/api/v1/client";
String nonce = UUID.randomUUID().toString();
// GET requests have no body line
String stringToSign = String.join("\n", apiKey, method, path, nonce);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
String signature = Base64.getEncoder()
.encodeToString(mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8)));
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + path))
.header("x-api-key", apiKey)
.header("x-nonce", nonce)
.header("Authorization", "HMACSHA256 " + signature)
.header("Content-Type", "application/json")
.GET().build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
}
}using System.Security.Cryptography;
using System.Text;
var apiKey = "your-api-key";
var secretKey = "your-secret-key";
var baseUrl = "https://www.api.external.test.mercury.global";
var method = "GET";
var path = "/api/v1/client";
var nonce = Guid.NewGuid().ToString();
// GET requests have no body line
var stringToSign = string.Join("\n", apiKey, method, path, nonce);
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secretKey));
var signature = Convert.ToBase64String(
hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)));
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("x-api-key", apiKey);
client.DefaultRequestHeaders.Add("x-nonce", nonce);
client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", $"HMACSHA256 {signature}");
var response = await client.GetAsync($"{baseUrl}{path}");
Console.WriteLine(await response.Content.ReadAsStringAsync());Signing a POST request
For every method other than GET, the raw request body goes between the path and the nonce. The body must be the exact string you send, same key order, same whitespace.
To check your body handling: with the worked example credentials and nonce, POST /api/v1/client and the body below signs to D+claMkvDcCUVew4/wsG01bl4kRtuWRyRDbmHmSINcM=.
METHOD="POST"
REQUEST_PATH="/api/v1/client"
# Abbreviated body to show signing. See the Client onboarding guide for every required field.
BODY='{"externalReference":"EXT-001","clientType":"Limited Company","riskRating":"Low","expectedMonthlySpend":5000}'
NONCE=$(uuidgen)
# The body sits between the path and the nonce
STRING_TO_SIGN=$(printf '%s\n%s\n%s\n%s\n%s' "$API_KEY" "$METHOD" "$REQUEST_PATH" "$BODY" "$NONCE")
SIGNATURE=$(printf '%s' "$STRING_TO_SIGN" | openssl dgst -sha256 -hmac "$SECRET_KEY" -binary | base64)
curl -s -X POST \
"https://www.api.external.test.mercury.global${REQUEST_PATH}" \
-H "x-api-key: ${API_KEY}" \
-H "x-nonce: ${NONCE}" \
-H "Authorization: HMACSHA256 ${SIGNATURE}" \
-H "Content-Type: application/json" \
-d "$BODY" | jq .import json
method = "POST"
path = "/api/v1/client"
nonce = str(uuid.uuid4())
# Abbreviated body to show signing. See the Client onboarding guide for every required field.
body = json.dumps({
"externalReference": "EXT-001",
"clientType": "Limited Company",
"riskRating": "Low",
"expectedMonthlySpend": 5000
}, separators=(",", ":")) # compact JSON, must match what you send
# The body sits between the path and the nonce
string_to_sign = "\n".join([API_KEY, method, path, body, nonce])
signature = base64.b64encode(
hmac.new(SECRET_KEY.encode(), string_to_sign.encode(), hashlib.sha256).digest()
).decode()
response = requests.post(
f"{BASE_URL}{path}",
headers={
"x-api-key": API_KEY,
"x-nonce": nonce,
"Authorization": f"HMACSHA256 {signature}",
"Content-Type": "application/json",
},
data=body.encode("utf-8"), # send the exact string you signed (not json=)
)Common mistakes
Join the values with \n in exactly this order: apiKey, method, path, body (not for GET), nonce. No trailing newline, and no Windows line endings (\r\n).
The header value is HMACSHA256, a space, then the signature. Bearer is not accepted and returns 401.
GET requests have no body line: the string goes straight from path to nonce. Every other method keeps the body line, even when the body is empty, so the string contains path\n\nnonce.
The HMAC output must be Base64-encoded. Many HMAC libraries default to hex output. Make sure you're getting the raw bytes and converting to Base64.
Every request needs a unique nonce. Mercury records each nonce as soon as the request arrives, even if the signature then fails, so always generate a new one when you retry. Use UUID v4 for guaranteed uniqueness.
The body in your string to sign must be byte-for-byte identical to what you send. Watch for libraries that re-serialise JSON (changing key order or whitespace).
Security best practices
Updated about 2 hours ago
