Skip to main content

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

1

Extract the signature

Get the X-Webhook-Signature header from the incoming request.
2

Compute the expected signature

Create an HMAC-SHA256 hash of the raw request body using your webhook secret.
3

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);
});

Go

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.