McTaba Labs logo
Software EngineeringThe WhiteboardProjectsEventsMasterclassesWebinarsLearn
Book Call

500+ developers already in the community

Get answers, find accountability partners, and stay ahead

Join Discord →
McTaba Labs logo

Educating 1 million students in tech by 2030.

Programs

  • HighSchool Graduates Launchpad
  • AI & Business Automation
  • Corporate Training
  • McTaba Story & Mission
  • Full-Stack Curriculum
  • Course Pricing & Plans
  • Frequently Asked Questions

Resources

  • Join Discord Community
  • Tech Events in Nairobi
  • Masterclasses
  • Free Resources
  • Learn
  • Campus
  • Africa Tech Talent Report
  • Partner With Us
  • Start a Project
  • Our Work

Contact Us

  • 124 Manyani East Road, Lavington, Nairobi
  • +254727795603
  • Book a Discovery Call

Office Hours:

Mon-Fri: 9am - 2pm EAT

Sat: 10am - 12pm EAT

© 2027 McTaba Labs - A product of Edtech Industries. All rights reserved.

Privacy PolicyTerms of ServiceRefund Policy
  1. Learn
  2. Paystack for African Developers
  3. Reconciling Dedicated Virtual Account Deposits
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 22, 2026

Reconciling Dedicated Virtual Account Deposits

To reconcile Dedicated Virtual Account deposits, listen for the dedicatedaccount.assign.success webhook when accounts are created and the charge.success webhook when deposits land. Match each deposit to a customer using the DVA account number or customer code. Check the amount against what was expected. Handle overpayments and underpayments with clear business logic. Run daily reconciliation against the Paystack transaction list to catch any missed webhooks.

In this article

  1. 1. How DVA Deposits Flow Through Your System
  2. 2. Matching Deposits to Customer Records
  3. 3. Handling Overpayment and Underpayment
  4. 4. Daily Reconciliation Job
  5. 5. Handling Multiple Deposits and Accumulation
  6. 6. Debugging DVA Reconciliation Issues
  7. 7. Stay Up to Date

How DVA Deposits Flow Through Your System

When you assign a Dedicated Virtual Account to a customer, Paystack generates a permanent bank account number. The customer transfers money to this number from any bank. Here is the flow:

  1. Customer transfers money to their DVA account number (e.g., via mobile banking app, USSD, or bank branch).
  2. The receiving bank notifies Paystack that a deposit landed.
  3. Paystack matches the deposit to the DVA and fires a charge.success webhook to your server.
  4. Your server processes the webhook, identifies the customer, and credits their account or fulfills their order.
  5. On the next settlement cycle, Paystack settles the deposit amount (minus fees) to your bank account.

The critical engineering challenge is step 4: correctly matching the deposit to the customer and handling the amount. Unlike card or USSD payments where you control the exact amount, DVA deposits are customer-initiated transfers. The customer can send any amount at any time.

// Webhook handler for DVA deposits
app.post('/webhooks/paystack', async (req, res) => {
  // Verify webhook signature first (omitted for brevity)
  const event = req.body;

  if (event.event === 'charge.success') {
    const data = event.data;

    // Check if this is a DVA deposit
    if (data.channel === 'dedicated_nuban') {
      const customerCode = data.customer.customer_code;
      const amount = data.amount; // in kobo
      const reference = data.reference;

      // Find the customer in your database
      const customer = await db.query(
        'SELECT id, email FROM customers WHERE paystack_customer_code = $1',
        [customerCode]
      );

      if (customer.rows.length > 0) {
        // Credit the customer's wallet or process the payment
        await processDeposit(customer.rows[0].id, amount, reference);
      } else {
        // Unknown customer deposit. Log for manual review.
        console.log('DVA deposit from unknown customer: ' + customerCode + ', amount: ' + amount);
      }
    }
  }

  res.sendStatus(200);
});

Matching Deposits to Customer Records

You need a reliable way to connect each DVA deposit to a customer in your system. There are two primary matching strategies:

Match by customer code: When you create a DVA through the Paystack API, you associate it with a Paystack customer. The webhook payload includes the customer.customer_code. Store this code in your customer table when you create the DVA, and match on it when deposits arrive.

