Authentication & HMAC Signatures
The SureLink Gateway uses industry-standard HMAC-SHA256 signature authentication to prevent message tampering, replay attacks, and unauthorized access. Every request made to /api/v1/* must include authenticated HTTP headers.
1. Authentication Headers
Merchants must provide the following headers in every HTTP request:
| Header Name | Type | Required | Description |
|---|---|---|---|
X-Api-Key | string | Yes | The merchant's public API Key. |
X-Timestamp | string | Yes | The millisecond Unix epoch timestamp of request creation (e.g., 1715000000000). The clock skew between client and server must be within $\pm 5$ minutes (300,000 ms). |
X-Nonce | string | Yes | A cryptographically random string between 16 and 64 characters. Deduplicated per API Key in Redis for 600 seconds. |
X-Signature | string | Yes | The computed HMAC-SHA256 signature formatted as a 64-character lowercase hex string. |
Idempotency-Key | string | Recommended | Unique key (e.g., UUID) to protect mutating requests from double-execution. Falls back to merchantOrderNo if omitted. |
Security Policy
Any authentication failure surfaces a generic 401 Unauthorized with { "errorCode": "MERCHANT_API_UNAUTHORIZED", "message": "unauthorized" }. The server never reveals whether the key, timestamp, nonce, or signature failed.
2. Signature Calculation
2.1 Signing Material (Source String)
The signing string is constructed by concatenating the Timestamp, Nonce, and Raw Request Body, separated by dots (.):
$$\text{Signing String} = \text{X-Timestamp} + \text{"."} + \text{X-Nonce} + \text{"."} + \text{RawRequestBody}$$
Important Guidelines:
- Raw Body Byte Exactness: Use the exact raw JSON byte string transmitted across the wire. Do not parse and re-serialize the JSON object, as differences in key sorting or whitespace will invalidate the signature.
- GET Requests or Empty Bodies: For
GETor requests without a body,RawRequestBodyis an empty string"". The signing string ends with a trailing dot: $$\text{Signing String} = \text{X-Timestamp} + \text{"."} + \text{X-Nonce} + \text{"."}$$
2.2 Computing the HMAC-SHA256 Digest
Calculate the HMAC-SHA256 digest using your API Secret as the key, and format the output as a lowercase hex string:
$$\text{X-Signature} = \operatorname{HMAC-SHA256}_{\text{API Secret}}(\text{Signing String}).\text{toHex()}$$
3. Replay Protection & Security Guards
The gateway applies layered defensive checks in order:
- Header Validation: Checks presence and format of required headers.
- Timestamp Window: Validates that $| \text{Server Time} - \text{Timestamp} | \le 300\text{ seconds}$.
- Redis Nonce Atomic Dedup: Uses Redis
SET hmac_nonce:${apiKey}:${nonce} 1 NX EX 600. Because the nonce TTL (10 minutes) exceeds the timestamp skew window (5 minutes), replay attacks are mathematically precluded. - IP Whitelist Enforcement: Validates that the request origin IP matches the merchant's configured whitelist. Mismatches return
403 Forbidden(IP_NOT_WHITELISTED).
4. Code Examples Across Languages
import { createHmac, randomBytes } from 'node:crypto';
interface RequestOptions {
apiKey: string;
apiSecret: string;
method: string;
url: string;
body?: Record<string, any>;
}
export async function sendSignedRequest({ apiKey, apiSecret, method, url, body }: RequestOptions) {
const timestamp = Date.now().toString();
// 16 bytes = 32 lowercase hex characters
const nonce = randomBytes(16).toString('hex');
const rawBody = body ? JSON.stringify(body) : '';
const source = `${timestamp}.${nonce}.${rawBody}`;
const signature = createHmac('sha256', apiSecret).update(source, 'utf8').digest('hex');
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'X-Api-Key': apiKey,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': signature,
};
if (method.toUpperCase() === 'POST' && body?.merchantOrderNo) {
headers['Idempotency-Key'] = body.merchantOrderNo;
}
const response = await fetch(url, {
method,
headers,
body: rawBody || undefined,
});
return response.json();
}import time
import secrets
import hmac
import hashlib
import json
import requests
def make_signed_request(api_key: str, api_secret: str, method: str, url: str, body: dict = None):
timestamp = str(int(time.time() * 1000))
nonce = secrets.token_hex(16)
raw_body = json.dumps(body, separators=(',', ':')) if body else ""
source = f"{timestamp}.{nonce}.{raw_body}"
signature = hmac.new(
api_secret.encode('utf-8'),
source.encode('utf-8'),
hashlib.sha256
).hexdigest()
headers = {
"Content-Type": "application/json",
"X-Api-Key": api_key,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature": signature
}
if method.upper() == "POST" and body and "merchantOrderNo" in body:
headers["Idempotency-Key"] = body["merchantOrderNo"]
resp = requests.request(method, url, headers=headers, data=raw_body.encode('utf-8') if raw_body else None)
return resp.json()import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
public class SureLinkSigner {
private static final String HMAC_SHA256 = "HmacSHA256";
public static String computeHmacHex(String secret, String message) throws Exception {
Mac mac = Mac.getInstance(HMAC_SHA256);
SecretKeySpec keySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HMAC_SHA256);
mac.init(keySpec);
byte[] rawHmac = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
StringBuilder hexString = new StringBuilder();
for (byte b : rawHmac) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) hexString.append('0');
hexString.append(hex);
}
return hexString.toString();
}
public static String generateNonce() {
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[16];
random.nextBytes(bytes);
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}package main
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"time"
)
func generateNonce() (string, error) {
bytes := make([]byte, 16)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return hex.EncodeToString(bytes), nil
}
func computeSignature(secret, timestamp, nonce, rawBody string) string {
source := fmt.Sprintf("%s.%s.%s", timestamp, nonce, rawBody)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(source))
return hex.EncodeToString(mac.Sum(nil))
}
func main() {
apiKey := "mch_key_demo123"
apiSecret := "sec_live_9876543210fedcba"
timestamp := fmt.Sprintf("%d", time.Now().UnixMilli())
nonce, _ := generateNonce()
rawBody := `{"merchantOrderNo":"MCH20261011001","assetAmount":"100.000000"}`
signature := computeSignature(apiSecret, timestamp, nonce, rawBody)
fmt.Printf("X-Signature: %s\n", signature)
}TIMESTAMP="1715000000000"
NONCE="9f8e7d6c5b4a392817263544a1b2c3d4"
SIGNATURE="5a228f237ef81d6fbb5d0e7403aefbb94adbb4a6435c44f77c3e1e24748c0827"
curl -X POST "https://api-sandbox.surelink.io/api/v1/orders" \
-H "Content-Type: application/json" \
-H "X-Api-Key: your_api_key_here" \
-H "X-Timestamp: $TIMESTAMP" \
-H "X-Nonce: $NONCE" \
-H "X-Signature: $SIGNATURE" \
-H "Idempotency-Key: MCH_ORDER_20261011_001" \
-d '{"merchantOrderNo":"MCH_ORDER_20261011_001","assetAmount":"100.000000"}'