Skip to main content
Dedicated Virtual Accounts (DVAs) allow you to assign unique bank account numbers to your customers. This enables direct bank transfers and automatic payment reconciliation.

Overview

Virtual accounts provide:
  • Unique account numbers for each customer
  • Automatic payment reconciliation
  • Support for multiple banks
  • Real-time payment notifications via webhooks
  • Custom split configurations
Virtual accounts are available in Nigeria (NGN) and Ghana (GHS). Contact Paystack for availability in other regions.

Create a Virtual Account

Assign a dedicated account number to a customer.
1

Initialize the SDK

import { Paystack } from '@efobi/paystack';

const paystack = new Paystack(process.env.PAYSTACK_SECRET_KEY!);
2

Create the virtual account

const { data, error } = await paystack.virtualAccount.create({
  customer: 'CUS_xxxxxxxxxx', // Customer code
  preferred_bank: 'wema-bank', // Optional: specific bank slug
  first_name: 'John',
  last_name: 'Doe',
  phone: '+2348012345678'
});

if (error) {
  console.error('Creation failed:', error);
  return;
}

console.log('Virtual account created!');
console.log('Bank:', data.data.bank.name);
console.log('Account Name:', data.data.account_name);
console.log('Account Number:', data.data.account_number);
console.log('Customer:', data.data.customer.email);
3

Display to customer

// Show the account details to your customer
const accountInfo = {
  bank: data.data.bank.name,
  accountNumber: data.data.account_number,
  accountName: data.data.account_name
};

// Store in your database for future reference
await db.virtualAccounts.create({
  customerId: customer.id,
  accountId: data.data.id,
  accountNumber: data.data.account_number,
  bankSlug: data.data.bank.slug
});

Create Parameters

customer
string
required
Customer code or email address
preferred_bank
string
Slug of preferred bank provider (e.g., wema-bank, titan-paystack)
subaccount
string
Subaccount code for routing payments
split_code
string
Split code for payment splitting
first_name
string
Customer’s first name (for account name generation)
last_name
string
Customer’s last name (for account name generation)
phone
string
Customer’s phone number

List Virtual Accounts

Retrieve all virtual accounts on your integration.
const { data, error } = await paystack.virtualAccount.list({
  perPage: 50,
  page: 1,
  active: true,
  currency: 'NGN'
});

if (data) {
  console.log(`Found ${data.meta.total} virtual accounts`);
  
  data.data.forEach(account => {
    console.log(`${account.account_number}:`, {
      customer: account.customer.email,
      bank: account.bank.name,
      active: account.active,
      assigned: account.assigned
    });
  });
}

List Parameters

perPage
number
default:"50"
Number of accounts per page
page
number
default:"1"
Page number to retrieve
active
boolean
Filter by active status
currency
string
Filter by currency: NGN or GHS

Fetch Single Virtual Account

Get details of a specific virtual account.
const accountId = '123456';

const { data, error } = await paystack.virtualAccount.fetch(accountId);

if (data) {
  console.log('Account details:', {
    bank: data.data.bank.name,
    accountNumber: data.data.account_number,
    accountName: data.data.account_name,
    customer: data.data.customer.email,
    active: data.data.active,
    currency: data.data.currency,
    splitConfig: data.data.split_config
  });
}

Assign Virtual Account

Assign a dedicated virtual account to a customer.
const { data, error } = await paystack.virtualAccount.assign({
  email: 'customer@email.com',
  first_name: 'John',
  last_name: 'Doe',
  phone: '+2348012345678',
  preferred_bank: 'wema-bank',
  country: 'NG'
});

if (data && data.status) {
  console.log('Virtual account assigned successfully!');
} else if (error) {
  console.error('Assignment failed:', error.data?.message);
}
Use the assign method when you want Paystack to handle customer creation automatically. Use create when you already have a customer code.

Requery Virtual Account

