Skip to main content

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

  1. Paystack creates an HMAC SHA512 hash of the request body using your secret key
  2. The hash is sent in the x-paystack-signature header
  3. Your server recreates the hash using the same secret key
  4. If the hashes match, the webhook is authentic

Implementation

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:
.env
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

  1. You configure a secret hash in both Flutterwave dashboard and your server
  2. Flutterwave sends this hash in the verif-hash header
  3. Your server compares the header value with your stored secret hash
  4. If they match, the webhook is authentic

Implementation

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:
.env
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');

Issue: Missing Signature Header

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

Build docs developers (and LLMs) love