Skip to content

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