Signature Verification
Every FLUID webhook includes an HMAC-SHA256 signature that allows you to verify the webhook originated from FLUID and hasn't been tampered with. Always verify signatures to protect your webhook endpoint from spoofed requests.
Why Verify Signatures?
Without signature verification, your webhook endpoint is vulnerable to:
- Spoofed Requests: Attackers sending fake webhooks to trigger unauthorized actions
- Man-in-the-Middle Attacks: Modified webhook data during transmission
- Replay Attacks: Old webhooks being replayed to cause duplicate actions
Security Critical: Never trust webhook data without signature verification. Always validate signatures before processing webhook events or updating your database.
How Signature Verification Works
Webhook Headers
Every webhook includes these security headers:
| Header | Description | Example |
|---|---|---|
X-FLUID-Signature | HMAC-SHA256 signature (hex-encoded) | a3f8b7c2d1e9f4g5... |
X-FLUID-Event | Event type | transaction.completed |
X-FLUID-Delivery-ID | Unique delivery identifier | 12345 |
X-FLUID-Timestamp | Unix timestamp (seconds) | 1748793600 |
Signature Generation Algorithm
FLUID generates signatures using this algorithm:
- Serialize Payload: Convert webhook payload to JSON string (minified, no extra spaces)
- Compute HMAC: Generate HMAC-SHA256 hash using webhook secret
- Hex Encode: Convert binary hash to lowercase hexadecimal string
- Include in Header: Send as
X-FLUID-Signatureheader
Pseudo-code
payload_json = JSON.stringify(payload) // No pretty-printing
signature = HMAC-SHA256(payload_json, webhook_secret)
hex_signature = hex_encode(signature).lowercase()Implementation Guide
Signature Verification Steps
- Extract Headers: Get
X-FLUID-Signaturefrom request headers - Get Raw Body: Retrieve the raw request body before JSON parsing
- Compute HMAC: Calculate HMAC-SHA256 hash using raw body and your webhook secret
- Compare Securely: Use constant-time comparison to check if signatures match
- Process or Reject: If signatures match, process the webhook; otherwise return 401 Unauthorized
Key Implementation Requirements
Use Raw Request Body: You must use the raw request body (before JSON parsing) for signature verification. JSON parsing may reorder keys or change whitespace, causing signature mismatch.
Use Constant-Time Comparison: Always use timing-safe comparison functions to prevent timing attacks:
- Node.js:
crypto.timingSafeEqual() - Python:
hmac.compare_digest() - PHP:
hash_equals() - Ruby:
ActiveSupport::SecurityUtils.secure_compare()
Store Secret Securely: Never hardcode webhook secrets in your code. Always use environment variables or secure configuration management.
Testing Signature Verification
Generate Test Signature
Use this bash script to generate test signatures for your webhook endpoint:
#!/bin/bash
# generate-signature.sh
WEBHOOK_SECRET="your_webhook_secret_here"
PAYLOAD='{"event":"transaction.completed","event_id":"evt_test123","timestamp":"2025-01-28T10:00:00Z","api_version":"v1","data":{"transaction":{"id":"dt_12345","status":"completed"}}}'
# Generate signature
SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | sed 's/^.* //')
echo "Payload: $PAYLOAD"
echo ""
echo "Signature: $SIGNATURE"
echo ""
echo "Test webhook with:"
echo "curl -X POST http://localhost:3000/webhooks/fluid \\"
echo " -H 'Content-Type: application/json' \\"
echo " -H 'X-FLUID-Signature: $SIGNATURE' \\"
echo " -H 'X-FLUID-Event: transaction.completed' \\"
echo " -d '$PAYLOAD'"Manual Testing
# 1. Generate signature
echo -n '{"event":"transaction.completed","data":{"transaction":{"id":"dt_12345"}}}' | \
openssl dgst -sha256 -hmac "your_secret" | \
sed 's/^.* //'
# Output: a3f8b7c2d1e9f4g5h6i7j8k9l0m1n2o3...
# 2. Send test webhook
curl -X POST http://localhost:3000/webhooks/fluid \
-H "Content-Type: application/json" \
-H "X-FLUID-Signature: a3f8b7c2d1e9f4g5h6i7j8k9l0m1n2o3..." \
-H "X-FLUID-Event: transaction.completed" \
-d '{"event":"transaction.completed","data":{"transaction":{"id":"dt_12345"}}}'
# Expected: 200 OK (if signature valid)
# Expected: 401 Unauthorized (if signature invalid)Troubleshooting
Signature Verification Fails
1. Check Raw Body Usage
Problem: Using parsed JSON body instead of raw body.
Solution: Ensure you're computing signature on raw request body before any JSON parsing occurs. Store the raw body buffer when configuring your HTTP framework's body parser.
2. Check Secret
Problem: Using wrong webhook secret.
Solution: Verify you're using the correct secret provided by FLUID. Check your environment variables match the secret from your FLUID dashboard.
3. Check Comparison Method
Problem: Using non-constant-time comparison.
Solution: Use timing-safe comparison function provided by your language's crypto library (timingSafeEqual, compare_digest, hash_equals, secure_compare).
4. Check Charset Encoding
Problem: Incorrect string encoding (UTF-16 vs UTF-8).
Solution: Ensure UTF-8 encoding when converting buffer to string. Most languages default to UTF-8, but verify your framework configuration.
Common Errors
| Error | Cause | Solution |
|---|---|---|
Invalid signature | Wrong secret or body mismatch | Verify secret; use raw body |
Missing signature header | Header not extracted correctly | Check header name case-sensitivity |
TypeError: timingSafeEqual | Buffer length mismatch | Ensure both arguments are same length |
Signature undefined | Header parsing issue | Check for X-FLUID-Signature (not X_FLUID_SIGNATURE) |
Additional Security Measures
1. Timestamp Validation
Reject old webhook requests to prevent replay attacks. Check the X-FLUID-Timestamp header and reject webhooks older than 5 minutes. Calculate the absolute difference between the webhook timestamp and current time, and reject if greater than your threshold.
2. Idempotency Check
Prevent duplicate processing with event ID tracking. Store processed event IDs in a database or cache. Before processing a webhook, check if the event_id has already been processed. If yes, return 200 OK but skip processing. If no, process the webhook and store the event ID.
3. IP Whitelisting (Optional)
Restrict webhook requests to FLUID's IP addresses. Extract the client IP from the request and compare against a whitelist of FLUID IPs. Return 403 Forbidden if the IP is not authorized.
Contact Support for IPs: Contact your FLUID integration manager for the current list of webhook source IP addresses.
Security Checklist
Before going live, verify:
- ✅ Signature verification implemented
- ✅ Using raw request body (not parsed JSON)
- ✅ Using constant-time comparison
- ✅ Webhook secret stored securely (environment variable)
- ✅ HTTPS enabled on webhook endpoint
- ✅ Timestamp validation implemented (optional but recommended)
- ✅ Event ID deduplication implemented
- ✅ Proper error handling (return 401 for invalid signatures)
- ✅ Webhook endpoint tested with valid and invalid signatures
- ✅ Monitoring/logging configured for failed verifications
Next Steps
See Also
- Webhooks Overview - Introduction to webhooks
- Payload Structure - Complete payload reference
- HMAC Authentication - HMAC authentication for API requests