Skip to content

Virtual Accounts ​

This guide provides comprehensive examples for creating and managing virtual accounts using the Flutterwave SDK. Virtual accounts allow your customers to make payments directly to dedicated bank accounts.

Overview ​

Flutterwave offers two types of virtual accounts:

  • Static Virtual Accounts - Permanent accounts tied to a customer for recurring payments
  • Dynamic Virtual Accounts - Temporary accounts for one-time or limited-use payments

Basic Virtual Account Creation ​

Create a Static Virtual Account ​

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

const flutterwave = new Flutterwave({
  clientId: process.env.CLIENT_ID!,
  clientSecret: process.env.CLIENT_SECRET!,
  environment: 'sandbox',
});

const account = await flutterwave.api.virtualAccounts.create({
  type: 'static',
  reference: `va-${Date.now()}`,
  customer: {
    name: {
      first: 'John',
      last: 'Doe',
    },
    email: 'john.doe@example.com',
  },
  currency: 'NGN',
});

console.log('Account Number:', account.account_number);
console.log('Bank Name:', account.bank_name);
console.log('Account Name:', account.account_name);
console.log('Account ID:', account.id);

Create a Dynamic Virtual Account ​

typescript
const account = await flutterwave.api.virtualAccounts.create({
  type: 'dynamic',
  reference: `va-dynamic-${Date.now()}`,
  customer: {
    name: {
      first: 'Jane',
      last: 'Smith',
    },
    email: 'jane.smith@example.com',
  },
  currency: 'NGN',
  amount: 50000, // Expected payment amount
  expiry: 24, // Expires in 24 hours
});

console.log('Dynamic Account Created');
console.log('Account Number:', account.account_number);
console.log('Expected Amount:', account.amount);
console.log('Expires At:', account.expiry_date);

Advanced Virtual Account Creation ​

Create with Full Customer Details ​

typescript
const account = await flutterwave.api.virtualAccounts.create({
  type: 'static',
  reference: `va-full-${Date.now()}`,
  customer: {
    name: {
      first: 'Michael',
      last: 'Johnson',
    },
    email: 'michael.johnson@example.com',
    phone_number: '+2348012345678',
  },
  currency: 'NGN',
  narration: 'Payment for services',
  bvn: '12345678901', // Bank Verification Number
  meta: {
    customer_id: 'CUST-12345',
    plan: 'premium',
  },
});

console.log('Account Created with Metadata');
console.log('Reference:', account.reference);
console.log('Metadata:', account.meta);

Create with Customer ID ​

If you already have a customer in the system:

typescript
// First, get the customer
const customers = await flutterwave.api.customers.list({
  page: 1,
  size: 10,
});

const customerId = customers.data[0].id;

// Create virtual account for existing customer
const account = await flutterwave.api.virtualAccounts.create({
  type: 'static',
  reference: `va-customer-${Date.now()}`,
  customer_id: customerId,
  currency: 'NGN',
});

console.log('Virtual Account for Customer:', customerId);
console.log('Account Number:', account.account_number);

Managing Virtual Accounts ​

List All Virtual Accounts ​

typescript
const accounts = await flutterwave.api.virtualAccounts.list({
  page: 1,
  size: 20,
});

console.log(`Total Accounts: ${accounts.meta.total}`);
console.log(`Current Page: ${accounts.meta.current_page}`);

accounts.data.forEach((account) => {
  console.log(
    `${account.reference}: ${account.account_number} (${account.currency})`,
  );
});

Retrieve a Specific Virtual Account ​

typescript
const accountId = 'va_abc123';

const account = await flutterwave.api.virtualAccounts.retrieve(accountId);

console.log('Account Details:');
console.log('Number:', account.account_number);
console.log('Bank:', account.bank_name);
console.log('Status:', account.status);
console.log('Balance:', account.balance);

Update Virtual Account ​

typescript
const accountId = 'va_abc123';

