Skip to main content

Signing Secrets

Signing Secrets enable you to verify that webhooks sent from Hooklistener to your services are authentic and haven't been tampered with. This security feature ensures your endpoints only process legitimate webhooks.

Overview

When Hooklistener delivers webhooks to your Destinations, it can sign each request with your Organization's signing secret. Your endpoint can then verify this signature to confirm the webhook came from Hooklistener.

Key features:

  • Webhook authenticity: Confirm webhooks are from Hooklistener
  • Tamper detection: Verify payload hasn't been modified
  • Replay protection: Prevent old webhooks from being replayed
  • Secret rotation: Update secrets without downtime
  • Industry standard: Uses HMAC-SHA256 signing

How Signing Works

The Signing Process

When Hooklistener sends a webhook to your Destination:

  1. Generate signature:

    • Combine timestamp + request body
    • Create HMAC-SHA256 hash using your signing secret
    • Format: t=timestamp,v1=signature
  2. Add to request:

    • Include signature in X-Hooklistener-Signature header
    • Send webhook to your endpoint
  3. Your endpoint verifies:

    • Extract timestamp and signature
    • Recreate signature using same method
    • Compare signatures
    • Check timestamp freshness

Signature Format

Hooklistener sends signatures in this format:

X-Hooklistener-Signature: t=1642262400,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

Components:

  • t= - Unix timestamp when signature was created
  • v1= - HMAC-SHA256 signature (hex encoded)

Setting Up Signing Secrets

Generating Your Signing Secret

Via Dashboard:

  1. Navigate to Organization Settings
  2. Go to Signing Secret section
  3. Click "Generate Signing Secret"
  4. Copy and store the secret securely
  5. Configure your endpoints to verify signatures

Via API:

curl -X POST https://api.hooklistener.com/api/v1/organizations/{org_id}/signing_secret \
-H "Authorization: Bearer YOUR_API_KEY"

Response:

{
"signing_secret": "whsec_abc123def456...",
"created_at": "2024-01-15T10:30:00Z"
}

Important: Save this secret securely - it's only shown once!

Storing Your Signing Secret

Store the signing secret securely:

For applications:

  • Environment variables
  • Secret management systems (AWS Secrets Manager, HashiCorp Vault)
  • Encrypted configuration files

Never:

  • Hard-code in source code
  • Commit to version control
  • Share via email or chat
  • Log in plain text

Verifying Signatures

Verification Steps

Your endpoint should verify every webhook:

  1. Extract signature from header
  2. Parse timestamp and signature
  3. Check timestamp freshness (within 5 minutes)
  4. Recreate signature using your secret
  5. Compare signatures (constant-time comparison)
  6. Accept or reject based on match

Implementation Examples

Node.js/JavaScript:

const crypto = require('crypto');

function verifyWebhookSignature(req, signingSecret) {
const signature = req.headers['x-hooklistener-signature'];
const body = JSON.stringify(req.body);

// Parse signature header
const elements = signature.split(',');
const timestamp = elements[0].split('=')[1];
const receivedSignature = elements[1].split('=')[1];

// Check timestamp (prevent replay attacks)
const currentTime = Math.floor(Date.now() / 1000);
if (Math.abs(currentTime - timestamp) > 300) { // 5 minutes
throw new Error('Webhook timestamp too old');
}

// Recreate signature
const signedPayload = `${timestamp}.${body}`;
const expectedSignature = crypto
.createHmac('sha256', signingSecret)
.update(signedPayload)
.digest('hex');

// Compare signatures (constant-time)
if (!crypto.timingSafeEqual(
Buffer.from(receivedSignature),
Buffer.from(expectedSignature)
)) {
throw new Error('Invalid webhook signature');
}

return true;
}

