Signature Verification
When you configure a signing secret on your webhook subscription, VelaFlows includes an X-Webhook-Signature header with every delivery. Use this header to verify that the request genuinely came from VelaFlows and was not tampered with in transit.
How It Works
- VelaFlows computes an HMAC-SHA256 hash of the raw request body using your signing secret
- The hash is sent in the
X-Webhook-Signatureheader, prefixed withsha256= - Your server computes the same hash and compares it to the header value
- If they match, the request is authentic
Verification Examples
Node.js
const crypto = require('crypto');
function verifyWebhookSignature(req, secret) {
const signature = req.headers['x-webhook-signature'];
if (!signature) {
return false;
}
const expectedSignature = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(req.rawBody) // Use the raw body, not parsed JSON
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Express middleware example
app.post('/webhooks/velaflows', express.raw({ type: 'application/json' }), (req, res) => {
const isValid = verifyWebhookSignature(req, 'whsec_your_signing_secret');
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(req.body);
// Process the event...
res.status(200).json({ received: true });
});Python
import hmac
import hashlib
def verify_webhook_signature(body: bytes, signature: str, secret: str) -> bool:
"""Verify the HMAC-SHA256 webhook signature."""
if not signature:
return False
expected = 'sha256=' + hmac.new(
secret.encode('utf-8'),
body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
# Flask example
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhooks/velaflows', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Webhook-Signature', '')
is_valid = verify_webhook_signature(
request.get_data(),
signature,
'whsec_your_signing_secret'
)
if not is_valid:
return jsonify({'error': 'Invalid signature'}), 401
event = request.get_json()
# Process the event...
return jsonify({'received': True}), 200Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
)
func verifySignature(body []byte, signature, secret string) bool {
if signature == "" {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(signature), []byte(expected))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
signature := r.Header.Get("X-Webhook-Signature")
if !verifySignature(body, signature, "whsec_your_signing_secret") {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Process the event...
fmt.Fprintf(w, `{"received": true}`)
}PHP
<?php
function verifyWebhookSignature(string $body, string $signature, string $secret): bool
{
if (empty($signature)) {
return false;
}
$expected = 'sha256=' . hash_hmac('sha256', $body, $secret);
return hash_equals($expected, $signature);
}
// Usage
$body = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$secret = 'whsec_your_signing_secret';
if (!verifyWebhookSignature($body, $signature, $secret)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
$event = json_decode($body, true);
// Process the event...
http_response_code(200);
echo json_encode(['received' => true]);Important Notes
Use the Raw Body
Always compute the HMAC from the raw request body (the exact bytes received), not from a re-serialized JSON object. JSON serialization can change formatting, whitespace, and key ordering, which changes the hash.
Use Timing-Safe Comparison
Always use a constant-time comparison function (crypto.timingSafeEqual, hmac.compare_digest, hash_equals) to prevent timing attacks. Do not use === or == for signature comparison.
Keep Secrets Secure
- Store the signing secret in environment variables, not in source code
- Rotate secrets periodically by updating the subscription with a new secret
- During rotation, accept both old and new secrets briefly to avoid rejecting valid deliveries
Handle Missing Signatures
If a subscription has no signing secret configured, the X-Webhook-Signature header will not be present. You should either:
- Always configure a secret (recommended)
- Reject requests with missing signatures if you expect them