const updated = await flutterwave.api.virtualAccounts.update(accountId, {
  action_type: 'update_bvn',
  bvn: '12345678901',
});

console.log('Account Updated');
console.log('ID:', updated.id);
console.log('Status:', updated.status);

Webhook Integration ​

Handle Virtual Account Credits ​

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

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

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);

  if (event.event === 'virtual_account.credited') {
    handleVirtualAccountCredit(event.data);
  }

  res.sendStatus(200);
});

async function handleVirtualAccountCredit(data: any): Promise<void> {
  console.log('Virtual Account Credited');
  console.log('Account Number:', data.account_number);
  console.log('Amount:', data.amount);
  console.log('Currency:', data.currency);
  console.log('Customer Email:', data.customer.email);
  console.log('Reference:', data.reference);

  // Credit customer's account in your database
  await creditCustomerAccount(data.customer.email, data.amount);

  // Send confirmation email
  await sendPaymentConfirmation(data.customer.email, data.amount);
}

async function creditCustomerAccount(
  email: string,
  amount: number,
): Promise<void> {
  // Your database logic here
  console.log(`Credited ${email} with ${amount}`);
}

async function sendPaymentConfirmation(
  email: string,
  amount: number,
): Promise<void> {
  // Your email logic here
  console.log(`Sent confirmation to ${email} for ${amount}`);
}

app.listen(3000);

Use Cases ​

Subscription Payments ​

Create virtual accounts for subscription customers:

typescript
interface SubscriptionPlan {
  name: string;
  amount: number;
  interval: 'monthly' | 'yearly';
}

async function createSubscriptionAccount(
  customer: {
    firstName: string;
    lastName: string;
    email: string;
  },
  plan: SubscriptionPlan,
) {
  const account = await flutterwave.api.virtualAccounts.create({
    type: 'static',
    reference: `sub-${customer.email}-${Date.now()}`,
    customer: {
      name: {
        first: customer.firstName,
        last: customer.lastName,
      },
      email: customer.email,
    },
    currency: 'NGN',
    narration: `${plan.name} subscription`,
    meta: {
      plan_name: plan.name,
      plan_amount: plan.amount,
      plan_interval: plan.interval,
    },
  });

  // Store account details in your database
  await saveSubscriptionAccount({
    customerId: customer.email,
    accountNumber: account.account_number,
    accountId: account.id,
    plan: plan.name,
  });

  return account;
}

// Usage
const account = await createSubscriptionAccount(
  {
    firstName: 'Alice',
    lastName: 'Williams',
    email: 'alice@example.com',
  },
  {
    name: 'Premium Plan',
    amount: 5000,
    interval: 'monthly',
  },
);

console.log('Subscription Account:', account.account_number);

Invoice Payments ​

Create dynamic virtual accounts for invoices:

typescript
interface Invoice {
  id: string;
  customerId: string;
  amount: number;
  dueDate: Date;
  items: Array<{ description: string; amount: number }>;
}

async function createInvoiceAccount(invoice: Invoice) {
  // Get customer details
  const customer = await getCustomerById(invoice.customerId);

  // Calculate hours until due date
  const hoursUntilDue = Math.ceil(
    (invoice.dueDate.getTime() - Date.now()) / (1000 * 60 * 60),
  );

  const account = await flutterwave.api.virtualAccounts.create({
    type: 'dynamic',
    reference: `inv-${invoice.id}`,
    customer: {
      name: {
        first: customer.firstName,
        last: customer.lastName,
      },
      email: customer.email,
    },
    currency: 'NGN',
    amount: invoice.amount,
    expiry: hoursUntilDue,
    narration: `Payment for Invoice ${invoice.id}`,
    meta: {
      invoice_id: invoice.id,
      items: JSON.stringify(invoice.items),
    },
  });

  // Send invoice with account details to customer
  await sendInvoiceEmail(customer.email, invoice, account);

  return account;
}

async function getCustomerById(id: string): Promise<any> {
  // Your database logic
  return {
    firstName: 'Bob',
    lastName: 'Smith',
    email: 'bob@example.com',
  };
}