// Express.js middleware
app.post('/webhook', (req, res) => {
try {
verifyWebhookSignature(req, process.env.HOOKLISTENER_SIGNING_SECRET);
// Signature valid - process webhook
processWebhook(req.body);
res.status(200).send('OK');
} catch (error) {
console.error('Signature verification failed:', error.message);
res.status(401).send('Unauthorized');
}
});

Python:

import hmac
import hashlib
import time

def verify_webhook_signature(request, signing_secret):
# Get signature from header
signature_header = request.headers.get('X-Hooklistener-Signature', '')

# Parse components
elements = signature_header.split(',')
timestamp = elements[0].split('=')[1]
received_signature = elements[1].split('=')[1]

# Check timestamp freshness (5 minutes tolerance)
current_time = int(time.time())
if abs(current_time - int(timestamp)) > 300:
raise ValueError('Webhook timestamp too old')

# Recreate signature
signed_payload = f"{timestamp}.{request.data.decode('utf-8')}"
expected_signature = hmac.new(
signing_secret.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()

# Constant-time comparison
if not hmac.compare_digest(received_signature, expected_signature):
raise ValueError('Invalid webhook signature')

return True

# Flask example
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/webhook', methods=['POST'])
def webhook():
try:
verify_webhook_signature(
request,
os.environ['HOOKLISTENER_SIGNING_SECRET']
)
# Process webhook
data = request.get_json()
process_webhook(data)
return jsonify({'status': 'success'}), 200
except ValueError as e:
return jsonify({'error': str(e)}), 401

Ruby:

require 'openssl'
require 'json'

def verify_webhook_signature(request, signing_secret)
# Get signature header
signature_header = request.env['HTTP_X_HOOKLISTENER_SIGNATURE']

# Parse components
elements = signature_header.split(',')
timestamp = elements[0].split('=')[1]
received_signature = elements[1].split('=')[1]

# Check timestamp (5 minute tolerance)
current_time = Time.now.to_i
if (current_time - timestamp.to_i).abs > 300
raise 'Webhook timestamp too old'
end

# Recreate signature
signed_payload = "#{timestamp}.#{request.body.read}"
expected_signature = OpenSSL::HMAC.hexdigest(
'SHA256',
signing_secret,
signed_payload
)

# Constant-time comparison
unless Rack::Utils.secure_compare(received_signature, expected_signature)
raise 'Invalid webhook signature'
end

true
end

# Sinatra example
post '/webhook' do
begin
verify_webhook_signature(request, ENV['HOOKLISTENER_SIGNING_SECRET'])
# Process webhook
data = JSON.parse(request.body.read)
process_webhook(data)
status 200
rescue => e
status 401
{ error: e.message }.to_json
end
end

Go:

package main

import (
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
)

func verifyWebhookSignature(r *http.Request, signingSecret string) error {
// Get signature header
signatureHeader := r.Header.Get("X-Hooklistener-Signature")

// Parse components
parts := strings.Split(signatureHeader, ",")
timestamp := strings.Split(parts[0], "=")[1]
receivedSignature := strings.Split(parts[1], "=")[1]

// Check timestamp (5 minute tolerance)
ts, _ := strconv.ParseInt(timestamp, 10, 64)
currentTime := time.Now().Unix()
if currentTime - ts > 300 || ts - currentTime > 300 {
return fmt.Errorf("webhook timestamp too old")
}

// Read body
body, _ := io.ReadAll(r.Body)

// Recreate signature
signedPayload := fmt.Sprintf("%s.%s", timestamp, string(body))
h := hmac.New(sha256.New, []byte(signingSecret))
h.Write([]byte(signedPayload))
expectedSignature := hex.EncodeToString(h.Sum(nil))

// Constant-time comparison
if subtle.ConstantTimeCompare(
[]byte(receivedSignature),
[]byte(expectedSignature),
) != 1 {
return fmt.Errorf("invalid webhook signature")
}

return nil
}

func webhookHandler(w http.ResponseWriter, r *http.Request) {
signingSecret := os.Getenv("HOOKLISTENER_SIGNING_SECRET")

if err := verifyWebhookSignature(r, signingSecret); err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}

// Process webhook
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}

