Verify Webhook HMAC Signatures in Python

Create and verify HMAC-SHA256 signatures for webhook payloads using Python's hmac module, protecting against tampering.

Medium Python 3.9+ Aug 9, 2026 API design & gRPC 12 views 0 copies

Python code

22 lines
Python 3.9+
import hashlib
import hmac
import json

SECRET = b"super-secret-webhook-key"

def create_signature(payload: bytes) -> str:
    return hmac.new(SECRET, payload, hashlib.sha256).hexdigest()

def verify_signature(payload: bytes, signature: str) -> bool:
    expected = create_signature(payload)
    return hmac.compare_digest(expected, signature)

if __name__ == "__main__":
    event = {"action": "user.created", "user_id": 42}
    payload = json.dumps(event, sort_keys=True).encode()
    signature = create_signature(payload)

    print(f"Payload: {payload.decode()}")
    print(f"Signature: {signature}")
    print(f"Valid signature: {verify_signature(payload, signature)}")
    print(f"Tampered payload: {verify_signature(payload + b'x', signature)}")

Output

stdout
Payload: {"action": "user.created", "user_id": 42}
Signature: 5f6f0d5a4d9f0c8b1e2a3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f
Valid signature: True
Tampered payload: False

How it works

HMAC signatures ensure that the webhook payload hasn't been altered in transit. hmac.new computes the digest using SHA-256 and the shared secret, producing a fixed-length hexadecimal string. verify_signature recomputes the expected signature and compares it to the received one using hmac.compare_digest, which is constant-time and resistant to timing attacks. The signature changes if any byte of the payload changes, making tampering detectable.

Common mistakes

  • Using simple string comparison instead of hmac.compare_digest, which is vulnerable to timing attacks.
  • Not canonicalizing the payload (e.g., not using sort_keys=True) so signatures differ across byte orders.
  • Using the secret as a plain string instead of bytes, causing type errors.
  • Assuming the signature is valid if it is present; always verify both signature and payload freshness.

Variations

  1. Use `hashlib.sha256` directly with a key by prefixing, but HMAC is the standard for webhooks.
  2. Sign with the raw bytes of the request body exactly as received, without re-encoding in Python.

Real-world use cases

  • Verifying incoming webhook requests from Stripe, GitHub, or Slack to ensure payload integrity.
  • Signing outbound webhook notifications to consumers so they can validate authenticity.
  • Preventing replay attacks by combining HMAC verification with timestamp checks in your API gateway.

Sponsored

Run this sample

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

Open editor

More from API design & gRPC

Related tutorials and quizzes for this topic.