Handling Overpayment and Underpayment on Bank Transfer
When a customer uses Paystack bank transfer, they manually enter the transfer amount. If they send too much (overpayment), Paystack may complete the transaction for the expected amount and queue a refund for the difference. If they send too little (underpayment), the transaction may fail or remain pending. Your system should handle both cases: check the verified amount against expected, process accordingly, and record the discrepancy in your ledger.
How Paystack Bank Transfers Work
When a customer selects "Bank Transfer" on your Paystack checkout, Paystack generates a temporary bank account number (a dedicated virtual account or DVA). The customer is shown the account number, the bank name, and the exact amount to transfer. They then open their banking app, enter the details, and send the money.
The problem is the manual step. Unlike card payments where the amount is pre-filled, bank transfers require the customer to type the amount. And customers make mistakes:
- They round the amount. If they owe NGN 45,750, they might send NGN 45,000 or NGN 46,000.
- Their bank adds a transfer fee that they subtract from the amount. If the fee is NGN 52.50 and they owe NGN 50,000, they might send NGN 49,947.50.
- They type a wrong digit. NGN 50,000 becomes NGN 5,000 or NGN 500,000.
- They send the amount in a different format. Some banking apps expect the amount without decimals; others include them.
Each of these scenarios requires your system to respond correctly. For the overall payment verification pipeline, see the complete verification and reconciliation guide.
Detecting Amount Mismatches
Your standard verification flow already catches amount mismatches. When you call the verify endpoint and compare data.amount against your expected amount, any discrepancy is flagged:
// Detect overpayment or underpayment
async function checkBankTransferAmount(reference) {
var order = await db.query(
'SELECT id, expected_amount, expected_currency FROM orders WHERE paystack_reference = $1',
[reference]
);
if (order.rows.length === 0) {
return { error: 'Order not found' };
}
var expected = order.rows[0];
var response = await fetch(
'https://api.paystack.co/transaction/verify/'
+ encodeURIComponent(reference),
{
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
},
}
);
var data = await response.json();
if (!data.status || data.data.status !== 'success') {
return { status: data.data ? data.data.status : 'unknown' };
}
var actual = data.data.amount;
var difference = actual - expected.expected_amount;
if (difference === 0) {
return { match: true, amount: actual };
}
if (difference > 0) {
return {
match: false,
type: 'overpayment',
expected: expected.expected_amount,
actual: actual,
overage: difference,
currency: expected.expected_currency,
};
}
return {
match: false,
type: 'underpayment',
expected: expected.expected_amount,
actual: actual,
shortfall: Math.abs(difference),
currency: expected.expected_currency,
};
}
Handling Overpayment
The customer sent more than they owed. You have three options:
Option 1: Accept the expected amount, refund the difference. This is the cleanest approach. You grant value based on the expected amount and issue a refund for the overage. The customer gets their product and their excess money back.
// Handle overpayment: grant value, refund excess
async function handleOverpayment(order, txn, overage) {
var client = await pool.connect();
try {
await client.query('BEGIN');
// Grant value for the expected amount
await client.query(
'UPDATE orders SET status = $1, paid_at = $2 WHERE id = $3',
['paid', txn.paid_at, order.id]
);
// Record the full charge in the ledger
await client.query(
'INSERT INTO payment_ledger '
+ '(reference, event_type, direction, gross_amount, fee_amount, '
+ 'net_amount, currency, metadata) '
+ 'VALUES ($1, $2, $3, $4, $5, $6, $7, $8) '
+ 'ON CONFLICT (reference, event_type) DO NOTHING',
[
txn.reference, 'charge.success', 'credit',
txn.amount, txn.fees, txn.amount - txn.fees,
txn.currency, JSON.stringify(txn),
]
);
// Record the overpayment event
await client.query(
'INSERT INTO payment_ledger '
+ '(reference, event_type, direction, gross_amount, fee_amount, '
+ 'net_amount, currency, metadata) '
+ 'VALUES ($1, $2, $3, $4, $5, $6, $7, $8) '
+ 'ON CONFLICT (reference, event_type) DO NOTHING',
[
txn.reference, 'overpayment.detected', 'credit',
overage, 0, overage, txn.currency,
JSON.stringify({ expected: order.expected_amount, actual: txn.amount }),
]
);
await client.query('COMMIT');
// Queue a partial refund for the overage
await queueRefund(txn.reference, overage, 'Overpayment refund');
return { handled: true, refund_queued: true, refund_amount: overage };
} catch (err) {
await client.query('ROLLBACK');
throw err;
} finally {
client.release();
}
}
Option 2: Accept the full amount as credit. If your business model supports account balances (like a wallet), credit the excess to the customer's account for future purchases.
Option 3: Reject and require the exact amount. This is the strictest approach but creates friction. The customer must request a refund and retry with the correct amount. Only use this for regulated transactions where the exact amount matters legally.
Handling Underpayment
The customer sent less than they owed. This is trickier because you cannot give them full value for a partial payment (in most cases). Your options:
Option 1: Reject the payment. Do not grant value. Notify the customer that the amount was insufficient and ask them to pay the difference or retry with the full amount. Issue a refund for the partial amount.
// Handle underpayment: reject and notify
async function handleUnderpayment(order, txn, shortfall) {
// Record the partial payment in the ledger for tracking
await db.query(
'INSERT INTO payment_ledger '
+ '(reference, event_type, direction, gross_amount, fee_amount, '
+ 'net_amount, currency, metadata) '
+ 'VALUES ($1, $2, $3, $4, $5, $6, $7, $8) '
+ 'ON CONFLICT (reference, event_type) DO NOTHING',
[
txn.reference, 'underpayment.detected', 'credit',
txn.amount, txn.fees, txn.amount - txn.fees,
txn.currency,
JSON.stringify({
expected: order.expected_amount,
actual: txn.amount,
shortfall: shortfall,
}),
]
);
// Update order status
await db.query(
'UPDATE orders SET status = $1, failure_reason = $2 WHERE id = $3',
['underpaid', 'Customer sent ' + txn.amount + ', expected ' + order.expected_amount, order.id]
);
// Notify the customer
await sendEmail({
to: txn.customer.email,
subject: 'Payment amount is insufficient',
body: 'Your transfer of ' + formatCurrency(txn.amount, txn.currency)
+ ' was less than the required amount of '
+ formatCurrency(order.expected_amount, txn.currency)
+ '. Please contact support for assistance.',
});
// Alert support team
await sendAlert('Underpayment detected', {
reference: txn.reference,
expected: order.expected_amount,
actual: txn.amount,
shortfall: shortfall,
customer: txn.customer.email,
});
return { handled: true, action: 'rejected', refund_needed: true };
}
Option 2: Accept if within tolerance. If the shortfall is very small (for example, caused by a bank transfer fee), you might accept it. Define a tolerance threshold:
// Tolerance-based acceptance
function isWithinTolerance(expected, actual, currency) {
var difference = expected - actual;
// Define tolerances per currency (in smallest unit)
var tolerances = {
NGN: 10000, // NGN 100 (covers typical transfer fees)
KES: 1000, // KES 10
GHS: 500, // GHS 5
ZAR: 500, // ZAR 5
USD: 100, // USD 1
};
var tolerance = tolerances[currency] || 0;
return difference >= 0 && difference <= tolerance;
}
Be careful with tolerances. A tolerance that is too large opens you up to customers underpaying deliberately. A tolerance that is too small rejects legitimate payments where the bank deducted a small fee. Know your banks and your customers.
How Paystack Handles These Cases
Paystack's behavior for overpayments and underpayments on bank transfers depends on the specific integration and configuration. The behavior may vary and can change over time. Here is what to be aware of:
- Dedicated Virtual Accounts (DVAs): When using DVAs for bank transfers, Paystack may have specific rules about how amount mismatches are handled. Some configurations accept the transferred amount as-is; others may reject mismatches.
- Temporary accounts: For one-time transfer accounts generated during checkout, the behavior around amount mismatches depends on the integration type.
The safest approach is to never assume Paystack will handle the mismatch for you. Always verify the actual amount from the API response and apply your own business logic. Check the Paystack documentation or contact their support team for the current behavior specific to your integration type.
Regardless of how Paystack handles it, your verification code should compare amounts. The amount verification guide covers the standard three-part check. For bank transfers, you add the additional step of checking whether the mismatch falls within your tolerance or requires special handling.
Ledger Entries for Amount Mismatches
Record every overpayment and underpayment as a distinct event in your payments ledger. This gives your finance team the data they need for reconciliation and reporting:
-- Query overpayments and underpayments for a period
SELECT
reference,
event_type,
gross_amount,
currency,
metadata->>'expected' as expected_amount,
metadata->>'actual' as actual_amount,
metadata->>'shortfall' as shortfall,
created_at
FROM payment_ledger
WHERE event_type IN ('overpayment.detected', 'underpayment.detected')
AND created_at >= '2026-07-01'
ORDER BY created_at DESC;
-- Summary of mismatches by type
SELECT
event_type,
COUNT(*) as count,
SUM(gross_amount) as total_amount,
currency
FROM payment_ledger
WHERE event_type IN ('overpayment.detected', 'underpayment.detected')
AND created_at >= '2026-07-01'
GROUP BY event_type, currency;
If overpayments or underpayments happen frequently, the problem is likely in your checkout UX. Maybe the amount is not displayed clearly enough, or the transfer instructions are confusing. Fix the UX problem rather than building increasingly complex mismatch handling.
Preventing Amount Mismatches
The best approach is to prevent mismatches from happening in the first place. Design your checkout flow to minimize the chance of a customer entering the wrong amount:
- Display the amount prominently. Use large, bold text for the transfer amount. Show it as "NGN 45,750.00" with the currency and decimal places. Do not show "45750" and expect the customer to figure out the formatting.
- Add a copy button. Let the customer copy the amount to their clipboard with one tap. This eliminates typing errors entirely.
- Include the amount in the account reference. If Paystack's DVA supports it, include the amount in the reference so the customer can see it in their banking app.
- Send an SMS or email. After the customer selects bank transfer, send them an SMS with the account details and amount. They can reference it while switching to their banking app.
- Remind about bank fees. If you know that certain banks charge transfer fees, add a note: "Please transfer the exact amount of NGN 45,750.00. Your bank may charge a separate transfer fee."
For the complete checkout optimization picture, see the broader verification and reconciliation guide.
Edge Cases to Handle
Multiple partial transfers. A customer might split the amount across two transfers. The first transfer arrives but is less than expected (underpayment). The second transfer arrives later. You need to decide: do you accept the sum of both? This is complex because each transfer might generate a separate Paystack transaction. Your system needs to track partial payments per order and sum them up.
Transfer to the wrong account. The customer might transfer to a previously generated temporary account that has expired, or to a different order's account. Paystack handles this by rejecting transfers to expired accounts. If the customer contacts support, they may need a new payment link.
Currency mismatches. This is rare for bank transfers (you cannot send USD to a Naira account), but if your business operates across currencies, double-check that the currency in the verification response matches what you expected.
Same-day retry. A customer underpays, gets notified, and wants to pay the difference. You have two options: refund the partial payment and issue a new payment link for the full amount, or accept a second payment for just the shortfall. The second option is harder to reconcile but better for the customer experience.
Key Takeaways
- ✓Bank transfers are error-prone because customers type the amount manually. Typos, rounding, and bank fees can cause the transferred amount to differ from the expected amount.
- ✓Overpayment: the customer transfers more than the invoiced amount. Paystack may process the transaction for the expected amount and handle the difference, but verify your specific integration behavior.
- ✓Underpayment: the customer transfers less than expected. The transaction may fail or show a partial amount. Your system should not grant value for underpayments without explicit policy.
- ✓Always verify the actual amount via the Paystack API before granting value. Do not assume the customer sent the right amount just because the transaction exists.
- ✓Record overpayments and underpayments as distinct ledger events so your finance team can track and resolve them.
- ✓Communicate clearly with customers about exact amounts. Show the amount prominently on your checkout page and in confirmation messages.
Frequently Asked Questions
- Does Paystack automatically refund overpayments on bank transfers?
- Paystack behavior on overpayments depends on the specific integration type and configuration. Check the current Paystack documentation or contact their support for the behavior that applies to your integration. Do not assume automatic refunds. Always verify amounts in your code.
- How common are bank transfer amount mismatches?
- It depends on your customer base and the amounts involved. Businesses with round-number pricing (NGN 5,000, NGN 10,000) see fewer mismatches than those with precise amounts (NGN 45,750). Bank transfer fees also cause mismatches when customers subtract the fee from the amount they send.
- Should I set a tolerance for card payments too?
- No. Card payments are pre-filled with the exact amount. The customer cannot change the amount during a card transaction. Tolerance handling is specific to bank transfers (and potentially USSD, where manual entry is involved).
- What if the customer claims they sent the right amount but verification shows a different number?
- Ask the customer for their bank transfer receipt showing the amount and date. Compare it against what Paystack recorded. If there is a genuine discrepancy, escalate to Paystack support with both pieces of evidence. The bank transfer receipt is the customer proof; Paystack records are the payment proof.
- Can I prevent underpayments entirely by using card payments only?
- Yes, card payments eliminate manual amount entry entirely. However, removing bank transfer as an option may reduce your conversion rate, especially in markets where many customers prefer or need bank transfers. The better approach is to make bank transfer instructions very clear rather than removing the option.
Ready to build real-world apps?
Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.
Apply to the McTaba Marathon