Signature verification is the most important security measure for any webhook endpoint. Without it, anyone can send forged payloads to your server. This guide covers how HMAC-SHA256 works, with code examples and provider-specific details.
Your webhook URL is a publicly accessible HTTP endpoint. If an attacker discovers it, they can send fake payloads — triggering false payment confirmations, creating bogus accounts, or corrupting data. Signature verification proves that:
The provider and your server share a secret key. On each webhook delivery, the provider computes HMAC-SHA256(secret, request_body) and sends the result in a header. Your server recomputes the same hash and compares the two values.
import (
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"io"
"net/http"
)
func verifySignature(r *http.Request, secret string) bool {
body, _ := io.ReadAll(r.Body)
sig := r.Header.Get("X-Signature-256")
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
return subtle.ConstantTimeCompare(
[]byte(sig), []byte("sha256="+expected),
) == 1
}
const crypto = require('crypto');
function verifySignature(body, secret, sigHeader) {
const expected = 'sha256=' +
crypto.createHmac('sha256', secret)
.update(body, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(sigHeader),
Buffer.from(expected)
);
}
import hmac, hashlib
def verify_signature(body: bytes, secret: str, sig_header: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(sig_header, expected)
Header: Stripe-Signature. Format: t=timestamp,v1=signature. Stripe signs timestamp.body (concatenated with a dot). Use the official Stripe SDK's constructEvent() method for the safest implementation. Secret starts with whsec_.
Header: X-Hub-Signature-256. Format: sha256=hex_digest. Standard HMAC-SHA256 over the raw request body. The secret is configured per-webhook in repository or organization settings.
Header: X-Shopify-Hmac-Sha256. Format: Base64-encoded HMAC-SHA256 (not hex). This is a common source of bugs — make sure you Base64-encode your computed hash before comparing, not hex-encode it.
Do not use == for comparison. Naive string equality is vulnerable to timing attacks. Always use constant-time comparison: subtle.ConstantTimeCompare (Go), crypto.timingSafeEqual (Node.js), or hmac.compare_digest (Python).
Use the SDNBros Signature Generator to compute HMAC-SHA256 signatures for test payloads. Paste in your secret and payload body to generate the correct header value, then compare it against what your code produces.
SDNBros validates webhook signatures on every delivery and alerts you to verification failures in real-time.
Start Free →