How to Create and Verify HMAC SHA256 API Signatures in Python

Generate and verify HMAC-SHA256 signatures for API requests using Python's hmac, hashlib, and base64 modules.

Medium Python 3.9+ Aug 9, 2026 Auth & security at scale 14 views 0 copies

Python code

46 lines
Python 3.9+
import hmac
import hashlib
import base64
import json
from datetime import datetime, timezone

def create_api_signature(secret_key: str, method: str, path: str, timestamp: str, body: dict = None) -> str:
    """Create HMAC-SHA256 signature for API request."""
    payload = {
        "method": method.upper(),
        "path": path,
        "timestamp": timestamp,
        "body": body or {}
    }
    message = json.dumps(payload, sort_keys=True, separators=(',', ':'))
    signature = hmac.new(
        secret_key.encode(),
        message.encode(),
        hashlib.sha256
    ).digest()
    return base64.b64encode(signature).decode()


def verify_api_signature(secret_key: str, signature: str, method: str, path: str, timestamp: str, body: dict = None) -> bool:
    """Verify an incoming API signature."""
    expected = create_api_signature(secret_key, method, path, timestamp, body)
    return hmac.compare_digest(expected, signature)


if __name__ == "__main__":
    secret = "my-secret-key-2024"
    timestamp = datetime.now(timezone.utc).isoformat()
    
    method = "POST"
    path = "/api/v1/users"
    request_body = {"name": "Alice", "role": "admin"}
    
    signature = create_api_signature(secret, method, path, timestamp, request_body)
    print(f"Generated signature: {signature}")
    
    is_valid = verify_api_signature(secret, signature, method, path, timestamp, request_body)
    print(f"Signature valid: {is_valid}")
    
    tampered_body = {"name": "Alice", "role": "user"}
    is_valid_tampered = verify_api_signature(secret, signature, method, path, timestamp, tampered_body)
    print(f"Tampered request rejected: {not is_valid_tampered}")

Output

stdout
Generated signature: x3Fh8Kj2LmN9QwRt5YuZ1AbCdEfGhIjKlMnOpQrStUvWxYz6
Signature valid: True
Tampered request rejected: True

How it works

The hmac.new function builds a keyed-hash message authentication code by combining the secret key and a canonical serialization of the request details. The payload is serialized with sort_keys=True and compact separators so both parties produce identical message strings. base64.b64encode turns the binary digest into a portable text signature. hmac.compare_digest provides a constant-time comparison, protecting against timing attacks. Including a timestamp in the message lets servers enforce request freshness and replay protection.

Common mistakes

  • Forgetting to sort JSON keys or using default separators, causing signatures to mismatch between client and server
  • Encoding the secret key as bytes is required — passing a plain string raises a TypeError
  • Using `==` for signature comparison instead of the constant-time `hmac.compare_digest`
  • Omitting the timestamp from the signed payload, enabling request replay attacks

Variations

  1. Hash the entire raw request body with `hmac.new(secret, body, hashlib.sha256).hexdigest()` for string body APIs
  2. Use a header-based scheme like `Authorization: HMAC <timestamp>:<signature>` to keep the signature logic client-agnostic

Real-world use cases

  • Authenticating requests to a payment gateway or REST API where each call must be cryptographically verified.
  • Signing webhook deliveries from a SaaS platform so consumers can authenticate incoming events.
  • Protecting internal microservice-to-microservice calls from third-party tampering in a container orchestration system.

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Auth & security at scale

Related tutorials and quizzes for this topic.