Webhook Signature Validation Guide
Overview
This guide explains how to validate webhook signatures sent by Synqly. When Synqly sends webhooks to your endpoints, it signs the payload using HMAC-SHA256 to ensure authenticity and integrity.
How Signatures Are Generated
Synqly uses the following process to sign webhook payloads:
- Algorithm: HMAC-SHA256
- Encoding: Hexadecimal
- Format:
sha256=<hex_signature>
- Header:
Synqly-Signature
- Secret: The
Integrator Key
configured in your webhook settings
Signature Validation Process
Step 1: Extract the Signature
The signature is sent in the Synqly-Signature
HTTP header and follows this format:
Synqly-Signature: sha256=a1b2c3d4e5f6...
Step 2: Prepare the Payload
Use the raw request body exactly as received. Do not modify, parse, or reformat the JSON payload.
Step 3: Generate Expected Signature
Using your Integrator Key
(webhook secret), generate the expected signature:
Python Example
import hashlib
import hmac
def validate_webhook_signature(payload_body, signature_header, integrator_key):
"""
Validate a webhook signature from Synqly
Args:
payload_body (bytes): Raw request body
signature_header (str): Value from Synqly-Signature header
integrator_key (str): Your webhook secret (Integrator Key)
Returns:
bool: True if signature is valid, False otherwise
"""
# Remove 'sha256=' prefix if present
received_signature = signature_header.replace('sha256=', '')
# Generate expected signature
expected_signature = hmac.new(
integrator_key.encode('utf-8'),
payload_body,
hashlib.sha256
).hexdigest()
# Use timing-safe comparison
return hmac.compare_digest(received_signature, expected_signature)
# Usage example
def handle_webhook(request):
payload_body = request.body # Raw bytes
signature_header = request.headers.get('Synqly-Signature')
integrator_key = 'your-integrator-key-here'
if not validate_webhook_signature(payload_body, signature_header, integrator_key):
return {'error': 'Invalid signature'}, 401
# Process webhook payload
return {'status': 'success'}, 200
Go Example
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"strings"
)
// ValidateWebhookSignature validates a webhook signature from Synqly
func ValidateWebhookSignature(payloadBody []byte, signatureHeader, integratorKey string) bool {
// Remove 'sha256=' prefix if present
receivedSignature := strings.TrimPrefix(signatureHeader, "sha256=")
// Generate expected signature
mac := hmac.New(sha256.New, []byte(integratorKey))
mac.Write(payloadBody)
expectedSignature := hex.EncodeToString(mac.Sum(nil))
// Use timing-safe comparison
return hmac.Equal([]byte(receivedSignature), []byte(expectedSignature))
}
// Usage example
func webhookHandler(w http.ResponseWriter, r *http.Request) {
payloadBody, _ := io.ReadAll(r.Body)
signatureHeader := r.Header.Get("Synqly-Signature")
integratorKey := "your-integrator-key-here"
if !ValidateWebhookSignature(payloadBody, signatureHeader, integratorKey) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Process webhook payload
w.WriteHeader(http.StatusOK)
}
Security Best Practices
1. Always Use Timing-Safe Comparison
Never use simple string comparison (==
or equals()
) to compare signatures. Use timing-safe comparison functions:
- Python:
hmac.compare_digest()
- Go:
hmac.Equal()
2. Validate Before Processing
Always validate the signature before processing the webhook payload.
3. Handle Missing Signatures
def validate_webhook_signature(payload_body, signature_header, integrator_key):
if not signature_header:
raise ValueError("Missing signature header")
if not signature_header.startswith('sha256='):
raise ValueError("Invalid signature format")
# Continue with validation...
4. Use Raw Request Body
Always use the raw, unmodified request body for signature validation. Do not:
- Parse JSON first
- Modify whitespace
- Change encoding
- Reformat the payload
5. Store Secrets Securely
- Store your integrator key securely and inject at runtime
- Never commit secrets to version control
- Rotate keys periodically
Integration Examples
Flask Example
from flask import Flask, request, jsonify
import os
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def handle_webhook():
payload_body = request.get_data() # Raw bytes
signature_header = request.headers.get('Synqly-Signature')
integrator_key = os.environ.get('INTEGRATOR_KEY')
if not validate_webhook_signature(payload_body, signature_header, integrator_key):
return jsonify({'error': 'Invalid signature'}), 401
# Parse JSON after validation
payload = request.get_json()
# Process webhook
print(f'Webhook received: {payload}')
return jsonify({'status': 'success'})
Go HTTP Server Example
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
)
func main() {
http.HandleFunc("/webhook", webhookHandler)
log.Println("Starting webhook server on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
payloadBody, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read body", http.StatusBadRequest)
return
}
signatureHeader := r.Header.Get("Synqly-Signature")
integrationKey := os.Getenv("INTEGRATOR_KEY")
if !ValidateWebhookSignature(payloadBody, signatureHeader, integrationKey) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Parse JSON after validation
var payload map[string]interface{}
if err := json.Unmarshal(payloadBody, &payload); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// Process webhook
fmt.Printf("Webhook received: %+v\n", payload)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
}
Testing Your Implementation
You can test your signature validation using the following test vector:
Payload: {"test": "data"}
Secret: "test-secret"
Expected Signature: sha256=4b04c13cf8b8fa3b993c8a7e6c9dc6e0eddb0b2cee7b468cf3ed6b4b6fdda1a5
Troubleshooting
Common Issues
Signature doesn't match
- Ensure you're using the raw request body
- Verify the correct
integrator_key
- Check that you're not modifying the payload
Missing signature header
- Check if webhooks are properly configured
- Verify the
integrator_key
is set in webhook configuration
Encoding issues
- Always use UTF-8 encoding for the secret
- Use the raw bytes of the request body
If you encounter issues with webhook signature validation
- Verify your
integrator_key
is correctly configured - Test with the provided test vectors
- Check your implementation against the examples above
- Ensure you're using timing-safe comparison functions
- Verify your
Debug Helper
def debug_signature_validation(payload_body, signature_header, integrator_key):
print(f"Payload length: {len(payload_body)}")
print(f"Payload (first 100 chars): {payload_body[:100]}")
print(f"Received signature: {signature_header}")
print(f"Secret length: {len(integrator_key)}")
# Generate expected signature
expected_signature = hmac.new(
integrator_key.encode('utf-8'),
payload_body,
hashlib.sha256
).hexdigest()
print(f"Expected signature: sha256={expected_signature}")
return validate_webhook_signature(payload_body, signature_header, integrator_key)
Webhook Headers Reference
Header | Description | Example |
---|---|---|
Synqly-Signature | HMAC-SHA256 signature in hex format | sha256=a1b2c3d4... |
Content-Type | Always application/json; charset=utf-8 | application/json; charset=utf-8 |