Secret Rotation

When to Rotate

Rotate your signing secret:

Regularly:

  • Every 90 days as best practice
  • More frequently for high-security environments

Immediately if:

  • Secret is compromised or exposed
  • Team member with access leaves
  • Compliance requirement
  • After security incident

Rolling Rotation (Zero Downtime)

Hooklistener supports rolling rotation to avoid downtime:

Via Dashboard:

  1. Organization SettingsSigning Secret
  2. Click "Roll Signing Secret"
  3. New secret is generated
  4. Both old and new secrets are valid for 24 hours
  5. Update your endpoints to use new secret
  6. After 24 hours, old secret expires automatically

Via API:

curl -X POST https://api.hooklistener.com/api/v1/organizations/{org_id}/signing_secret/roll \
-H "Authorization: Bearer YOUR_API_KEY"

Response:

{
"new_signing_secret": "whsec_new123...",
"rollover_period_hours": 24,
"old_secret_expires_at": "2024-01-16T10:30:00Z"
}

Rotation Process

  1. Initiate rotation (generates new secret)
  2. Both secrets valid (24-hour overlap)
  3. Update endpoints to use new secret
  4. Test thoroughly before old secret expires
  5. Old secret expires automatically after 24 hours

Timeline:

  • Hour 0: Generate new secret
  • Hours 0-24: Both secrets work
  • Hour 24: Old secret expires

Security Best Practices

Implementation

  1. Always verify signatures:

    • Never skip verification in production
    • Reject webhooks with invalid signatures
    • Return 401 Unauthorized for failures
  2. Check timestamp freshness:

    • Reject old timestamps (>5 minutes)
    • Prevents replay attacks
    • Protects against timing attacks
  3. Use constant-time comparison:

    • Prevents timing attacks
    • Use built-in secure compare functions
    • Never use simple string comparison
  4. Validate before processing:

    • Verify signature first
    • Then process webhook
    • Don't leak information in errors

Secret Management

  1. Secure storage:

    • Environment variables (production)
    • Secret management systems
    • Encrypted configuration
    • Never in source code
  2. Access control:

    • Limit who can view secrets
    • Audit secret access
    • Rotate when team changes
  3. Monitoring:

    • Log verification failures
    • Alert on repeated failures
    • Track secret usage

Network Security

  1. Use HTTPS only:

    • Always serve webhooks over HTTPS
    • Valid SSL certificates
    • Reject HTTP requests
  2. IP allowlisting (optional):

    • Allow only Hooklistener IPs
    • Additional layer of security
    • Request IP ranges from support
  3. Rate limiting:

    • Prevent abuse
    • Limit requests per minute
    • Return 429 for excessive requests

Troubleshooting

Signature Verification Failing

Problem: All webhooks rejected with invalid signature

Common causes:

1. Wrong secret:

  • Using old secret after rotation
  • Copy/paste error
  • Environment variable not updated

Solution: Verify secret matches current Organization secret

2. Body modification:

  • Body parsed/modified before verification
  • Whitespace changes
  • Encoding issues

Solution: Verify signature before parsing or modifying body

3. Incorrect signature recreation:

  • Wrong timestamp format
  • Missing dot separator
  • Incorrect HMAC algorithm

Solution: Follow exact format: {timestamp}.{body}

4. Timing issues:

  • Server clock drift
  • Timestamp check too strict
  • Wrong timezone

Solution: Sync server time, use NTP

Timestamp Too Old Errors

Problem: Webhooks rejected for old timestamp

Causes:

  • Server clock out of sync
  • Network delays
  • Tolerance window too narrow

Solutions:

  1. Sync server clock with NTP
  2. Increase tolerance to 5 minutes
  3. Check network latency
  4. Verify timestamp parsing

