Skip to content

Webhook Validation

Webhooks allow Flutterwave to notify your application about events that happen in your account, such as successful payments, failed transactions, or completed transfers. The SDK provides a WebhookValidator class to verify that webhook requests are genuinely from Flutterwave.

Overview

When Flutterwave sends a webhook to your server, it includes a signature in the request headers. This signature is generated using a secret hash that only you and Flutterwave know. By validating this signature, you can ensure the webhook is authentic and hasn't been tampered with.

Setup

1. Get Your Secret Hash

First, obtain your webhook secret hash from the Flutterwave Dashboard:

  1. Log in to your Flutterwave Dashboard
  2. Navigate to Settings → Webhooks
  3. Copy your Secret Hash

2. Store the Secret Hash

Store your secret hash securely as an environment variable:

bash
# .env
SECRET_HASH=your_secret_hash_here

3. Import WebhookValidator

typescript
import { WebhookValidator } from 'flutterwave-node-v4';

Basic Usage

Creating a Validator Instance

typescript
import { WebhookValidator } from 'flutterwave-node-v4';

const validator = new WebhookValidator(process.env.SECRET_HASH!);

Validating Webhooks

The validate() method takes two parameters:

  • The raw request body as a string
  • The signature from the request headers
typescript
const isValid = validator.validate(rawBody, signature);

if (isValid) {
  console.log('Webhook is authentic');
} else {
  console.log('Webhook is invalid or tampered with');
}

Generating Signatures (for testing)

You can generate signatures for testing purposes:

typescript
const signature = validator.generateSignature(rawBody);

Express.js Implementation

Here's a complete example using Express.js:

typescript
import express from 'express';
import { WebhookValidator } from 'flutterwave-node-v4';

const app = express();
const validator = new WebhookValidator(process.env.SECRET_HASH!);

// Important: Use express.raw() to get the raw body for signature validation
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['verif-hash'] as string;
  const rawBody = req.body.toString('utf8');

  // Validate the webhook signature
  if (!validator.validate(rawBody, signature)) {
    console.error('Invalid webhook signature');
    return res.sendStatus(401);
  }

  // Parse the validated webhook
  const event = JSON.parse(rawBody);

  // Process the webhook
  processWebhook(event);

  res.sendStatus(200);
});

function processWebhook(event: any): void {
  console.log('Webhook event:', event.event);
  console.log('Webhook data:', event.data);
}

app.listen(3000, () => {
  console.log('Webhook server running on port 3000');
});

Node.js HTTP Server Implementation

Using the built-in HTTP module:

typescript
import http from 'http';
import { WebhookValidator } from 'flutterwave-node-v4';

const validator = new WebhookValidator(process.env.SECRET_HASH!);

const server = http.createServer((req, res) => {
  if (req.method === 'POST' && req.url === '/webhook') {
    let body = '';

    req.on('data', (chunk) => {
      body += chunk.toString();
    });

    req.on('end', () => {
      const signature = req.headers['verif-hash'] as string;

      if (!validator.validate(body, signature)) {
        console.error('Invalid webhook signature');
        res.writeHead(401);
        res.end('Unauthorized');
        return;
      }

      const event = JSON.parse(body);
      processWebhook(event);

      res.writeHead(200);
      res.end('OK');
    });
  } else {
    res.writeHead(404);
    res.end('Not Found');
  }
});

server.listen(3000);

Fastify Implementation

typescript
import Fastify from 'fastify';
import { WebhookValidator } from 'flutterwave-node-v4';

const fastify = Fastify();
const validator = new WebhookValidator(process.env.SECRET_HASH!);

fastify.post('/webhook', {
  config: {
    rawBody: true,
  },
  handler: async (request, reply) => {
    const signature = request.headers['verif-hash'] as string;
    const rawBody = request.rawBody as string;

    if (!validator.validate(rawBody, signature)) {
      return reply.code(401).send({ error: 'Invalid signature' });
    }

    const event = JSON.parse(rawBody);
    processWebhook(event);

    return reply.code(200).send({ status: 'ok' });
  },
});

fastify.listen({ port: 3000 });

Webhook Event Types

Flutterwave sends different types of webhook events. Here's how to handle them:

typescript
function processWebhook(event: any): void {
  switch (event.event) {
    case 'charge.completed':
      handleChargeCompleted(event.data);
      break;

    case 'transfer.completed':
      handleTransferCompleted(event.data);
      break;

    case 'transfer.failed':
      handleTransferFailed(event.data);
      break;

    case 'virtual_account.credited':
      handleVirtualAccountCredited(event.data);
      break;

    case 'refund.completed':
      handleRefundCompleted(event.data);
      break;

    default:
      console.log('Unhandled webhook event:', event.event);
  }
}

function handleChargeCompleted(data: any): void {
  console.log('Charge completed:', data.id);
  console.log('Amount:', data.amount);
  console.log('Customer:', data.customer);

  // Update your database
  // Send confirmation email
  // etc.
}

function handleTransferCompleted(data: any): void {
  console.log('Transfer completed:', data.id);
  console.log('Reference:', data.reference);

  // Update transfer status in database
}

function handleVirtualAccountCredited(data: any): void {
  console.log('Virtual account credited:', data.account_number);
  console.log('Amount:', data.amount);

  // Credit customer account
  // Send notification
}

Advanced Validation

Validate and Process in One Function

