To verify webhook authenticity, each request includes security headers that allow you to validate the request came from Featurebase.
| Header | Description |
|---|---|
X-Webhook-Signature | HMAC-SHA256 signature of the request |
X-Webhook-Timestamp | Unix timestamp when the request was sent |
The signature is generated by:
- Combining the timestamp and raw request body with a dot (
.) separator - Creating an HMAC-SHA256 hash using your webhook secret
Your webhook secret starts with whsec_ and can be found in your webhook settings or via the Webhooks API.
- Verify the webhook signature as soon as possible using HMAC-SHA256
- Ensure the timestamp is within a 5-minute window to prevent replay attacks
- Use constant-time comparison when checking signatures
- Return 200 OK immediately after verification
- Process the webhook payload asynchronously after responding
- Keep your webhook secret key safe and never commit it to source control
import hmac
import hashlib
import time
from fastapi import Request, HTTPException
WEBHOOK_SECRET = "whsec_..." # Load from environment variables
MAX_TIMESTAMP_DIFF = 300 # 5 minutes
async def verify_webhook_signature(request: Request) -> None:
"""Verify the webhook signature or raise HTTPException."""
try:
# Get signature and timestamp
signature = request.headers.get("X-Webhook-Signature")
timestamp = request.headers.get("X-Webhook-Timestamp")
if not signature or not timestamp:
raise HTTPException(401, "Missing signature headers")
# Verify timestamp freshness
timestamp_diff = abs(int(time.time()) - int(timestamp))
if timestamp_diff > MAX_TIMESTAMP_DIFF:
raise HTTPException(401, "Webhook timestamp too old")
# Get raw body
body = await request.body()
# Create signed payload
signed_payload = f"{timestamp}.{body.decode('utf-8')}"
# Calculate expected signature
expected = hmac.new(
WEBHOOK_SECRET.encode('utf-8'),
signed_payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
# Secure comparison
if not hmac.compare_digest(expected, signature):
raise HTTPException(401, "Invalid signature")
except ValueError:
raise HTTPException(401, "Invalid signature format")
# Usage in webhook handler:
@app.post("/webhooks/featurebase")
async def handle_webhook(request: Request):
await verify_webhook_signature(request)
# ... rest of handler codeconst crypto = require("crypto");
const WEBHOOK_SECRET = "whsec_..."; // Load from environment variables
const MAX_TIMESTAMP_DIFF = 300; // 5 minutes
function verifyWebhookSignature(req) {
const signature = req.headers["x-webhook-signature"];
const timestamp = req.headers["x-webhook-timestamp"];
if (!signature || !timestamp) {
throw new Error("Missing signature headers");
}
// Verify timestamp freshness
const timestampDiff = Math.abs(
Math.floor(Date.now() / 1000) - parseInt(timestamp)
);
if (timestampDiff > MAX_TIMESTAMP_DIFF) {
throw new Error("Webhook timestamp too old");
}
// Create signed payload
const signedPayload = `${timestamp}.${req.rawBody}`;
// Calculate expected signature
const expected = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(signedPayload)
.digest("hex");
// Secure comparison
if (
!crypto.timingSafeEqual(
Buffer.from(expected, "utf8"),
Buffer.from(signature, "utf8")
)
) {
throw new Error("Invalid signature");
}
}
// Usage in webhook handler:
router.post(
"/webhooks/featurebase",
express.raw({ type: "application/json" }),
(req, res) => {
try {
verifyWebhookSignature(req);
// ... rest of handler code
} catch (error) {
return res.status(401).json({ error: error.message });
}
}
);<?php
class WebhookSignatureVerifier
{
private const WEBHOOK_SECRET = 'whsec_...'; // Load from environment
private const MAX_TIMESTAMP_DIFF = 300; // 5 minutes
public static function verify(string $rawBody, array $headers): void
{
$signature = $headers['X-Webhook-Signature'] ?? null;
$timestamp = $headers['X-Webhook-Timestamp'] ?? null;
if (!$signature || !$timestamp) {
throw new InvalidArgumentException('Missing signature headers');
}
// Verify timestamp freshness
$timestampDiff = abs(time() - (int)$timestamp);
if ($timestampDiff > self::MAX_TIMESTAMP_DIFF) {
throw new InvalidArgumentException('Webhook timestamp too old');
}
// Create signed payload
$signedPayload = $timestamp . '.' . $rawBody;
// Calculate expected signature
$expected = hash_hmac(
'sha256',
$signedPayload,
self::WEBHOOK_SECRET
);
// Secure comparison
if (!hash_equals($expected, $signature)) {
throw new InvalidArgumentException('Invalid signature');
}
}
}
// Usage in webhook handler:
public function handle(Request $request)
{
try {
WebhookSignatureVerifier::verify(
$request->getContent(),
$request->headers->all()
);
// ... rest of handler code
} catch (InvalidArgumentException $e) {
return response()->json(
['error' => $e->getMessage()],
401
);
}
}# app/services/webhook_signature_verifier.rb
class WebhookSignatureVerifier
WEBHOOK_SECRET = ENV.fetch('WEBHOOK_SECRET')
MAX_TIMESTAMP_DIFF = 300 # 5 minutes
class InvalidSignature < StandardError; end
def self.verify!(raw_body:, headers:)
new(raw_body: raw_body, headers: headers).verify!
end
def initialize(raw_body:, headers:)
@raw_body = raw_body
@signature = headers['X-Webhook-Signature']
@timestamp = headers['X-Webhook-Timestamp']
end
def verify!
validate_presence!
validate_timestamp!
validate_signature!
end
private
def validate_presence!
unless @signature && @timestamp
raise InvalidSignature, 'Missing signature headers'
end
end
def validate_timestamp!
timestamp_diff = (Time.now.to_i - @timestamp.to_i).abs
if timestamp_diff > MAX_TIMESTAMP_DIFF
raise InvalidSignature, 'Webhook timestamp too old'
end
end
def validate_signature!
signed_payload = "#{@timestamp}.#{@raw_body}"
expected = OpenSSL::HMAC.hexdigest(
'SHA256',
WEBHOOK_SECRET,
signed_payload
)
unless Rack::Utils.secure_compare(expected, @signature)
raise InvalidSignature, 'Invalid signature'
end
end
end
# Usage in webhook handler:
def handle
WebhookSignatureVerifier.verify!(
raw_body: request.raw_post,
headers: request.headers
)
# ... rest of handler code
rescue WebhookSignatureVerifier::InvalidSignature => e
render json: { error: e.message }, status: :unauthorized
end@Component
public class WebhookSignatureVerifier {
private static final String WEBHOOK_SECRET = "whsec_..."; // Load from config
private static final int MAX_TIMESTAMP_DIFF = 300; // 5 minutes
public void verify(String rawBody, HttpHeaders headers)
throws WebhookSignatureException {
String signature = headers.getFirst("X-Webhook-Signature");
String timestamp = headers.getFirst("X-Webhook-Timestamp");
if (signature == null || timestamp == null) {
throw new WebhookSignatureException("Missing signature headers");
}
// Verify timestamp freshness
long timestampDiff = Math.abs(
Instant.now().getEpochSecond() - Long.parseLong(timestamp)
);
if (timestampDiff > MAX_TIMESTAMP_DIFF) {
throw new WebhookSignatureException("Webhook timestamp too old");
}
// Create signed payload
String signedPayload = timestamp + "." + rawBody;
// Calculate expected signature
String expected = calculateHmac(signedPayload);
// Secure comparison
if (!MessageDigest.isEqual(
expected.getBytes(StandardCharsets.UTF_8),
signature.getBytes(StandardCharsets.UTF_8)
)) {
throw new WebhookSignatureException("Invalid signature");
}
}
private String calculateHmac(String payload) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(
WEBHOOK_SECRET.getBytes(StandardCharsets.UTF_8),
"HmacSHA256"
);
mac.init(secretKey);
byte[] hmacData = mac.doFinal(
payload.getBytes(StandardCharsets.UTF_8)
);
return Hex.encodeHexString(hmacData);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new WebhookSignatureException("Error calculating signature", e);
}
}
}package webhooks
import (
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"fmt"
"io/ioutil"
"net/http"
"strconv"
"time"
)
const (
webhookSecret = "whsec_..." // Load from config
maxTimestampDiff = 300 // 5 minutes
)
type SignatureError struct {
Message string
}
func (e *SignatureError) Error() string {
return e.Message
}
func VerifyWebhookSignature(r *http.Request) error {
// Get headers
signature := r.Header.Get("X-Webhook-Signature")
timestamp := r.Header.Get("X-Webhook-Timestamp")
if signature == "" || timestamp == "" {
return &SignatureError{"Missing signature headers"}
}
// Verify timestamp freshness
ts, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil {
return &SignatureError{"Invalid timestamp format"}
}
timestampDiff := time.Now().Unix() - ts
if abs(timestampDiff) > maxTimestampDiff {
return &SignatureError{"Webhook timestamp too old"}
}
// Read body
body, err := ioutil.ReadAll(r.Body)
if err != nil {
return fmt.Errorf("error reading request body: %v", err)
}
r.Body.Close()
// Create signed payload
signedPayload := fmt.Sprintf("%s.%s", timestamp, string(body))
// Calculate expected signature
mac := hmac.New(sha256.New, []byte(webhookSecret))
mac.Write([]byte(signedPayload))
expected := hex.EncodeToString(mac.Sum(nil))
// Secure comparison
if subtle.ConstantTimeCompare(
[]byte(expected),
[]byte(signature),
) != 1 {
return &SignatureError{"Invalid signature"}
}
return nil
}
// Usage in webhook handler:
func (h *WebhookHandler) HandleWebhook(w http.ResponseWriter, r *http.Request) {
if err := VerifyWebhookSignature(r); err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
// ... rest of handler code
}public class WebhookSignatureVerifier
{
private const string WebhookSecret = "whsec_..."; // Load from config
private const int MaxTimestampDiff = 300; // 5 minutes
public static void Verify(string rawBody, IHeaderDictionary headers)
{
var signature = headers["X-Webhook-Signature"].ToString();
var timestamp = headers["X-Webhook-Timestamp"].ToString();
if (string.IsNullOrEmpty(signature) || string.IsNullOrEmpty(timestamp))
{
throw new WebhookSignatureException("Missing signature headers");
}
// Verify timestamp freshness
if (!long.TryParse(timestamp, out var ts))
{
throw new WebhookSignatureException("Invalid timestamp format");
}
var timestampDiff = Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - ts);
if (timestampDiff > MaxTimestampDiff)
{
throw new WebhookSignatureException("Webhook timestamp too old");
}
// Create signed payload
var signedPayload = $"{timestamp}.{rawBody}";
// Calculate expected signature
var expected = CalculateHmac(signedPayload);
// Secure comparison
if (!CryptographicOperations.FixedTimeEquals(
Convert.FromHexString(expected),
Convert.FromHexString(signature)))
{
throw new WebhookSignatureException("Invalid signature");
}
}
private static string CalculateHmac(string payload)
{
using var hmac = new HMACSHA256(
Encoding.UTF8.GetBytes(WebhookSecret)
);
var hash = hmac.ComputeHash(
Encoding.UTF8.GetBytes(payload)
);
return Convert.ToHexString(hash).ToLower();
}
}
// Usage in webhook handler:
[HttpPost("featurebase")]
public async Task<IActionResult> HandleWebhook()
{
try
{
using var reader = new StreamReader(Request.Body);
var rawBody = await reader.ReadToEndAsync();
WebhookSignatureVerifier.Verify(rawBody, Request.Headers);
// ... rest of handler code
}
catch (WebhookSignatureException ex)
{
return Unauthorized(new { error = ex.Message });
}
}