Skip to Content
Webhooks & EventsSignature Verification

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

  1. VelaFlows computes an HMAC-SHA256 hash of the raw request body using your signing secret
  2. The hash is sent in the X-Webhook-Signature header, prefixed with sha256=
  3. Your server computes the same hash and compares it to the header value
  4. 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}), 200

Go

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