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.
Signature format
Section titled “Signature format”The header uses a Stripe-style format:
X-Varda-Signature: t=1711411200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdt— Unix timestamp when the signature was generatedv1— HMAC-SHA256 hex digest
Verification steps
Section titled “Verification steps”- Extract the timestamp and signature from the header
- Construct the signed payload:
{timestamp}.{raw_body} - Compute HMAC-SHA256 using your webhook secret
- Compare the computed signature with
v1 - Optionally check the timestamp is recent (within 5 minutes)
Examples
Section titled “Examples”Node.js
Section titled “Node.js”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;}Python
Section titled “Python”import hmacimport hashlibimport 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 validrequire '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 < 300endReplay protection
Section titled “Replay protection”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.