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=<hex_signature>
  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

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

  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

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

HeaderDescriptionExample
Synqly-SignatureHMAC-SHA256 signature in hex formatsha256=a1b2c3d4...
Content-TypeAlways application/json; charset=utf-8application/json; charset=utf-8