Skip to main content

Webhook Security

FLTR signs every webhook with HMAC-SHA256 to prove authenticity.

Signature Header

X-FLTR-Signature: <hex-encoded-signature>

Verification

Always verify signatures before processing webhooks.

Python

import hmac
import hashlib

def verify_webhook(secret, signature, body):
    expected = hmac.new(
        secret.encode(),
        body,
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature, expected)

# Usage
if not verify_webhook(WEBHOOK_SECRET, signature, request.body):
    return {"error": "Invalid signature"}, 401

JavaScript

const crypto = require('crypto');

function verifyWebhook(secret, signature, body) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(body)
    .digest('hex');

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

if (!verifyWebhook(WEBHOOK_SECRET, signature, body)) {
  return res.status(401).json({ error: 'Invalid signature' });
}

Go

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
)

func verifyWebhook(secret, signature string, body []byte) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(body)
    expected := hex.EncodeToString(mac.Sum(nil))

    return hmac.Equal([]byte(signature), []byte(expected))
}

Best Practices

Use HTTPS

FLTR only sends webhooks to HTTPS endpoints in production.

Verify Every Request

Never trust webhook payloads without signature verification.

Use Raw Body

Verify against the raw request body, not parsed JSON.
# ✅ Good
body = request.get_data()
verify_webhook(secret, signature, body)

# ❌ Bad
body = json.dumps(request.json)  # May not match

Rotate Secrets

Rotate webhook secrets periodically (every 90 days).

Return 200 Quickly

Process webhooks asynchronously:
@app.route('/webhooks', methods=['POST'])
def webhook():
    # Verify
    if not verify_webhook(...):
        return {}, 401

    # Queue for processing
    queue.enqueue(process_webhook, request.json)

    # Return immediately
    return {}, 200

Testing

Generate Test Signature

import hmac
import hashlib
import json

payload = {"event": "document.processed", "data": {}}
body = json.dumps(payload)

signature = hmac.new(
    b'whsec_test_secret',
    body.encode(),
    hashlib.sha256
).hexdigest()

print(f"X-FLTR-Signature: {signature}")

Send Test Webhook

curl -X POST http://localhost:5000/webhooks \
  -H "Content-Type: application/json" \
  -H "X-FLTR-Signature: abc123..." \
  -d '{"event":"document.processed","data":{}}'

Troubleshooting

Signature Mismatch

Common causes:
  1. Using parsed JSON instead of raw body
  2. Wrong secret
  3. Extra whitespace in body
  4. Character encoding issues

Debugging

Log both signatures:
print(f"Received: {signature}")
print(f"Expected: {expected}")
Compare byte-by-byte to find discrepancies.