VantageKitVantageKit Docs

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

HeaderDescription
X-VantageKit-Signaturesha256= followed by the hex-encoded HMAC-SHA256
X-VantageKit-TimestampUnix timestamp in seconds when the delivery was sent
X-VantageKit-EventEvent type (e.g., deal_room.viewed)
X-VantageKit-DeliveryUnique delivery ID (UUID)
User-AgentVantageKit-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

MistakeFix
Parsing JSON before getting the raw bodyUse express.raw() or read the raw request body — don't parse JSON first
Using string comparison for signaturesUse crypto.timingSafeEqual() or hmac.compare_digest() to prevent timing attacks
Forgetting the sha256= prefixThe signature header includes a sha256= prefix — strip it before comparing
Wrong encodingThe secret, timestamp, and body are all UTF-8 strings

On this page