Signature Verification
How to check webhook platform signatures with HMAC-SHA256 over the raw body and timestamp, with examples of working in Go, Python, and JavaScript.
Each webhook platform includes a HMAC signature. Check it before you analyze the body or perform side effects. Signature verification proves that the usage load and timestamp were produced with the subscription signature secret; a timestamp checks freshness limits replay attacks.
How it works
Posts Tagged ‘how it works’the platform signs each webhook payload using HMAC-SHA256 with the signature secret from your webhook subscription. The signature is sent to the header X-Probo-Webhook-Signature.
The signed message is the concatenation of the timestamp and the gross application body, separated by a colon:
{timestamp}:{body}Where:
timestampis the value in the headerX-Probo-Webhook-Timestamp(Unix second)bodyis the raw JSON request body
Use the full signing secret string (including the prefix whsec_) as the HMAC key.
Verification steps
Section entitled “Verification steps”-
Extract the headers
Read
X-Probo-Webhook-TimestampandX-Probo-Webhook-Signaturein the application. -
Build the signed message
Connect the timestamp, a colon (
:) and the gross request body. -
Compute the expected signature
Calculate
HMAC-SHA256using the full signature secret (including the prefixwhsec_) as the key and the signed message as the input. -
Compare signatures
Use a constant time comparison to check if the calculated signature matches the
X-Probo-Webhook-Signatureheader. -
Check timestamp freshness
Once the signature matches, reject the request if its timestamp is more than 5 minutes in the past or in the future.
Examples
Section entitled “Examples”package main
import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "fmt" "io" "net/http" "strconv" "time")
func verifyWebhook(r *http.Request, signingSecret string) ([]byte, error) { body, err := io.ReadAll(r.Body) if err != nil { return nil, err }
timestamp := r.Header.Get("X-Probo-Webhook-Timestamp") signature := r.Header.Get("X-Probo-Webhook-Signature") if timestamp == "" || signature == "" { return nil, fmt.Errorf("missing signature headers") }
mac := hmac.New(sha256.New, []byte(signingSecret)) mac.Write([]byte(timestamp)) mac.Write([]byte(":")) mac.Write(body)
received, err := hex.DecodeString(signature) if err != nil || !hmac.Equal(mac.Sum(nil), received) { return nil, fmt.Errorf("invalid signature") }
signedAt, err := strconv.ParseInt(timestamp, 10, 64) if err != nil { return nil, fmt.Errorf("invalid timestamp") } delta := time.Now().Unix() - signedAt if delta > 300 || delta < -300 { return nil, fmt.Errorf("stale timestamp") }
return body, nil}import hashlibimport hmacimport reimport time
def verify_webhook( body: bytes, timestamp: str | None, signature: str | None, signing_secret: str,) -> bool: if ( timestamp is None or signature is None or re.fullmatch(r"[0-9]+", timestamp) is None ): return False
expected = hmac.new( signing_secret.encode(), timestamp.encode("ascii") + b":" + body, hashlib.sha256, ).digest()
if re.fullmatch(r"[0-9a-fA-F]{64}", signature) is None: return False if not hmac.compare_digest(expected, bytes.fromhex(signature)): return False
signed_at = int(timestamp) return abs(time.time() - signed_at) <= 300import { createHmac, timingSafeEqual } from "node:crypto";
function verifyWebhook(rawBody, timestamp, signature, signingSecret) { if ( !Buffer.isBuffer(rawBody) || typeof timestamp !== "string" || typeof signature !== "string" || !/^[0-9]+$/.test(timestamp) || !/^[0-9a-fA-F]{64}$/.test(signature) ) { return false; }
const expected = createHmac("sha256", signingSecret) .update(timestamp) .update(":") .update(rawBody) .digest(); const received = Buffer.from(signature, "hex");
if ( expected.length !== received.length || !timingSafeEqual(expected, received) ) { return false; }
const signedAt = Number(timestamp); return ( Number.isFinite(signedAt) && Math.abs(Date.now() / 1000 - signedAt) <= 300 );}Security recommendations
Section “Security Recommendations”- Check your signature before you analyze JSON, authorize your organization, or sequence work.
- Reject missing, deformed, outdated and dated timestamps in the future. Examples use a 5-minute tolerance.
- First check their length, where the comparison requires entries of equal length.
- Keep a separate secret for each subscription and store it in a secret manager.
- Return a general answer
400or403. Do not disclose which verification failed. - It records
eventIdafter checking and processes it once. timestamp validation limits playtime; idempotency prevents duplicate side effects.
Troubleshooting
Section “Troubleshooting”| Symptom | Likely cause |
|---|---|
| Every signature fails | The frame breaks or changes the body before checking |
| Only non-ASCII payloads fail | The receiver decoded and re-coded the body instead of hashing raw bytes |
timingSafeEqual throws |
The received signature was not first validated as 32-byte hexadecimal |
| Valid deliveries are almost as stands | The receiver clock is not synchronized or the timestamp has been treated as milliseconds |
| Verification works with only one subscription | The end point is selecting the wrong subscription secret |