Verifying Webhook Signatures
Every webhook from KnoxCall includes an HMAC-SHA256 signature. Verifying this signature ensures the webhook:
Came from KnoxCall (not an attacker)
Wasn’t modified in transit
How Signatures Work
When KnoxCall sends a webhook:
Takes the JSON payload
Signs it with your webhook’s secret key using HMAC-SHA256
Includes the signature in the X-Webhook-Signature header
X-Webhook-Signature: sha256=a1b2c3d4e5f6...
Your endpoint should:
Get the signature from the header
Compute the expected signature using the same secret
Compare them (timing-safe comparison)
Reject if they don’t match
Implementation Examples
Node.js (Express)
const crypto = require ( 'crypto' );
const express = require ( 'express' );
const app = express ();
// IMPORTANT: Use raw body for signature verification
app . use ( '/webhook' , express . raw ({ type: 'application/json' }));
const WEBHOOK_SECRET = process . env . WEBHOOK_SECRET ;
function verifySignature ( payload , signature ) {
const expected = 'sha256=' + crypto
. createHmac ( 'sha256' , WEBHOOK_SECRET )
. update ( payload )
. digest ( 'hex' );
// Use timing-safe comparison to prevent timing attacks
return crypto . timingSafeEqual (
Buffer . from ( signature ),
Buffer . from ( expected )
);
}
app . post ( '/webhook' , ( req , res ) => {
const signature = req . headers [ 'x-webhook-signature' ];
if ( ! signature ) {
return res . status ( 401 ). json ({ error: 'Missing signature' });
}
if ( ! verifySignature ( req . body , signature )) {
return res . status ( 401 ). json ({ error: 'Invalid signature' });
}
// Signature valid - process the webhook
const event = JSON . parse ( req . body );
console . log ( 'Received event:' , event . event );
res . status ( 200 ). json ({ received: true });
});
app . listen ( 3000 );
Python (Flask)
import hmac
import hashlib
from flask import Flask, request, jsonify
app = Flask( __name__ )
WEBHOOK_SECRET = os.environ.get( 'WEBHOOK_SECRET' )
def verify_signature ( payload , signature ):
expected = 'sha256=' + hmac.new(
WEBHOOK_SECRET .encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
@app.route ( '/webhook' , methods = [ 'POST' ])
def webhook ():
signature = request.headers.get( 'X-Webhook-Signature' )
if not signature:
return jsonify({ 'error' : 'Missing signature' }), 401
if not verify_signature(request.data, signature):
return jsonify({ 'error' : 'Invalid signature' }), 401
# Signature valid - process the webhook
event = request.json
print ( f "Received event: { event[ 'event' ] } " )
return jsonify({ 'received' : True }), 200
if __name__ == '__main__' :
app.run( port = 3000 )
PHP
<? php
$webhookSecret = getenv ( 'WEBHOOK_SECRET' );
$payload = file_get_contents ( 'php://input' );
$signature = $_SERVER [ 'HTTP_X_WEBHOOK_SIGNATURE' ] ?? '' ;
function verifySignature ( $payload , $signature , $secret ) {
$expected = 'sha256=' . hash_hmac ( 'sha256' , $payload , $secret );
return hash_equals ( $expected , $signature );
}
if ( empty ( $signature )) {
http_response_code ( 401 );
echo json_encode ([ 'error' => 'Missing signature' ]);
exit ;
}
if ( ! verifySignature ( $payload , $signature , $webhookSecret )) {
http_response_code ( 401 );
echo json_encode ([ 'error' => 'Invalid signature' ]);
exit ;
}
// Signature valid - process the webhook
$event = json_decode ( $payload , true );
error_log ( "Received event: " . $event [ 'event' ]);
http_response_code ( 200 );
echo json_encode ([ 'received' => true ]);
package main
import (
" crypto/hmac "
" crypto/sha256 "
" encoding/hex "
" encoding/json "
" io "
" net/http "
" os "
)
var webhookSecret = os . Getenv ( "WEBHOOK_SECRET" )
func verifySignature ( payload [] byte , signature string ) bool {
mac := hmac . New ( sha256 . New , [] byte ( webhookSecret ))
mac . Write ( payload )
expected := "sha256=" + hex . EncodeToString ( mac . Sum ( nil ))
return hmac . Equal ([] byte ( expected ), [] byte ( signature ))
}
func webhookHandler ( w http . ResponseWriter , r * http . Request ) {
signature := r . Header . Get ( "X-Webhook-Signature" )
if signature == "" {
http . Error ( w , `{"error":"Missing signature"}` , http . StatusUnauthorized )
return
}
payload , _ := io . ReadAll ( r . Body )
if ! verifySignature ( payload , signature ) {
http . Error ( w , `{"error":"Invalid signature"}` , http . StatusUnauthorized )
return
}
// Signature valid - process the webhook
var event map [ string ] interface {}
json . Unmarshal ( payload , & event )
w . Header (). Set ( "Content-Type" , "application/json" )
w . Write ([] byte ( `{"received":true}` ))
}
func main () {
http . HandleFunc ( "/webhook" , webhookHandler )
http . ListenAndServe ( ":3000" , nil )
}
Ruby (Sinatra)
require 'sinatra'
require 'json'
require 'openssl'
WEBHOOK_SECRET = ENV [ 'WEBHOOK_SECRET' ]
def verify_signature ( payload , signature )
expected = 'sha256=' + OpenSSL :: HMAC . hexdigest (
'sha256' ,
WEBHOOK_SECRET ,
payload
)
Rack :: Utils . secure_compare (expected, signature)
end
post '/webhook' do
request. body . rewind
payload = request. body . read
signature = request. env [ 'HTTP_X_WEBHOOK_SIGNATURE' ]
unless signature
halt 401 , { error: 'Missing signature' }. to_json
end
unless verify_signature (payload, signature)
halt 401 , { error: 'Invalid signature' }. to_json
end
# Signature valid - process the webhook
event = JSON . parse (payload)
puts "Received event: #{ event[ 'event' ] } "
{ received: true }. to_json
end
Getting Your Webhook Secret
View Secret
Navigate to Webhooks
Click on your webhook
Click Reveal Secret
Copy the secret
The secret looks like: a1b2c3d4e5f6... (64 hex characters)
Regenerate Secret
If your secret is compromised:
Click Regenerate Secret
Copy the new secret immediately
Update your endpoint with the new secret
Regenerating invalidates the old secret immediately. Update your endpoint before regenerating, or webhooks will fail validation.
Common Mistakes
1. Parsing JSON Before Verification
Wrong:
// DON'T DO THIS
app . use ( express . json ());
app . post ( '/webhook' , ( req , res ) => {
// req.body is already parsed - signature will fail!
const signature = computeSignature ( JSON . stringify ( req . body ));
});
Right:
// Use raw body for webhook routes
app . use ( '/webhook' , express . raw ({ type: 'application/json' }));
app . post ( '/webhook' , ( req , res ) => {
// req.body is the raw string
const signature = computeSignature ( req . body );
});
2. Wrong Secret
Symptoms:
All signatures fail
No intermittent issues
Fix:
Copy secret directly from KnoxCall
Ensure no extra whitespace
Check environment variable is set
3. Non-Timing-Safe Comparison
Wrong:
// DON'T DO THIS - vulnerable to timing attacks
if ( signature === expected ) { ... }
Right:
// Use timing-safe comparison
if ( crypto . timingSafeEqual ( Buffer . from ( signature ), Buffer . from ( expected ))) { ... }
4. Encoding Issues
Symptoms:
Signatures fail for certain payloads
Works for simple data, fails for special characters
Fix:
Use raw bytes, not decoded strings
Ensure UTF-8 encoding throughout
Security Best Practices
1. Always Verify Signatures
Never process webhooks without signature verification:
if ( ! verifySignature ( payload , signature )) {
// Log the attempt for security monitoring
console . warn ( 'Invalid webhook signature from:' , req . ip );
return res . status ( 401 ). json ({ error: 'Invalid signature' });
}
2. Store Secrets Securely
Use environment variables (not hardcoded)
Rotate secrets periodically
Use secrets management services in production
3. Reject Missing Signatures
if ( ! signature ) {
return res . status ( 401 ). json ({ error: 'Missing signature' });
}
4. Log Verification Failures
Monitor for repeated failures - could indicate an attack:
if ( ! verifySignature ( payload , signature )) {
logger . warn ({
message: 'Webhook signature verification failed' ,
ip: req . ip ,
webhookId: req . headers [ 'x-webhook-id' ]
});
}
5. Use HTTPS Only
Signatures protect against tampering, but HTTPS protects against eavesdropping:
Always use HTTPS endpoints
Reject HTTP in production
Debugging Signature Issues
Step 1: Log Both Signatures
console . log ( 'Received:' , signature );
console . log ( 'Expected:' , expected );
Step 2: Check Payload
console . log ( 'Payload length:' , payload . length );
console . log ( 'Payload:' , payload . toString ());
Step 3: Verify Secret
console . log ( 'Secret length:' , WEBHOOK_SECRET . length );
console . log ( 'Secret first 4 chars:' , WEBHOOK_SECRET . substring ( 0 , 4 ));
Step 4: Test with Known Values
Use the KnoxCall test feature and compare:
Test payload vs what you receive
Expected signature vs what you compute
Next Steps
Statistics
Level : intermediate
Time : 8 minutes
Tags webhooks, security, signatures, hmac, verification