Skip to content

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 NameTypeRequiredDescription
X-Api-KeystringYesThe merchant's public API Key.
X-TimestampstringYesThe 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-NoncestringYesA cryptographically random string between 16 and 64 characters. Deduplicated per API Key in Redis for 600 seconds.
X-SignaturestringYesThe computed HMAC-SHA256 signature formatted as a 64-character lowercase hex string.
Idempotency-KeystringRecommendedUnique 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: ​

  1. 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.
  2. GET Requests or Empty Bodies: For GET or requests without a body, RawRequestBody is 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:

  1. Header Validation: Checks presence and format of required headers.
  2. Timestamp Window: Validates that $| \text{Server Time} - \text{Timestamp} | \le 300\text{ seconds}$.
  3. 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.
  4. 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 ​

typescript
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();
}
python
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()
java
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();
    }
}
go
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)
}
bash
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"}'