// When creating the DVA, store the mapping
async function assignDVA(userId, email) {
  // First, create or fetch the Paystack customer
  const customerResponse = await fetch('https://api.paystack.co/customer', {
    method: 'POST',
    headers: {
      Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ email }),
  });
  const customerData = await customerResponse.json();

  // Create the DVA
  const dvaResponse = await fetch('https://api.paystack.co/dedicated_account', {
    method: 'POST',
    headers: {
      Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      customer: customerData.data.id,
      preferred_bank: 'wema-bank',
    }),
  });
  const dvaData = await dvaResponse.json();

  // Store the mapping
  await db.query(
    'UPDATE customers SET paystack_customer_code = $1, dva_account_number = $2, dva_bank = $3 WHERE id = $4',
    [customerData.data.customer_code, dvaData.data.account_number, dvaData.data.bank.name, userId]
  );

  return dvaData.data;
}

Match by account number: The webhook payload includes the DVA account number. If you stored the account number in your customer record when you created the DVA, you can match on that instead.

// Alternative: match by DVA account number
const accountNumber = event.data.authorization.receiver_bank_account_number;
const customer = await db.query(
  'SELECT id FROM customers WHERE dva_account_number = $1',
  [accountNumber]
);

Use both matching strategies as fallbacks. Try the customer code first (more reliable), and fall back to the account number if needed.

Get weekly developer tips

Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.

No spam. Unsubscribe anytime.

Handling Overpayment and Underpayment

With DVAs, the customer controls the transfer amount. They might send exactly what you expected, more, or less. Your business logic needs to handle all three cases.

Exact match: The customer sends exactly the amount due. Process normally.

Overpayment: The customer sends more than required. Options:

  • Credit the excess to their wallet balance for future purchases.
  • Apply the excess to their next invoice automatically.
  • Initiate a refund for the excess amount via Paystack Transfers.

Underpayment: The customer sends less than required. Options:

  • Accept the partial payment and mark the order as partially paid. Notify the customer of the remaining balance.
  • Reject the payment entirely and refund the amount. Notify the customer to send the correct amount.
  • Credit their wallet with the amount they sent. Do not fulfill the order until the full amount is reached.
async function processDeposit(customerId, amountInKobo, reference) {
  // Find any pending invoice for this customer
  const invoice = await db.query(
    'SELECT id, amount_due FROM invoices WHERE customer_id = $1 AND status = $2 ORDER BY created_at ASC LIMIT 1',
    [customerId, 'pending']
  );

  if (invoice.rows.length === 0) {
    // No pending invoice. Credit to wallet.
    await db.query(
      'UPDATE customers SET wallet_balance = wallet_balance + $1 WHERE id = $2',
      [amountInKobo, customerId]
    );
    console.log('Credited ' + (amountInKobo / 100) + ' to wallet for customer ' + customerId);
    return;
  }

  const amountDue = invoice.rows[0].amount_due;

  if (amountInKobo >= amountDue) {
    // Full payment (or overpayment)
    await db.query(
      'UPDATE invoices SET status = $1, paid_at = $2, payment_reference = $3 WHERE id = $4',
      ['paid', new Date(), reference, invoice.rows[0].id]
    );

    if (amountInKobo > amountDue) {
      // Credit the excess to wallet
      const excess = amountInKobo - amountDue;
      await db.query(
        'UPDATE customers SET wallet_balance = wallet_balance + $1 WHERE id = $2',
        [excess, customerId]
      );
      console.log('Overpayment of ' + (excess / 100) + ' credited to wallet');
    }
  } else {
    // Underpayment
    const remaining = amountDue - amountInKobo;
    await db.query(
      'UPDATE invoices SET amount_due = $1 WHERE id = $2',
      [remaining, invoice.rows[0].id]
    );
    console.log('Partial payment. Remaining: ' + (remaining / 100));
    // Notify customer of remaining balance
  }
}

Daily Reconciliation Job

Webhooks can fail. Your server might miss a deposit notification. Run a daily reconciliation job that pulls all DVA transactions from Paystack and checks them against your records.

