HMAC-SHA256 Authentication Standard

How to authenticate requests to the SmallPict API using cryptographic HMAC-SHA256 signatures.

HMAC-SHA256 Authentication Standard

SmallPict secures all API traffic using cryptographic HMAC-SHA256 request signing.

This standard guarantees:

  1. Payload Integrity: The request body and URI path cannot be altered in transit.
  2. Replay Attack Immunity: The timestamp drift window blocks old or intercepted requests.
  3. Zero Secret Exposure: Your secret key is never sent over the wire.

📋 Required HTTP Headers

Every authenticated API request must include the following 3 HTTP headers:

X-API-Key: sp_live_9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b
X-Timestamp: 1716301234
X-Signature: 3a9f8b2c4d6e8a0f1b3c5d7e9f2a4b6c8d0e1f3a5b7c9d1e3f5a7b9c1d3e5f7a
HeaderTypeDescription
X-API-KeyStringYour customer API key (e.g., sp_live_..., sp_test_..., sp_sdk_...)
X-TimestampInteger StringUnix epoch timestamp in seconds at the time of sending
X-SignatureHex StringHex-encoded HMAC-SHA256 signature generated with your secret key

🔐 Signature Algorithm

Step 1: Construct the Canonical String-To-Sign

Concatenate the HTTP method, path, timestamp, and SHA-256 hash of the request body with newline (\n) delimiters:

{HTTP_METHOD}\n{PATH}\n{TIMESTAMP}\n{BODY_SHA256_HEX}
  • {HTTP_METHOD}: Uppercase HTTP verb (e.g. POST, GET, DELETE).
  • {PATH}: Exact request URI path without query parameters (e.g. /v1/optimize, /v1/quota).
  • {TIMESTAMP}: Unix epoch timestamp in seconds (integer string).
  • {BODY_SHA256_HEX}: Hex-encoded SHA-256 hash of the raw request body. For GET requests or empty bodies, use the SHA-256 of an empty string: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.

Step 2: Compute the HMAC-SHA256 Hash

Signature = hex(HMAC_SHA256(SecretKey, StringToSign))

💻 Signature Implementation Examples

Node.js (TypeScript)

import crypto from 'node:crypto';

export function signRequest(
  method: string,
  path: string,
  secretKey: string,
  bodyString: string = ''
) {
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const bodyHash = crypto.createHash('sha256').update(bodyString).digest('hex');
  const stringToSign = `${method.toUpperCase()}\n${path}\n${timestamp}\n${bodyHash}`;
  const signature = crypto
    .createHmac('sha256', secretKey)
    .update(stringToSign)
    .digest('hex');

  return { timestamp, signature };
}

Python 3

import hmac
import hashlib
import time

def sign_request(method: str, path: str, secret_key: str, body_bytes: bytes = b"") -> tuple[str, str]:
    timestamp = str(int(time.time()))
    body_hash = hashlib.sha256(body_bytes).hexdigest()
    string_to_sign = f"{method.upper()}\n{path}\n{timestamp}\n{body_hash}"
    signature = hmac.new(
        secret_key.encode("utf-8"),
        string_to_sign.encode("utf-8"),
        hashlib.sha256
    ).hexdigest()
    return timestamp, signature

Go

package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"strconv"
	"time"
)

func SignRequest(method, path, secretKey string, body []byte) (timestamp string, signature string) {
	ts := strconv.FormatInt(time.Now().Unix(), 10)
	h := sha256.Sum256(body)
	bodyHash := hex.EncodeToString(h[:])
	stringToSign := fmt.Sprintf("%s\n%s\n%s\n%s", method, path, ts, bodyHash)

	mac := hmac.New(sha256.New, []byte(secretKey))
	mac.Write([]byte(stringToSign))
	return ts, hex.EncodeToString(mac.Sum(nil))
}

⏱️ Timestamp Drift Window

To prevent replay attacks where an eavesdropper captures and resends an authenticated request, SmallPict enforces a ±300-second (5-minute) drift window.

Requests with |server_time - X-Timestamp| > 300 will be rejected with 401 Unauthorized (ERR_TIMESTAMP_DRIFT). Ensure your server clocks are synchronized with NTP.