async function sendInvoiceEmail(
  email: string,
  invoice: Invoice,
  account: any,
): Promise<void> {
  console.log(`Invoice ${invoice.id} sent to ${email}`);
  console.log(`Pay to: ${account.account_number}`);
  console.log(`Amount: ${invoice.amount}`);
}

Event Ticketing ​

Use virtual accounts for event ticket payments:

typescript
interface Event {
  id: string;
  name: string;
  date: Date;
  ticketPrice: number;
  venue: string;
}

interface Attendee {
  firstName: string;
  lastName: string;
  email: string;
  phone: string;
}

async function createTicketAccount(event: Event, attendee: Attendee) {
  // Create account that expires 30 minutes before event
  const expiryDate = new Date(event.date);
  expiryDate.setMinutes(expiryDate.getMinutes() - 30);

  const hoursUntilExpiry = Math.ceil(
    (expiryDate.getTime() - Date.now()) / (1000 * 60 * 60),
  );

  const account = await flutterwave.api.virtualAccounts.create({
    type: 'dynamic',
    reference: `ticket-${event.id}-${Date.now()}`,
    customer: {
      name: {
        first: attendee.firstName,
        last: attendee.lastName,
      },
      email: attendee.email,
      phone_number: attendee.phone,
    },
    currency: 'NGN',
    amount: event.ticketPrice,
    expiry: hoursUntilExpiry,
    narration: `Ticket for ${event.name}`,
    meta: {
      event_id: event.id,
      event_name: event.name,
      event_date: event.date.toISOString(),
      event_venue: event.venue,
    },
  });

  return {
    account,
    ticketId: `TCK-${Date.now()}`,
  };
}

// Usage
const ticketAccount = await createTicketAccount(
  {
    id: 'EVT-001',
    name: 'Tech Conference 2024',
    date: new Date('2024-06-15T09:00:00'),
    ticketPrice: 25000,
    venue: 'Lagos Convention Center',
  },
  {
    firstName: 'Carol',
    lastName: 'Brown',
    email: 'carol@example.com',
    phone: '+2348012345678',
  },
);

console.log('Ticket Account:', ticketAccount.account.account_number);
console.log('Ticket ID:', ticketAccount.ticketId);

Account Monitoring ​

Check Account Balance ​

typescript
async function checkAccountBalance(accountId: string) {
  const account = await flutterwave.api.virtualAccounts.retrieve(accountId);

  console.log('Account Balance:', account.balance);
  console.log('Currency:', account.currency);
  console.log('Status:', account.status);

  return account.balance;
}

Monitor Account Activity ​

typescript
interface AccountActivity {
  accountId: string;
  lastChecked: Date;
  previousBalance: number;
  currentBalance: number;
  difference: number;
}

async function monitorAccount(accountId: string): Promise<AccountActivity> {
  // Get previous balance from your database
  const previousBalance = await getPreviousBalance(accountId);

  // Get current balance
  const account = await flutterwave.api.virtualAccounts.retrieve(accountId);
  const currentBalance = account.balance;

  const activity: AccountActivity = {
    accountId,
    lastChecked: new Date(),
    previousBalance,
    currentBalance,
    difference: currentBalance - previousBalance,
  };

  // Update balance in your database
  await updateStoredBalance(accountId, currentBalance);

  if (activity.difference > 0) {
    console.log(`Account ${accountId} received ${activity.difference}`);
    // Trigger notification
  }

  return activity;
}

async function getPreviousBalance(accountId: string): Promise<number> {
  // Your database logic
  return 0;
}

async function updateStoredBalance(
  accountId: string,
  balance: number,
): Promise<void> {
  // Your database logic
  console.log(`Updated balance for ${accountId}: ${balance}`);
}

Error Handling ​

Robust Account Creation ​

typescript
import {
  BadRequestException,
  UnauthorizedRequestException,
} from 'flutterwave-node-v4';

