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.
Initialize the SDK
import { Paystack } from '@efobi/paystack';
const paystack = new Paystack(process.env.PAYSTACK_SECRET_KEY!);
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);
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 code or email address
Slug of preferred bank provider (e.g., wema-bank, titan-paystack)
Subaccount code for routing payments
Split code for payment splitting
Customer’s first name (for account name generation)
Customer’s last name (for account name generation)
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
Number of accounts per page
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
- Store account details in your database with customer records
- Verify customer identity before creating virtual accounts
- Display account info prominently to customers
- Handle webhook events for automatic payment processing
- Monitor account status and reactivate when needed
- Use split configurations for automatic payment routing
- Provide payment instructions to customers
- 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' };
}