Webhook Security

Verify webhook signatures with HMAC-SHA256 to ensure payloads are authentic


Signature Verification

Every webhook delivery includes an X-Formfex-Signature header containing an HMAC-SHA256 signature. You should always verify this signature before processing the payload to ensure it was sent by Formfex and hasn't been tampered with.

How Signing Works

  1. Formfex takes the raw JSON request body
  2. Computes an HMAC-SHA256 hash using your webhook's signing secret as the key
  3. Prefixes the hex-encoded hash with sha256= and sends it in the X-Formfex-Signature header (e.g. sha256=abc123...)

Verification Examples

Node.js

javascript
import crypto from "crypto";

function verifyWebhook(req, secret) {
  const signature = req.headers["x-formfex-signature"];
  const body = JSON.stringify(req.body);

  const expected = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(body)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

// Express example
app.post("/webhooks/formfex", (req, res) => {
  const secret = process.env.FORMFEX_WEBHOOK_SECRET;

  if (!verifyWebhook(req, secret)) {
    return res.status(401).send("Invalid signature");
  }

  const { type, data } = req.body;
  console.log(`Received event: ${type}`, data);

  res.status(200).send("OK");
});

Python

python
import hmac
import hashlib

def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature, expected)

# Flask example
@app.route("/webhooks/formfex", methods=["POST"])
def handle_webhook():
    signature = request.headers.get("X-Formfex-Signature")
    secret = os.environ["FORMFEX_WEBHOOK_SECRET"]

    if not verify_webhook(request.data, signature, secret):
        abort(401)

    event = request.json
    print(f"Received: {event['type']}")

    return "OK", 200

PHP

php
function verifyWebhook(string $payload, string $signature, string $secret): bool {
    $expected = 'sha256=' . hash_hmac('sha256', $payload, $secret);
    return hash_equals($expected, $signature);
}

// Usage
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_FORMFEX_SIGNATURE'] ?? '';
$secret = getenv('FORMFEX_WEBHOOK_SECRET');

if (!verifyWebhook($payload, $signature, $secret)) {
    http_response_code(401);
    exit('Invalid signature');
}

$event = json_decode($payload, true);
// Process event...

Use timing-safe comparison

Always use timing-safe comparison functions (crypto.timingSafeEqual, hmac.compare_digest, hash_equals) to prevent timing attacks.

Rotating Secrets

If you suspect your signing secret has been compromised, you can regenerate it from the Formfex dashboard or via the dashboard API (POST /webhooks/:id/regenerate-secret). After regeneration:

  1. Update the secret in your server's environment variables
  2. Existing deliveries in the retry queue will use the new secret

Best practices

  • Always verify signatures before processing payloads
  • Store secrets in environment variables, never in source code
  • Return a 2xx response within 10 seconds to avoid retries
  • Use idempotency keys (X-Webhook-Delivery-Id) to handle duplicate deliveries
  • Log failed verifications for monitoring