async function reconcileDVADeposits() {
  const yesterday = new Date(Date.now() - 86400000).toISOString().split('T')[0];
  const today = new Date().toISOString().split('T')[0];

  // Fetch all successful transactions from yesterday
  let page = 1;
  let hasMore = true;

  while (hasMore) {
    const response = await fetch(
      'https://api.paystack.co/transaction?from=' + yesterday +
      '&to=' + today +
      '&channel=dedicated_nuban' +
      '&status=success' +
      '&perPage=200' +
      '&page=' + page,
      {
        headers: {
          Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
        },
      }
    );

    const data = await response.json();

    for (const tx of data.data) {
      // Check if we already processed this deposit
      const existing = await db.query(
        'SELECT id FROM processed_deposits WHERE paystack_reference = $1',
        [tx.reference]
      );

      if (existing.rows.length === 0) {
        // Missed deposit! Process it now.
        console.log('Found unprocessed DVA deposit: ' + tx.reference + ', amount: ' + (tx.amount / 100));
        const customer = await db.query(
          'SELECT id FROM customers WHERE paystack_customer_code = $1',
          [tx.customer.customer_code]
        );

        if (customer.rows.length > 0) {
          await processDeposit(customer.rows[0].id, tx.amount, tx.reference);
          await db.query(
            'INSERT INTO processed_deposits (paystack_reference, processed_at) VALUES ($1, $2)',
            [tx.reference, new Date()]
          );
        }
      }
    }

    if (page >= data.meta.pageCount) {
      hasMore = false;
    } else {
      page++;
    }
  }
}

This job is your safety net. It catches deposits that were successful on Paystack but never processed in your system. Run it every morning and alert your team if it finds unprocessed deposits.

Handling Multiple Deposits and Accumulation

Some use cases involve customers making multiple deposits to the same DVA over time. For example:

  • Wallet top-ups: The customer deposits money whenever they want. Each deposit adds to their wallet balance.
  • Installment payments: The customer pays part of an invoice today and the rest next week.
  • Savings products: The customer makes regular deposits toward a savings goal.

For each of these, your system needs to accumulate deposits correctly:

// Track all deposits for a customer
async function recordDeposit(customerId, amountInKobo, reference) {
  // Record the individual deposit
  await db.query(
    'INSERT INTO deposits (customer_id, amount, paystack_reference, created_at) VALUES ($1, $2, $3, $4)',
    [customerId, amountInKobo, reference, new Date()]
  );

  // Update the running total
  await db.query(
    'UPDATE customers SET total_deposited = total_deposited + $1, wallet_balance = wallet_balance + $1 WHERE id = $2',
    [amountInKobo, customerId]
  );
}

Keep a deposits table that records every individual deposit. This is your audit trail. If a customer disputes their balance, you can show them every deposit they made, with amounts, dates, and Paystack references. The wallet_balance field is computed from the deposits minus any withdrawals or purchases.

For applications with high deposit volumes, ensure your webhook handler and reconciliation job are idempotent. Use the Paystack reference as a unique constraint on your deposits table to prevent double-processing.

Debugging DVA Reconciliation Issues

Common DVA reconciliation problems and how to investigate them:

Customer says they transferred but no deposit recorded: Check the Paystack dashboard for the DVA. Look for the transaction in the transaction list. If Paystack shows the deposit, the issue is on your side (missed webhook). Run the reconciliation job or manually process the deposit. If Paystack does not show the deposit, the customer's bank transfer may still be processing or may have failed.

Deposit matched to the wrong customer: Verify your customer-to-DVA mapping. Pull the transaction from Paystack and check the customer_code. Cross-reference it with your database. If the mapping is wrong, fix the customer record and reprocess the deposit.

Amount mismatch between Paystack and your records: Paystack reports amounts in kobo. Verify you are dividing by 100 correctly. Also check for currency mismatches. If the customer deposited in a different currency than expected, the amount conversion may cause discrepancies.

Duplicate deposits processed: Check your idempotency logic. Every deposit should be recorded with its Paystack reference as a unique key. If two webhook deliveries for the same deposit arrive (Paystack retries), your handler should detect the duplicate reference and skip processing.

For the full accept payments workflow, see the complete accept payments guide. For DVA setup details, see Dedicated Virtual Accounts implementation.

Stay Up to Date

Paystack continues to improve their DVA product with new features and bank partnerships. We update these guides as the API evolves.

Join the McTaba newsletter for Paystack engineering updates.

Sign up for the McTaba newsletter

Key Takeaways

  • ✓Each DVA is permanently linked to a specific customer. When a deposit arrives, you know which customer it belongs to because the DVA maps to their record in your system.
  • ✓Deposits to DVAs trigger charge.success webhooks. The webhook payload includes the DVA details, the deposited amount, and the customer information. Use this data to credit the customer in your system.
  • ✓Amounts may not always match what you expected. A customer might transfer more or less than the required amount. Your system needs rules for handling overpayment (credit the excess as balance, refund it, or apply to next invoice) and underpayment (reject, accept partial, or request the remainder).
  • ✓Run a daily reconciliation job that queries Paystack for all DVA transactions and compares them against your internal records. This catches deposits where the webhook was missed or not processed.
  • ✓Store the DVA account number and bank name in your customer record. This makes it easy to look up which customer a deposit belongs to when debugging or reconciling manually.
  • ✓DVA deposits settle to your bank account on the normal settlement cycle, not instantly. The customer transfers to the DVA, Paystack processes it, and settlement happens on the next cycle.

