Verify Webhook Signatures
Copy-paste verification code for Node.js, Python, and Go to secure your webhook endpoint.
Every webhook delivery is signed with HMAC-SHA256 using your endpoint's signing secret. Always verify signatures before processing events to ensure the request came from VantageKit.
How signing works
VantageKit signs the webhook payload by computing:
HMAC-SHA256(secret, "${timestamp}.${body}")The signature is sent in the X-VantageKit-Signature header as sha256={hex_digest}.
Headers sent with every delivery
| Header | Description |
|---|---|
X-VantageKit-Signature | sha256= followed by the hex-encoded HMAC-SHA256 |
X-VantageKit-Timestamp | Unix timestamp in seconds when the delivery was sent |
X-VantageKit-Event | Event type (e.g., deal_room.viewed) |
X-VantageKit-Delivery | Unique delivery ID (UUID) |
User-Agent | VantageKit-Webhooks/1.0 |
Verification code
import crypto from 'node:crypto'
function verifyWebhookSignature(secret, signature, timestamp, body) {
// 1. Strip the "sha256=" prefix
const receivedSig = signature.replace('sha256=', '')
// 2. Compute the expected signature
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${body}`)
.digest('hex')
// 3. Use timing-safe comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(receivedSig),
Buffer.from(expected)
)
}import hashlib
import hmac
def verify_webhook_signature(secret: str, signature: str, timestamp: str, body: str) -> bool:
# Strip the "sha256=" prefix
received_sig = signature.removeprefix("sha256=")
# Compute the expected signature
expected = hmac.new(
secret.encode("utf-8"),
f"{timestamp}.{body}".encode("utf-8"),
hashlib.sha256,
).hexdigest()
# Timing-safe comparison
return hmac.compare_digest(received_sig, expected)package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"
)
func verifyWebhookSignature(secret, signature, timestamp, body string) bool {
// Strip the "sha256=" prefix
receivedSig := strings.TrimPrefix(signature, "sha256=")
// Compute the expected signature
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(fmt.Sprintf("%s.%s", timestamp, body)))
expected := hex.EncodeToString(mac.Sum(nil))
// Timing-safe comparison
return hmac.Equal([]byte(receivedSig), []byte(expected))
}Prevent replay attacks
Always check the timestamp to reject stale deliveries:
const MAX_AGE_SECONDS = 300 // 5 minutes
const timestamp = request.headers['x-vantagekit-timestamp']
const age = Math.floor(Date.now() / 1000) - Number(timestamp)
if (age > MAX_AGE_SECONDS) {
return response.status(401).send('Timestamp too old')
}Common mistakes
| Mistake | Fix |
|---|---|
| Parsing JSON before getting the raw body | Use express.raw() or read the raw request body — don't parse JSON first |
| Using string comparison for signatures | Use crypto.timingSafeEqual() or hmac.compare_digest() to prevent timing attacks |
Forgetting the sha256= prefix | The signature header includes a sha256= prefix — strip it before comparing |
| Wrong encoding | The secret, timestamp, and body are all UTF-8 strings |