Manually check for new transactions on a virtual account.
const { data, error } = await paystack.virtualAccount.requery({
  account_number: '1234567890',
  provider_slug: 'wema-bank'
});

if (data && data.status) {
  console.log('Account requeried successfully');
  console.log('Message:', data.message);
}
Requerying is useful when you suspect a payment was made but not automatically detected. Most payments are detected automatically.

Deactivate Virtual Account

Deactivate a virtual account when it’s no longer needed.
const accountId = '123456';

const { data, error } = await paystack.virtualAccount.deactivate(accountId);

if (data) {
  console.log('Account deactivated');
  console.log('Active status:', data.data.active); // Should be false
  
  // Update your database
  await db.virtualAccounts.update(accountId, { active: false });
}
Deactivating an account prevents it from receiving payments. This action cannot be undone through the API.

Split Configuration

Add or remove split configurations from virtual accounts.

Add Split

Route payments to multiple accounts automatically.
const { data, error } = await paystack.virtualAccount.addSplit({
  account_number: '1234567890',
  split_code: 'SPL_xxxxxxxxxx'
});

if (data) {
  console.log('Split added successfully!');
  console.log('Split config:', data.data.split_config);
}

Remove Split

Remove split configuration from a virtual account.
const { data, error } = await paystack.virtualAccount.removeSplit({
  account_number: '1234567890'
});

if (data) {
  console.log('Split removed');
  console.log('Split config:', data.data.split_config); // Empty object
}

Available Banks

Fetch list of available bank providers for virtual accounts.
const { data, error } = await paystack.virtualAccount.fetchBanks();

if (data) {
  console.log('Available banks:');
  
  data.data.forEach(bank => {
    console.log(`${bank.bank_name}:`, {
      slug: bank.provider_slug,
      bankId: bank.bank_id,
      id: bank.id
    });
  });
}
Use the provider_slug when creating virtual accounts with a specific bank preference.

Webhook Integration

Listen for payments to virtual accounts:
paystack.webhook
  .on('dedicatedaccount.assign.success', (data) => {
    console.log('Virtual account assigned!');
    
    const account = data.dedicated_account;
    // Store account details
    db.virtualAccounts.create({
      customerId: data.customer.id,
      accountNumber: account.account_number,
      bankName: account.bank.name,
      active: account.active
    });
  })
  .on('dedicatedaccount.assign.failed', (data) => {
    console.log('Assignment failed');
    // Handle failure
    notifyAdmin('virtual_account_assignment_failed', data);
  })
  .on('charge.success', (data) => {
    // Check if payment is from a virtual account
    if (data.channel === 'dedicated_nuban') {
      console.log('Payment received via virtual account!');
      console.log('Amount:', data.amount / 100);
      console.log('Customer:', data.customer.email);
      
      // Credit customer's account
      creditCustomerAccount(data.customer.email, data.amount / 100);
    }
  });

Best Practices

  1. Store account details in your database with customer records
  2. Verify customer identity before creating virtual accounts
  3. Display account info prominently to customers
  4. Handle webhook events for automatic payment processing
  5. Monitor account status and reactivate when needed
  6. Use split configurations for automatic payment routing
  7. Provide payment instructions to customers
  8. Test thoroughly in test mode before going live

Common Scenarios

Wallet Funding

async function createWalletAccount(userId: string) {
  const user = await db.users.findById(userId);
  
  // Get or create Paystack customer
  let customer = await db.customers.findByEmail(user.email);
  
  if (!customer) {
    // Create customer first
    customer = await createPaystackCustomer(user);
  }

  // Create virtual account
  const { data, error } = await paystack.virtualAccount.create({
    customer: customer.code,
    first_name: user.firstName,
    last_name: user.lastName,
    phone: user.phone
  });

  if (error) {
    throw new Error('Failed to create wallet account');
  }

  // Store account details
  await db.walletAccounts.create({
    userId,
    accountNumber: data.data.account_number,
    bankName: data.data.bank.name,
    accountName: data.data.account_name,
    accountId: data.data.id
  });

  return {
    accountNumber: data.data.account_number,
    bankName: data.data.bank.name,
    accountName: data.data.account_name
  };
}