async function safeCreateVirtualAccount(accountData: any) {
  try {
    const account = await flutterwave.api.virtualAccounts.create(accountData);

    console.log('Virtual account created successfully');
    console.log('Account Number:', account.account_number);

    return {
      success: true,
      account,
    };
  } catch (error) {
    if (error instanceof BadRequestException) {
      console.error('Invalid account data:', error.message);
      return {
        success: false,
        error: 'Invalid account details',
        message: 'Please check the customer information and try again',
      };
    } else if (error instanceof UnauthorizedRequestException) {
      console.error('Authentication failed:', error.message);
      return {
        success: false,
        error: 'Authentication error',
        message: 'Please verify your credentials',
      };
    } else {
      console.error('Account creation failed:', error);
      return {
        success: false,
        error: 'Account creation failed',
        message: 'An unexpected error occurred',
      };
    }
  }
}

// Usage
const result = await safeCreateVirtualAccount({
  type: 'static',
  reference: `safe-va-${Date.now()}`,
  customer: {
    name: {
      first: 'Test',
      last: 'User',
    },
    email: 'test@example.com',
  },
  currency: 'NGN',
});

if (result.success) {
  console.log('Account created:', result.account.account_number);
} else {
  console.error('Failed:', result.message);
}

Best Practices ​

1. Use Unique References ​

typescript
function generateVirtualAccountReference(prefix: string = 'va'): string {
  const timestamp = Date.now();
  const random = Math.random().toString(36).substring(7);
  return `${prefix}-${timestamp}-${random}`;
}

const account = await flutterwave.api.virtualAccounts.create({
  reference: generateVirtualAccountReference('subscription'),
  // ... other fields
});

2. Store Account Details ​

typescript
interface StoredAccount {
  accountId: string;
  accountNumber: string;
  bankName: string;
  customerId: string;
  reference: string;
  createdAt: Date;
  metadata: any;
}

async function createAndStoreVirtualAccount(
  accountData: any,
): Promise<StoredAccount> {
  const account = await flutterwave.api.virtualAccounts.create(accountData);

  const stored: StoredAccount = {
    accountId: account.id,
    accountNumber: account.account_number,
    bankName: account.bank_name,
    customerId: accountData.customer.email,
    reference: account.reference,
    createdAt: new Date(),
    metadata: account.meta,
  };

  // Save to database
  await saveAccountToDatabase(stored);

  return stored;
}

async function saveAccountToDatabase(account: StoredAccount): Promise<void> {
  // Your database logic
  console.log('Saved account:', account.accountNumber);
}

3. Implement Expiry Alerts ​

typescript
async function createAccountWithExpiryAlert(accountData: any) {
  const account = await flutterwave.api.virtualAccounts.create(accountData);

  if (accountData.type === 'dynamic' && accountData.expiry) {
    // Schedule alert before expiry
    const alertTime = new Date();
    alertTime.setHours(alertTime.getHours() + accountData.expiry - 1);

    scheduleExpiryAlert(account.id, alertTime);
  }

  return account;
}

function scheduleExpiryAlert(accountId: string, alertTime: Date): void {
  const delay = alertTime.getTime() - Date.now();

  setTimeout(() => {
    console.log(`Alert: Account ${accountId} expires in 1 hour!`);
    // Send notification to customer
  }, delay);
}

4. Validate Customer Data ​

typescript
function validateCustomerData(customer: any): void {
  if (!customer.name?.first || !customer.name?.last) {
    throw new Error('Customer name is required');
  }

  if (!customer.email) {
    throw new Error('Customer email is required');
  }

  // Email validation
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (!emailRegex.test(customer.email)) {
    throw new Error('Invalid email format');
  }
}

// Usage
try {
  validateCustomerData({
    name: { first: 'John', last: 'Doe' },
    email: 'john@example.com',
  });

  // Proceed with account creation
} catch (error) {
  console.error('Validation failed:', error.message);
}

Released under the MIT License.