Gensail uses HMAC signatures to secure webhook deliveries. This allows you to verify that webhook payloads genuinely originate from Gensail and haven't been tampered with.
When webhook authentication is enabled, every webhook request includes an X-Signature header containing a Stripe-style HMAC signature. Your server should verify this signature before processing the webhook payload.
The X-Signature header uses this format:
X-Signature: t=1734789600,v1=a3b2c1d4e5f6789abc123def456...| Component | Description |
|---|---|
t | Unix timestamp (seconds) when the signature was generated |
v1 | HMAC-SHA256 signature in hexadecimal format |
Gensail computes the signature using:
- Signed Payload:
{timestamp}.{json_body} - Algorithm: HMAC-SHA256
- Key: Your webhook secret (provided by Gensail)
signature = HMAC-SHA256(
key = webhook_secret,
message = "{timestamp}.{json_body}"
)To verify a webhook signature:
- Extract the timestamp (
t) and signature (v1) from theX-Signatureheader - Reconstruct the signed payload:
{timestamp}.{raw_request_body} - Compute your own HMAC-SHA256 using your webhook secret
- Compare your computed signature with the received
v1value - Check that the timestamp is within tolerance (recommended: 5 minutes)
import hmac
import hashlib
import time
def verify_webhook(payload_bytes: bytes, signature_header: str, secret: str) -> bool:
"""
Verify Gensail webhook signature.
Args:
payload_bytes: Raw request body (bytes)
signature_header: X-Signature header value
secret: Your webhook secret
Returns:
True if signature is valid, False otherwise
"""
try:
# Parse header: "t=123,v1=abc..."
parts = dict(p.split("=", 1) for p in signature_header.split(","))
timestamp = int(parts["t"])
received_sig = parts["v1"]
except (ValueError, KeyError):
return False # Invalid header format
# Check timestamp (5 minute tolerance)
if abs(time.time() - timestamp) > 300:
return False # Signature too old (replay protection)
# Compute expected signature
signed_payload = f"{timestamp}.{payload_bytes.decode('utf-8')}"
expected_sig = hmac.new(
secret.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
# Constant-time comparison (prevents timing attacks)
return hmac.compare_digest(received_sig, expected_sig)
# Flask example
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = "your_webhook_secret_here"
@app.route('/webhook', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Signature', '')
payload = request.get_data()
if not verify_webhook(payload, signature, WEBHOOK_SECRET):
return jsonify({'error': 'Invalid signature'}), 401
# Process the verified webhook
data = request.get_json()
# ... your processing logic ...
return jsonify({'status': 'received'}), 200const crypto = require('crypto');
function verifyWebhook(payloadString, signatureHeader, secret) {
try {
// Parse header: "t=123,v1=abc..."
const parts = Object.fromEntries(
signatureHeader.split(',').map(p => p.split('='))
);
const timestamp = parseInt(parts.t);
const receivedSig = parts.v1;
// Check timestamp (5 minute tolerance)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > 300) {
return false; // Signature too old
}
// Compute expected signature
const signedPayload = `${timestamp}.${payloadString}`;
const expectedSig = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Constant-time comparison
return crypto.timingSafeEqual(
Buffer.from(receivedSig),
Buffer.from(expectedSig)
);
} catch (e) {
return false;
}
}
// Express example
const express = require('express');
const app = express();
const WEBHOOK_SECRET = 'your_webhook_secret_here';
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.headers['x-signature'] || '';
const payload = req.body.toString();
if (!verifyWebhook(payload, signature, WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process the verified webhook
const data = JSON.parse(payload);
// ... your processing logic ...
res.json({ status: 'received' });
});require 'openssl'
require 'json'
def verify_webhook(payload, signature_header, secret)
begin
# Parse header: "t=123,v1=abc..."
parts = signature_header.split(',').map { |p| p.split('=', 2) }.to_h
timestamp = parts['t'].to_i
received_sig = parts['v1']
# Check timestamp (5 minute tolerance)
return false if (Time.now.to_i - timestamp).abs > 300
# Compute expected signature
signed_payload = "#{timestamp}.#{payload}"
expected_sig = OpenSSL::HMAC.hexdigest('sha256', secret, signed_payload)
# Constant-time comparison
ActiveSupport::SecurityUtils.secure_compare(received_sig, expected_sig)
rescue
false
end
end
# Rails controller example
class WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token
WEBHOOK_SECRET = ENV['WEBHOOK_SECRET']
def receive
signature = request.headers['X-Signature']
payload = request.raw_post
unless verify_webhook(payload, signature, WEBHOOK_SECRET)
render json: { error: 'Invalid signature' }, status: :unauthorized
return
end
# Process the verified webhook
data = JSON.parse(payload)
# ... your processing logic ...
render json: { status: 'received' }
end
end<?php
function verifyWebhook(string $payload, string $signatureHeader, string $secret): bool {
// Parse header: "t=123,v1=abc..."
$parts = [];
foreach (explode(',', $signatureHeader) as $part) {
[$key, $value] = explode('=', $part, 2);
$parts[$key] = $value;
}
$timestamp = (int)($parts['t'] ?? 0);
$receivedSig = $parts['v1'] ?? '';
// Check timestamp (5 minute tolerance)
if (abs(time() - $timestamp) > 300) {
return false;
}
// Compute expected signature
$signedPayload = "{$timestamp}.{$payload}";
$expectedSig = hash_hmac('sha256', $signedPayload, $secret);
// Constant-time comparison
return hash_equals($receivedSig, $expectedSig);
}
// Usage example
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$secret = getenv('WEBHOOK_SECRET');
if (!verifyWebhook($payload, $signature, $secret)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
// Process the verified webhook
$data = json_decode($payload, true);
// ... your processing logic ...
echo json_encode(['status' => 'received']);Never process webhook payloads without verifying the signature first. This protects against:
- Spoofing: Attackers sending fake webhooks
- Tampering: Modified payloads in transit
- Replay attacks: Re-sending old webhooks (use timestamp validation)
Always use constant-time string comparison functions to prevent timing attacks:
- Python:
hmac.compare_digest() - Node.js:
crypto.timingSafeEqual() - Ruby:
ActiveSupport::SecurityUtils.secure_compare() - PHP:
hash_equals()
Reject signatures older than 5 minutes to prevent replay attacks. This is especially important if an attacker captures a valid webhook.
- Use environment variables, not hardcoded values
- Never commit secrets to version control
- Rotate secrets periodically
Always use HTTPS for your webhook endpoint to encrypt data in transit.
- Check the secret: Ensure you're using the correct webhook secret
- Raw body: Use the raw request body, not parsed JSON
- Encoding: Ensure UTF-8 encoding for both payload and secret
- No modifications: Don't modify the payload before verification
- Server time: Ensure your server's clock is synchronized (use NTP)
- Tolerance: Consider increasing tolerance if network latency is high
- Timestamp format: The
tvalue is Unix seconds, not milliseconds
- Configuration: Verify webhook authentication is enabled for your workspace
- Contact support: Ensure your webhook secret is properly configured
Your webhook secret is provided when your workspace is configured. Contact your Gensail administrator or check your workspace settings in the Gensail platform.
If you need to rotate your webhook secret, contact support@gensail.com.
To test your webhook verification locally:
# Generate a test signature
SECRET="your_webhook_secret"
TIMESTAMP=$(date +%s)
PAYLOAD='{"test": "data"}'
SIGNATURE=$(echo -n "${TIMESTAMP}.${PAYLOAD}" | openssl dgst -sha256 -hmac "$SECRET" | cut -d' ' -f2)
# Send test webhook
curl -X POST http://localhost:3000/webhook \
-H "Content-Type: application/json" \
-H "X-Signature: t=${TIMESTAMP},v1=${SIGNATURE}" \
-d "${PAYLOAD}"For questions about webhook authentication, contact support@gensail.com.