Webhook Verification
Webhook signature verification is critical for securing your payment endpoints. Without proper verification, malicious actors could send fake payment notifications to your system.
Never skip webhook verification in production. Always validate signatures before processing payment data or minting tickets.
Why Verify Webhooks?
Webhook endpoints are publicly accessible URLs that anyone can discover and call. Signature verification ensures:
Authenticity - The webhook came from the payment provider, not an attacker
Integrity - The payload hasn’t been tampered with during transmission
Security - Prevents unauthorized ticket minting and fraud
Compliance - Meets payment industry security standards
Paystack Signature Verification
Paystack uses HMAC SHA512 signatures to secure webhooks.
How It Works
Paystack creates an HMAC SHA512 hash of the request body using your secret key
The hash is sent in the x-paystack-signature header
Your server recreates the hash using the same secret key
If the hashes match, the webhook is authentic
Implementation
TypeScript (webhooks.ts:82-89)
JavaScript
Python
import crypto from 'crypto' ;
const secret = process . env . PAYSTACK_SECRET_KEY ;
if ( ! secret ) {
return res . status ( 500 ). json ({ error: 'Server configuration error' });
}
const signature = req . headers [ 'x-paystack-signature' ];
if ( ! signature ) {
return res . status ( 401 ). json ({ error: 'No signature' });
}
const hash = crypto
. createHmac ( 'sha512' , secret )
. update ( req . body )
. digest ( 'hex' );
if ( hash !== signature ) {
return res . status ( 401 ). json ({ error: 'Invalid signature' });
}
// Webhook is verified - safe to process
Configuration
Set your Paystack secret key in environment variables:
PAYSTACK_SECRET_KEY = sk_live_your_secret_key_here
Never commit secret keys to version control. Use environment variables or secure secret management.
Flutterwave Signature Verification
Flutterwave uses a simpler secret hash verification method.
How It Works
You configure a secret hash in both Flutterwave dashboard and your server
Flutterwave sends this hash in the verif-hash header
Your server compares the header value with your stored secret hash
If they match, the webhook is authentic
Implementation
TypeScript (flutterwave.ts:58-61)
JavaScript
Python
export const validateFlutterwaveWebhook = (
signature : string ,
payload : any
) : boolean => {
const secretHash = process . env . FLW_SECRET_HASH ;
return signature === secretHash ;
};
// Usage in webhook handler (webhooks.ts:107-110)
const signature = req . headers [ 'verif-hash' ];
if ( ! signature || ! validateFlutterwaveWebhook ( signature as string , req . body )) {
return res . status ( 401 ). json ({ error: 'Invalid signature' });
}
// Webhook is verified - safe to process
Configuration
Set your Flutterwave secret hash in environment variables:
FLW_SECRET_KEY = FLWSECK-your-secret-key-here
FLW_SECRET_HASH = your_custom_secret_hash_string
The FLW_SECRET_HASH should be:
A random, unpredictable string
At least 32 characters long
Configured in both your Flutterwave dashboard and server
Security Best Practices
1. Always Verify Before Processing
router . post ( '/webhook' , async ( req , res ) => {
// Verify FIRST
if ( ! verifySignature ( req )) {
return res . status ( 401 ). json ({ error: 'Invalid signature' });
}
// Then process
await processPayment ( req . body );
res . json ({ ok: true });
});
2. Use Constant-Time Comparison
For enhanced security, use constant-time comparison to prevent timing attacks:
import crypto from 'crypto' ;
function safeCompare ( a : string , b : string ) : boolean {
if ( a . length !== b . length ) return false ;
return crypto . timingSafeEqual ( Buffer . from ( a ), Buffer . from ( b ));
}
const isValid = safeCompare ( hash , signature );
3. Protect Your Secret Keys
Store keys in environment variables, never in code
Use different keys for development and production
Rotate keys periodically
Use secret management services (AWS Secrets Manager, HashiCorp Vault, etc.)
Restrict access to production keys
4. Log Verification Failures
if ( hash !== signature ) {
logger . warn ( 'Webhook signature verification failed' , {
ip: req . ip ,
headers: req . headers ,
timestamp: new Date (). toISOString ()
});
return res . status ( 401 ). json ({ error: 'Invalid signature' });
}
5. Implement Rate Limiting
Protect webhook endpoints from brute force attacks:
import rateLimit from 'express-rate-limit' ;
const webhookLimiter = rateLimit ({
windowMs: 15 * 60 * 1000 , // 15 minutes
max: 100 , // Limit each IP to 100 requests per windowMs
message: 'Too many webhook requests'
});
router . post ( '/webhook' , webhookLimiter , async ( req , res ) => {
// Handle webhook
});
6. Handle Webhook Body Correctly
For Paystack verification, use the raw body buffer , not parsed JSON. Express body parsers can alter the body, breaking signature verification.
import express from 'express' ;
const app = express ();
// Store raw body for webhook verification
app . use (
express . json ({
verify : ( req : any , res , buf ) => {
req . rawBody = buf . toString ();
}
})
);
// Use raw body for verification
const hash = crypto
. createHmac ( 'sha512' , secret )
. update ( req . rawBody ) // Not req.body
. digest ( 'hex' );
7. Validate Payload Structure
Even after signature verification, validate the payload structure:
if ( hash === signature ) {
const body = JSON . parse ( req . rawBody );
// Validate expected fields exist
if ( ! body . event || ! body . data || ! body . data . reference ) {
logger . error ( 'Invalid webhook payload structure' );
return res . status ( 400 ). json ({ error: 'Invalid payload' });
}
// Proceed with processing
}
Testing Verification
Generate Test Signatures
const crypto = require ( 'crypto' );
const payload = JSON . stringify ({
event: 'charge.success' ,
data: { reference: 'test_123' , id: 456 }
});
const secret = 'sk_test_your_secret_key' ;
const signature = crypto
. createHmac ( 'sha512' , secret )
. update ( payload )
. digest ( 'hex' );
console . log ( 'Signature:' , signature );
Unit Tests
import { describe , it , expect } from 'vitest' ;
import crypto from 'crypto' ;
describe ( 'Webhook Verification' , () => {
it ( 'should verify valid Paystack signature' , () => {
const secret = 'test_secret' ;
const payload = '{"event":"charge.success"}' ;
const signature = crypto
. createHmac ( 'sha512' , secret )
. update ( payload )
. digest ( 'hex' );
const hash = crypto
. createHmac ( 'sha512' , secret )
. update ( payload )
. digest ( 'hex' );
expect ( hash ). toBe ( signature );
});
it ( 'should reject invalid signature' , () => {
const secret = 'test_secret' ;
const payload = '{"event":"charge.success"}' ;
const invalidSignature = 'invalid_signature' ;
const hash = crypto
. createHmac ( 'sha512' , secret )
. update ( payload )
. digest ( 'hex' );
expect ( hash ). not . toBe ( invalidSignature );
});
});
Common Issues
Issue: Signature Mismatch
Causes:
Using parsed JSON instead of raw body buffer
Wrong secret key (development vs production)
Body modified by middleware before verification
Character encoding issues
Solution:
// Ensure raw body is preserved
app . use ( express . json ({
verify : ( req : any , res , buf ) => {
req . rawBody = buf . toString ( 'utf8' );
}
}));
// Use raw body for verification
const hash = crypto
. createHmac ( 'sha512' , secret )
. update ( req . rawBody )
. digest ( 'hex' );
Causes:
Webhook not configured in payment provider dashboard
Testing without proper headers
Header name mismatch
Solution:
const signature = req . headers [ 'x-paystack-signature' ] ||
req . headers [ 'verif-hash' ];
if ( ! signature ) {
logger . error ( 'Missing webhook signature header' , {
headers: Object . keys ( req . headers )
});
return res . status ( 401 ). json ({ error: 'No signature' });
}
Issue: Configuration Error
Causes:
Missing environment variables
Secret key not loaded
Solution:
const secret = process . env . PAYSTACK_SECRET_KEY ;
if ( ! secret ) {
logger . error ( 'PAYSTACK_SECRET_KEY not configured' );
return res . status ( 500 ). json ({
error: 'Server configuration error'
});
}
Source Code References
GatePass webhook verification implementation:
Paystack verification : /src/packages/server/src/routes/webhooks.ts:82-89
Flutterwave verification : /src/packages/server/src/utils/flutterwave.ts:58-61
Webhook handlers : /src/packages/server/src/routes/webhooks.ts:79-123
Payment Webhooks Learn about payment webhook endpoints
Paystack Docs Official Paystack webhook documentation
Flutterwave Docs Official Flutterwave webhook documentation
HMAC Security Learn about HMAC cryptographic signatures