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
- Formfex takes the raw JSON request body
- Computes an HMAC-SHA256 hash using your webhook's signing secret as the key
- Prefixes the hex-encoded hash with
sha256=and sends it in theX-Formfex-Signatureheader (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:
- Update the secret in your server's environment variables
- 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