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.
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:
Each of these scenarios requires your system to respond correctly. For the overall payment verification pipeline, see the complete verification and reconciliation guide.
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,
};
}
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
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.
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.
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:
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.
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.
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:
For the complete checkout optimization picture, see the broader verification and reconciliation guide.
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.
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