Frequently Asked Questions

How quickly do DVA deposits appear on Paystack after the customer transfers?
Most bank transfers to DVAs are processed within seconds to a few minutes. The exact timing depends on the sending bank and the receiving bank (the one Paystack partners with for DVAs). Paystack fires the webhook as soon as the deposit is confirmed. During bank downtime or high-volume periods, there may be delays.
Can a customer deposit any amount to their DVA?
Yes. Unlike Initialize Transaction where you set the amount, DVA deposits are customer-initiated bank transfers. The customer can send any amount they choose. Your application must handle varying amounts and decide what to do with overpayments and underpayments based on your business rules.
What happens if someone who is not the account owner deposits to a DVA?
The deposit goes through regardless of who initiated the transfer. DVAs identify the receiving account, not the sender. If you need to know who sent the money, the bank transfer details may include the sender account name, but this is not always reliable. For most use cases, you credit the DVA owner (your customer) regardless of who sent the money.
How do I handle DVA deposits for customers who have been deactivated?
If a customer is deactivated in your system but their DVA is still active, deposits will still arrive. You have two options: deactivate the DVA through the Paystack API to prevent future deposits, or keep the DVA active and credit the deposit to a holding account for manual review. The safest approach is to deactivate the DVA when you deactivate the customer.
Are DVA deposits subject to the same fees as card payments?
DVA deposit fees may differ from card payment fees. Bank transfer fees are typically structured differently from card processing fees. Check your Paystack dashboard for the specific fee structure that applies to DVA deposits on your account.

Learn Paystack Integration

KES 2,999

The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.

Start Paystack Course

Not ready? Create Free Account

Keep reading

Accepting Payments with Paystack: Complete Guide

Everything you need to accept your first payment with Paystack: choosing between Initialize Transaction and the Charge API, handling amounts in kobo, attaching metadata, setting callback URLs, and designing transaction references that keep your system clean.

Read more

Paystack Metadata: Passing Custom Data Through a Transaction

Paystack metadata lets you attach arbitrary JSON data to a transaction that flows through the entire lifecycle: from initialization to verification to webhooks. This guide shows you what to put in metadata, how to structure it for different use cases, and the limits you need to stay within.

Read more

Paystack Product API for Simple Storefronts

The Paystack Product API lets you create products with names, prices, and descriptions, then generate payment links for each one. For simple storefronts selling a handful of products, this is faster than building a full e-commerce backend. This guide covers creating products, linking them to payment pages, and knowing when to outgrow this approach.

Read more

Building a Wallet Top-Up Flow on Paystack

A step-by-step guide to building wallet top-up flows with Paystack. Initialize payment, handle webhooks, credit wallets safely, enforce minimum amounts, and prevent double-crediting in production.

Read more

Replaying Failed Paystack Webhooks Safely

When Paystack webhook processing fails, you need to replay the events from your stored payloads. This guide covers when to replay, how to build a safe replay tool, the idempotency checks that prevent double-processing, and batch replay strategies for recovering from outages.

Read more

Explore related topics

M-Pesa & African Stack

M-Pesa Daraja API tutorials, USSD development, and WhatsApp Business API guides. The African Stack that global bootcamps do not teach.

Learn to Code in Kenya

Everything a Kenyan needs to know before writing their first line of code. Career paths, real costs, hardware requirements, and honest answers about what it takes.

Learning to Code

Roadmaps, language guides, and practical advice for learning to code and launching a programming career.

Breaking Into Tech

Practical, no-nonsense guides for anyone looking to start or change their career in tech. From choosing your first language to acing interviews.

The African Stack: Skills for This Market

The skills that make Kenyan developers hard to replace: M-Pesa, USSD, WhatsApp, fintech, and building for the African market.

Bonaventure Ogeto

Bonaventure Ogeto

Founder, McTaba Labs

Bonaventure is the founder of McTaba Labs, a coding bootcamp in Nairobi, Kenya. He has spent years building products for the African market and mentoring the next generation of developers across the continent.