typescript
function validateAndProcessWebhook(
  rawBody: string,
  signature: string,
): boolean {
  const validator = new WebhookValidator(process.env.SECRET_HASH!);

  if (!validator.validate(rawBody, signature)) {
    console.error('Invalid webhook signature');
    return false;
  }

  try {
    const event = JSON.parse(rawBody);
    processWebhook(event);
    return true;
  } catch (error) {
    console.error('Error processing webhook:', error);
    return false;
  }
}

Logging Webhooks

typescript
interface WebhookLog {
  timestamp: string;
  event: string;
  valid: boolean;
  data: any;
}

const webhookLogs: WebhookLog[] = [];

function logWebhook(rawBody: string, signature: string, valid: boolean): void {
  const event = valid ? JSON.parse(rawBody) : null;

  const log: WebhookLog = {
    timestamp: new Date().toISOString(),
    event: event?.event || 'unknown',
    valid,
    data: event?.data || null,
  };

  webhookLogs.push(log);

  if (!valid) {
    console.warn('Invalid webhook received:', log);
  }
}

app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['verif-hash'] as string;
  const rawBody = req.body.toString('utf8');
  const valid = validator.validate(rawBody, signature);

  logWebhook(rawBody, signature, valid);

  if (!valid) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(rawBody);
  processWebhook(event);

  res.sendStatus(200);
});

Testing Webhooks

Manual Testing

Generate test signatures for local testing:

typescript
import { WebhookValidator } from 'flutterwave-node-v4';

const validator = new WebhookValidator(process.env.SECRET_HASH!);

const testPayload = JSON.stringify({
  event: 'charge.completed',
  data: {
    id: 'test_123',
    amount: 5000,
    currency: 'NGN',
  },
});

const signature = validator.generateSignature(testPayload);

console.log('Test payload:', testPayload);
console.log('Test signature:', signature);

// Use these values to test your webhook endpoint

Automated Testing

typescript
import { describe, it, expect } from 'vitest';
import { WebhookValidator } from 'flutterwave-node-v4';

describe('Webhook Validation', () => {
  const secretHash = 'test_secret_hash';
  const validator = new WebhookValidator(secretHash);

  it('should validate correct signature', () => {
    const payload = JSON.stringify({ event: 'test', data: {} });
    const signature = validator.generateSignature(payload);

    expect(validator.validate(payload, signature)).toBe(true);
  });

  it('should reject invalid signature', () => {
    const payload = JSON.stringify({ event: 'test', data: {} });
    const wrongSignature = 'wrong_signature';

    expect(validator.validate(payload, wrongSignature)).toBe(false);
  });

  it('should reject tampered payload', () => {
    const payload = JSON.stringify({ event: 'test', data: {} });
    const signature = validator.generateSignature(payload);

    const tamperedPayload = JSON.stringify({
      event: 'test',
      data: { hacked: true },
    });

    expect(validator.validate(tamperedPayload, signature)).toBe(false);
  });
});

Best Practices

1. Always Validate Signatures

Never process webhooks without validation:

typescript
// Bad
app.post('/webhook', (req, res) => {
  processWebhook(req.body); // Dangerous!
  res.sendStatus(200);
});

// Good
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['verif-hash'] as string;
  const rawBody = req.body.toString('utf8');

  if (!validator.validate(rawBody, signature)) {
    return res.sendStatus(401);
  }

  processWebhook(JSON.parse(rawBody));
  res.sendStatus(200);
});

2. Use Raw Body for Validation

Always use the raw request body for signature validation:

typescript
// Express.js - use express.raw()
app.post('/webhook', express.raw({ type: 'application/json' }), handler);

// Don't use express.json() for webhook endpoints
// app.post('/webhook', express.json(), handler); // Wrong!

3. Respond Quickly

Process webhooks asynchronously and respond immediately:

typescript
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['verif-hash'] as string;
  const rawBody = req.body.toString('utf8');

  if (!validator.validate(rawBody, signature)) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(rawBody);

  // Respond immediately
  res.sendStatus(200);

  // Process asynchronously
  processWebhookAsync(event).catch((error) => {
    console.error('Webhook processing failed:', error);
  });
});

async function processWebhookAsync(event: any): Promise<void> {
  // Long-running operations here
  await updateDatabase(event);
  await sendNotifications(event);
}

4. Handle Duplicate Webhooks

Flutterwave may send the same webhook multiple times. Implement idempotency:

typescript
const processedWebhooks = new Set<string>();

function processWebhookIdempotent(event: any): void {
  const webhookId = event.id || event.data?.id;

  if (processedWebhooks.has(webhookId)) {
    console.log('Duplicate webhook, skipping:', webhookId);
    return;
  }

  processedWebhooks.add(webhookId);
  processWebhook(event);
}

5. Secure Your Endpoint

Protect your webhook endpoint:

typescript
// Only accept POST requests
if (req.method !== 'POST') {
  return res.sendStatus(405);
}

// Limit request size
app.use(
  '/webhook',
  express.raw({
    type: 'application/json',
    limit: '1mb',
  }),
);

// Rate limiting
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
});

app.post('/webhook', webhookLimiter, handler);

Troubleshooting

Signature Validation Fails

If validation consistently fails:

  1. Verify your secret hash is correct
  2. Ensure you're using the raw request body
  3. Check that the body encoding is UTF-8
  4. Verify the header name is exactly verif-hash

Testing Locally

Use tools like ngrok to expose your local server:

bash
# Install ngrok
npm install -g ngrok

# Expose your local server
ngrok http 3000

# Use the ngrok URL in Flutterwave Dashboard

Released under the MIT License.