Skip to content

Signature Verification

Every webhook delivery includes an X-Varda-Signature header that you should verify to ensure the request is from Varda and hasn’t been tampered with.

The header uses a Stripe-style format:

X-Varda-Signature: t=1711411200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
  • t — Unix timestamp when the signature was generated
  • v1 — HMAC-SHA256 hex digest
  1. Extract the timestamp and signature from the header
  2. Construct the signed payload: {timestamp}.{raw_body}
  3. Compute HMAC-SHA256 using your webhook secret
  4. Compare the computed signature with v1
  5. Optionally check the timestamp is recent (within 5 minutes)
const crypto = require('crypto');
function verifyWebhook(rawBody, signatureHeader, secret) {
const parts = signatureHeader.split(',');
const timestamp = parts[0].split('=')[1];
const signature = parts[1].split('=')[1];
// Compute expected signature
const signedPayload = `${timestamp}.${rawBody}`;
const expected = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Constant-time comparison
const valid = crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expected, 'hex')
);
// Check timestamp (reject if older than 5 minutes)
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp);
if (age > 300) return false;
return valid;
}
import hmac
import hashlib
import time
def verify_webhook(raw_body: bytes, signature_header: str, secret: str) -> bool:
parts = dict(p.split('=', 1) for p in signature_header.split(','))
timestamp = parts['t']
signature = parts['v1']
# Compute expected signature
signed_payload = f"{timestamp}.{raw_body.decode()}"
expected = hmac.new(
secret.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
# Constant-time comparison
valid = hmac.compare_digest(signature, expected)
# Check timestamp (reject if older than 5 minutes)
age = int(time.time()) - int(timestamp)
if age > 300:
return False
return valid
require 'openssl'
def verify_webhook(raw_body, signature_header, secret)
parts = signature_header.split(',').map { |p| p.split('=', 2) }.to_h
timestamp = parts['t']
signature = parts['v1']
signed_payload = "#{timestamp}.#{raw_body}"
expected = OpenSSL::HMAC.hexdigest('SHA256', secret, signed_payload)
valid = ActiveSupport::SecurityUtils.secure_compare(signature, expected)
age = Time.now.to_i - timestamp.to_i
valid && age < 300
end

The timestamp binding prevents replay attacks. Always check that t is within an acceptable window (5 minutes recommended). Reject requests with stale timestamps even if the signature is valid.

Note: Your signing secret is shown only once when you create the webhook endpoint. Store it securely. If compromised, regenerate it from Settings > Webhooks.