// Handle incoming payment
paystack.webhook.on('charge.success', async (data) => {
  if (data.channel === 'dedicated_nuban') {
    const account = await db.walletAccounts.findByAccountNumber(
      data.metadata.account_number
    );
    
    if (account) {
      await db.walletTransactions.create({
        userId: account.userId,
        amount: data.amount / 100,
        reference: data.reference,
        type: 'credit',
        status: 'completed'
      });
      
      await db.users.incrementBalance(account.userId, data.amount / 100);
      
      await notifyUser(account.userId, 'wallet_funded', {
        amount: data.amount / 100
      });
    }
  }
});

Invoice Payments

async function generateInvoiceAccount(
  invoiceId: string,
  customerEmail: string
) {
  const invoice = await db.invoices.findById(invoiceId);
  
  // Create temporary customer for the invoice
  const customer = await createOrGetCustomer({
    email: customerEmail,
    metadata: { invoice_id: invoiceId }
  });

  // Create virtual account with split for commission
  const { data, error } = await paystack.virtualAccount.create({
    customer: customer.code,
    split_code: process.env.INVOICE_SPLIT_CODE // Pre-configured split
  });

  if (error) {
    throw new Error('Failed to generate payment account');
  }

  // Store and send to customer
  await db.invoices.update(invoiceId, {
    paymentAccountNumber: data.data.account_number,
    paymentBankName: data.data.bank.name,
    paymentAccountName: data.data.account_name
  });

  await sendInvoiceEmail(customerEmail, {
    invoiceId,
    amount: invoice.amount,
    accountNumber: data.data.account_number,
    bankName: data.data.bank.name,
    accountName: data.data.account_name
  });

  return data.data;
}

Subscription Renewals

async function setupSubscriptionAccount(subscriptionId: string) {
  const subscription = await db.subscriptions.findById(subscriptionId);
  const user = await db.users.findById(subscription.userId);

  // Create or reuse virtual account
  let virtualAccount = await db.virtualAccounts.findByUserId(user.id);
  
  if (!virtualAccount) {
    const { data } = await paystack.virtualAccount.create({
      customer: user.paystackCustomerCode,
      first_name: user.firstName,
      last_name: user.lastName,
      phone: user.phone
    });
    
    virtualAccount = await db.virtualAccounts.create({
      userId: user.id,
      accountNumber: data.data.account_number,
      bankName: data.data.bank.name
    });
  }

  // Link to subscription
  await db.subscriptions.update(subscriptionId, {
    paymentAccountId: virtualAccount.id
  });

  return virtualAccount;
}

// Auto-renew when payment is received
paystack.webhook.on('charge.success', async (data) => {
  if (data.channel === 'dedicated_nuban' && data.metadata.subscription_id) {
    const subscriptionId = data.metadata.subscription_id;
    const amount = data.amount / 100;
    
    const subscription = await db.subscriptions.findById(subscriptionId);
    
    if (amount >= subscription.price) {
      await db.subscriptions.update(subscriptionId, {
        status: 'active',
        lastPaymentDate: new Date(),
        nextBillingDate: calculateNextBilling(subscription)
      });
      
      await notifyUser(subscription.userId, 'subscription_renewed');
    }
  }
});

Error Handling

try {
  const { data, error } = await paystack.virtualAccount.create({
    customer: customerCode
  });

  if (error) {
    // Handle validation errors
    if (error.message.includes('customer')) {
      return { 
        success: false, 
        message: 'Invalid customer code' 
      };
    }
    
    return { success: false, message: error.message };
  }

  if (!data.status) {
    // Handle API errors
    return { success: false, message: data.message };
  }

  return { success: true, account: data.data };

} catch (err) {
  console.error('Unexpected error:', err);
  return { success: false, message: 'Failed to create virtual account' };
}