Overview
Webhook signature verification ensures that incoming webhook requests are genuinely from Fin and have not been tampered with. This process uses HMAC (Hash-based Message Authentication Code) to validate each webhook payload.
How It Works
Extract the signature : Retrieve the signature value from the webhook request header
Recalculate the signature : Use your secret key and the same hash algorithm to compute the HMAC signature for the received payload
Compare signatures : Verify that your computed signature matches the one provided in the header
Verification Steps
Follow these steps to verify webhook signatures in your application:
Read the raw request body - Capture the HTTP body as raw bytes without any modifications or parsing
Extract signature headers - Read the x-fin-signature and x-fin-signature-algorithm headers from the request
Compute the HMAC - Using your webhook secret key and the algorithm specified in x-fin-signature-algorithm, calculate:
HMAC(algorithm, secretKey, rawBodyBytes) → hexadecimal string
Compare signatures - Accept the webhook only if your computed signature exactly matches the x-fin-signature header value
Always use a constant-time comparison function to prevent timing attacks when comparing signatures.
Code Examples
Here are implementation examples in different programming languages:
const crypto = require ( 'crypto' );
function verifyWebhookSignature ( request , webhookSecret ) {
// Extract headers
const receivedSignature = request . headers [ 'x-fin-signature' ];
const algorithm = request . headers [ 'x-fin-signature-algorithm' ] || 'sha256' ;
// Get raw body as string or buffer
const rawBody = request . body ; // Ensure this is the raw body, not parsed JSON
// Compute HMAC
const computedSignature = crypto
. createHmac ( algorithm , webhookSecret )
. update ( rawBody , 'utf8' )
. digest ( 'hex' );
// Compare signatures using constant-time comparison
return crypto . timingSafeEqual (
Buffer . from ( receivedSignature ),
Buffer . from ( computedSignature )
);
}
// Usage example with Express.js
app . post ( '/webhooks' , express . raw ({ type: 'application/json' }), ( req , res ) => {
const isValid = verifyWebhookSignature ( req , process . env . WEBHOOK_SECRET );
if ( ! isValid ) {
return res . status ( 401 ). send ( 'Invalid signature' );
}
// Process the webhook
const payload = JSON . parse ( req . body );
console . log ( 'Valid webhook received:' , payload );
res . status ( 200 ). send ( 'OK' );
});
package main
import (
" crypto/hmac "
" crypto/sha256 "
" crypto/subtle "
" encoding/hex "
" io "
" net/http "
)
func verifyWebhookSignature ( r * http . Request , webhookSecret string ) ( bool , error ) {
// Extract headers
receivedSignature := r . Header . Get ( "x-fin-signature" )
algorithm := r . Header . Get ( "x-fin-signature-algorithm" )
if algorithm == "" {
algorithm = "sha256"
}
// Read raw body
rawBody , err := io . ReadAll ( r . Body )
if err != nil {
return false , err
}
// Compute HMAC (using SHA-256 as example)
mac := hmac . New ( sha256 . New , [] byte ( webhookSecret ))
mac . Write ( rawBody )
computedSignature := hex . EncodeToString ( mac . Sum ( nil ))
// Constant-time comparison
return subtle . ConstantTimeCompare (
[] byte ( receivedSignature ),
[] byte ( computedSignature ),
) == 1 , nil
}
// HTTP handler example
func webhookHandler ( w http . ResponseWriter , r * http . Request ) {
webhookSecret := os . Getenv ( "WEBHOOK_SECRET" )
isValid , err := verifyWebhookSignature ( r , webhookSecret )
if err != nil || ! isValid {
http . Error ( w , "Invalid signature" , http . StatusUnauthorized )
return
}
// Process the webhook
w . WriteHeader ( http . StatusOK )
w . Write ([] byte ( "OK" ))
}
import hmac
import hashlib
from flask import Flask, request, abort
def verify_webhook_signature ( request , webhook_secret ):
"""
Verify the HMAC signature of an incoming webhook request.
Args:
request: Flask request object
webhook_secret: Your webhook secret key
Returns:
bool: True if signature is valid, False otherwise
"""
# Extract headers
received_signature = request.headers.get( 'x-fin-signature' )
algorithm = request.headers.get( 'x-fin-signature-algorithm' , 'sha256' )
if not received_signature:
return False
# Get raw body bytes
raw_body = request.get_data()
# Compute HMAC
hash_func = getattr (hashlib, algorithm)
computed_signature = hmac.new(
webhook_secret.encode( 'utf-8' ),
raw_body,
hash_func
).hexdigest()
# Constant-time comparison
return hmac.compare_digest(received_signature, computed_signature)
# Flask example
app = Flask( __name__ )
@app.route ( '/webhooks' , methods = [ 'POST' ])
def webhook_handler ():
webhook_secret = os.environ.get( 'WEBHOOK_SECRET' )
if not verify_webhook_signature(request, webhook_secret):
abort( 401 , 'Invalid signature' )
# Process the webhook
payload = request.get_json()
print ( f 'Valid webhook received: { payload } ' )
return 'OK' , 200
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.util.HexFormat;
public class WebhookVerifier {
public static boolean verifyWebhookSignature (
String receivedSignature ,
String algorithm ,
byte [] rawBody ,
String webhookSecret
) {
try {
// Default to SHA-256 if algorithm not specified
if (algorithm == null || algorithm . isEmpty ()) {
algorithm = "sha256" ;
}
// Convert algorithm name to Java format (e.g., "sha256" -> "HmacSHA256")
String javaAlgorithm = "Hmac" + algorithm . toUpperCase (). replace ( "SHA" , "SHA" );
// Compute HMAC
Mac mac = Mac . getInstance (javaAlgorithm);
SecretKeySpec secretKeySpec = new SecretKeySpec (
webhookSecret . getBytes ( "UTF-8" ),
javaAlgorithm
);
mac . init (secretKeySpec);
byte [] hmacBytes = mac . doFinal (rawBody);
// Convert to hex string
String computedSignature = HexFormat . of (). formatHex (hmacBytes);
// Constant-time comparison
return MessageDigest . isEqual (
receivedSignature . getBytes (),
computedSignature . getBytes ()
);
} catch ( Exception e ) {
e . printStackTrace ();
return false ;
}
}
// Spring Boot example
@ PostMapping ( "/webhooks" )
public ResponseEntity < String > handleWebhook (
@ RequestHeader ( "x-fin-signature" ) String signature ,
@ RequestHeader ( value = "x-fin-signature-algorithm" , defaultValue = "sha256" ) String algorithm ,
@ RequestBody byte [] rawBody
) {
String webhookSecret = System . getenv ( "WEBHOOK_SECRET" );
boolean isValid = verifyWebhookSignature (signature, algorithm, rawBody, webhookSecret);
if ( ! isValid) {
return ResponseEntity . status ( HttpStatus . UNAUTHORIZED ). body ( "Invalid signature" );
}
// Process the webhook
String payload = new String (rawBody, StandardCharsets . UTF_8 );
System . out . println ( "Valid webhook received: " + payload);
return ResponseEntity . ok ( "OK" );
}
}
Best Practices
Never hardcode your webhook secret in your source code. Use environment variables or a secure secrets management system.
Use constant-time comparison
Always use constant-time comparison functions (like crypto.timingSafeEqual in Node.js or hmac.compare_digest in Python) to prevent timing attacks.
Ensure your web framework provides access to the raw request body before any parsing or modifications occur.
Return appropriate HTTP status codes (401 Unauthorized) for failed verifications without revealing specific error details.
Troubleshooting
If signature verification is failing:
Check the raw body : Ensure you’re using the exact raw bytes received, not a re-serialized version
Verify the secret : Confirm you’re using the correct webhook secret from your Fin dashboard
Check character encoding : Make sure you’re using UTF-8 encoding consistently
Inspect headers : Verify that x-fin-signature and x-fin-signature-algorithm headers are present
Test the algorithm : Confirm you’re using the algorithm specified in the x-fin-signature-algorithm header