# 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: 1. **Algorithm**: HMAC-SHA256 2. **Encoding**: Hexadecimal 3. **Format**: `sha256=` 4. **Header**: `Synqly-Signature` 5. **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 ```python 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 ```go 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 ```python 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 ```python 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 ```go 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 1. **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 2. **Missing signature header** - Check if webhooks are properly configured - Verify the `integrator_key` is set in webhook configuration 3. **Encoding issues** - Always use UTF-8 encoding for the secret - Use the raw bytes of the request body 4. **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 ### Debug Helper ```python 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` |