Intermittent Failures

Problem: Some webhooks pass, others fail

Causes:

  • Load balancer distributing to multiple servers
  • Some servers have old secret
  • Environment variables not synced
  • Rolling rotation in progress

Solutions:

  1. Ensure all servers have same secret
  2. Sync environment variables
  3. During rotation: both secrets valid
  4. Check server configurations

Testing Signature Verification

Test Endpoint

Test your implementation:

# Send test webhook from Hooklistener
curl -X POST https://api.hooklistener.com/api/v1/test/signed-webhook \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"destination_url": "https://your-endpoint.com/webhook"
}'

Manual Testing

Create a test signature locally:

Node.js:

const crypto = require('crypto');

const secret = 'your-signing-secret';
const timestamp = Math.floor(Date.now() / 1000);
const body = JSON.stringify({test: true});
const signedPayload = `${timestamp}.${body}`;
const signature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');

console.log(`X-Hooklistener-Signature: t=${timestamp},v1=${signature}`);

Then send request with this header and verify your endpoint accepts it.

Integration Examples

Express.js Middleware

const express = require('express');
const crypto = require('crypto');

function verifyHooklistenerSignature(signingSecret) {
return (req, res, next) => {
try {
const signature = req.headers['x-hooklistener-signature'];
if (!signature) {
return res.status(401).send('Missing signature');
}

const body = JSON.stringify(req.body);
const elements = signature.split(',');
const timestamp = elements[0].split('=')[1];
const receivedSig = elements[1].split('=')[1];

const currentTime = Math.floor(Date.now() / 1000);
if (Math.abs(currentTime - timestamp) > 300) {
return res.status(401).send('Timestamp too old');
}

const signedPayload = `${timestamp}.${body}`;
const expectedSig = crypto
.createHmac('sha256', signingSecret)
.update(signedPayload)
.digest('hex');

if (!crypto.timingSafeEqual(
Buffer.from(receivedSig),
Buffer.from(expectedSig)
)) {
return res.status(401).send('Invalid signature');
}

next();
} catch (error) {
res.status(401).send('Signature verification failed');
}
};
}

// Usage
app.post('/webhook',
verifyHooklistenerSignature(process.env.HOOKLISTENER_SIGNING_SECRET),
(req, res) => {
// Process verified webhook
console.log('Webhook verified:', req.body);
res.status(200).send('OK');
}
);

Django View

from django.http import JsonResponse, HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
import hmac
import hashlib
import time
import json
import os

def verify_hooklistener_signature(request):
signature_header = request.META.get('HTTP_X_HOOKLISTENER_SIGNATURE', '')

if not signature_header:
return False, 'Missing signature'

elements = signature_header.split(',')
timestamp = elements[0].split('=')[1]
received_signature = elements[1].split('=')[1]

current_time = int(time.time())
if abs(current_time - int(timestamp)) > 300:
return False, 'Timestamp too old'

signed_payload = f"{timestamp}.{request.body.decode('utf-8')}"
expected_signature = hmac.new(
os.environ['HOOKLISTENER_SIGNING_SECRET'].encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()

if not hmac.compare_digest(received_signature, expected_signature):
return False, 'Invalid signature'

return True, 'Valid'

@csrf_exempt
@require_http_methods(["POST"])
def webhook_view(request):
valid, message = verify_hooklistener_signature(request)

if not valid:
return HttpResponse(message, status=401)

# Process verified webhook
data = json.loads(request.body)
# ... your webhook processing logic ...

return JsonResponse({'status': 'success'})

Next Steps

Now that you understand Signing Secrets:

  1. Generate your signing secret
  2. Implement signature verification in your endpoints
  3. Test your implementation
  4. Set up rotation schedule
  5. Monitor verification failures

Signing Secrets are essential for production webhook security. Always verify signatures to ensure webhooks are authentic and haven't been tampered with.