Why Verify Signatures?
Webhook signatures ensure that the payload was sent by FoxReach and hasn’t been tampered with. Always verify signatures before processing webhook events.
How It Works
Each webhook has a unique secret that’s generated when the webhook is created. We use this secret to create an HMAC-SHA256 signature of the request body and include it in the X-Webhook-Signature header.
Verification Steps
Extract the signature
Get the X-Webhook-Signature header from the incoming request.
Compute the expected signature
Create an HMAC-SHA256 hash of the raw request body using your webhook secret.
Compare
Use a constant-time comparison to check if the signatures match.
Code Examples
Python
import hmac
import hashlib
def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode("utf-8"),
msg=payload,
digestmod=hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
# In your webhook handler (Flask example)
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = "your_webhook_secret"
@app.post("/webhooks/outreach")
def handle_webhook():
signature = request.headers.get("X-Webhook-Signature", "")
if not verify_webhook(request.data, signature, WEBHOOK_SECRET):
abort(401, "Invalid signature")
event = request.json
# Process the event...
return "", 200
Node.js
const crypto = require("crypto");
function verifyWebhook(payload, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
// Express example
app.post("/webhooks/outreach", (req, res) => {
const signature = req.headers["x-webhook-signature"] || "";
const rawBody = JSON.stringify(req.body);
if (!verifyWebhook(rawBody, signature, WEBHOOK_SECRET)) {
return res.status(401).send("Invalid signature");
}
const event = req.body;
// Process the event...
res.sendStatus(200);
});
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
)
func verifyWebhook(payload []byte, signature, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(payload)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signature))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
signature := r.Header.Get("X-Webhook-Signature")
if !verifyWebhook(body, signature, "your_secret") {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Process the event...
w.WriteHeader(http.StatusOK)
}
Always use constant-time comparison (like hmac.compare_digest in Python or crypto.timingSafeEqual in Node.js) to prevent timing attacks.