Reconciling Paystack Against Your Bank Statement
Download your bank statement as CSV, parse the deposits, and match each deposit against your Paystack settlement records by amount and approximate date. Allow a 1-3 business day tolerance for bank processing delays. Flag deposits that do not match any settlement (unexpected income) and settlements that have no matching deposit (missing money). Run this reconciliation weekly or monthly.
Why Bank Reconciliation Matters
You have two layers of reconciliation before this one. The daily reconciliation job confirms your ledger matches Paystack. The settlement reconciliation confirms which transactions Paystack settled. But neither confirms the money actually arrived in your bank.
Bank reconciliation closes the loop. It answers: "For every settlement Paystack says they sent, did a matching deposit actually appear in our bank account?" This catches:
- Bank processing errors where a deposit was lost or misrouted.
- Paystack settlement errors where the amount deposited differs from the settlement record.
- Unexpected deposits that do not correspond to any Paystack settlement (could be refunds, transfers from other sources, or errors).
- Missing deposits where Paystack shows a settlement but your bank has no record of receiving it.
Getting Your Bank Statement Data
Most Nigerian and African banks offer statement downloads in CSV, Excel, or PDF format. CSV is the easiest to parse programmatically. If your bank only provides PDF statements, you will need a PDF parsing step (or switch to CSV export if available).
For some banks, you can also access statement data through banking APIs or open banking platforms. This is the ideal approach for automation but depends on your bank and country.
// Parse a bank statement CSV
var fs = require('fs');
function parseBankStatement(filePath) {
var content = fs.readFileSync(filePath, 'utf-8');
var lines = content.split('
');
// Skip header row
var header = lines[0].split(',');
// Find column indices (these vary by bank)
var dateCol = header.indexOf('Date') !== -1 ? header.indexOf('Date') : 0;
var creditCol = header.indexOf('Credit') !== -1 ? header.indexOf('Credit') : header.indexOf('Deposit');
var descCol = header.indexOf('Description') !== -1 ? header.indexOf('Description') : header.indexOf('Narration');
var deposits = [];
for (var i = 1; i < lines.length; i++) {
var cols = lines[i].split(',');
if (cols.length < 3) continue;
var creditStr = cols[creditCol] ? cols[creditCol].replace(/[^0-9.]/g, '') : '';
var creditAmount = parseFloat(creditStr);
if (creditAmount > 0) {
deposits.push({
date: new Date(cols[dateCol].trim()),
amount: Math.round(creditAmount * 100), // Convert to kobo
description: cols[descCol] ? cols[descCol].trim() : '',
raw_line: lines[i],
});
}
}
return deposits;
}
Column names and date formats vary by bank. You will need to adjust the parsing logic for your specific bank's CSV format. Test with a real statement before relying on the parser.
The Matching Algorithm
Match bank deposits against your settlement records. The match criteria are: amount matches exactly (or within a small tolerance) and the date is within a few business days of the settlement date.
// Match bank deposits against settlements
async function reconcileBankStatement(deposits) {
// Fetch recent settlements from our records
var settlements = await db.query(
'SELECT id, total_amount, currency, settled_date, paystack_settlement_id '
+ 'FROM settlements '
+ 'WHERE settled_date >= NOW() - INTERVAL '45 days' '
+ 'AND bank_matched = false '
+ 'ORDER BY settled_date'
);
var results = {
matched: [],
unmatched_deposits: [],
unmatched_settlements: [],
};
var settlementList = settlements.rows.slice();
for (var i = 0; i < deposits.length; i++) {
var deposit = deposits[i];
var matched = false;
for (var j = 0; j < settlementList.length; j++) {
var settlement = settlementList[j];
// Amount match (allow small tolerance for bank rounding)
var amountDiff = Math.abs(deposit.amount - settlement.total_amount);
if (amountDiff > 100) continue; // More than 1 naira difference
// Date match (allow 3 business days)
var daysDiff = Math.abs(deposit.date - new Date(settlement.settled_date));
var businessDays = daysDiff / (24 * 60 * 60 * 1000);
if (businessDays > 5) continue; // Allow up to 5 calendar days (3 business)
// Match found
results.matched.push({
deposit: deposit,
settlement: settlement,
amount_diff: amountDiff,
days_diff: Math.round(businessDays),
});
// Mark settlement as matched
await db.query(
'UPDATE settlements SET bank_matched = true, bank_match_date = $1 WHERE id = $2',
[deposit.date, settlement.id]
);
settlementList.splice(j, 1);
matched = true;
break;
}
if (!matched) {
// Check if the description mentions Paystack
var isPaystack = deposit.description.toLowerCase().indexOf('paystack') !== -1;
results.unmatched_deposits.push({
deposit: deposit,
likely_paystack: isPaystack,
});
}
}
// Remaining unmatched settlements
results.unmatched_settlements = settlementList;
return results;
}
Common Mismatch Scenarios
Timing differences. A settlement processed on Friday may not appear in your bank until Monday or Tuesday. Weekend and public holiday delays are the most common reason for apparent mismatches. Widen your date tolerance window and re-check after a few business days before escalating.
Bank fees. Some banks charge fees for receiving deposits. If Paystack sends NGN 4,925,000 but your bank charges a NGN 500 incoming transfer fee, the deposit shows as NGN 4,924,500. Set a tolerance in your matching algorithm to account for this. Track these bank fees separately for expense reporting.
Consolidated deposits. Some banks or Paystack configurations may combine multiple settlements into a single bank deposit. If your bank shows one deposit of NGN 9,850,000 but you have two settlements of NGN 4,925,000 each, your matching algorithm needs to handle this. One approach: if an unmatched deposit equals the sum of two unmatched settlements with similar dates, flag it as a potential consolidation for manual review.
Different description formats. The bank statement description might say "PAYSTACK SETTLEMENT" or "PS/BATCH/12345" or just a generic transfer reference. Map known description patterns to Paystack settlements. If the description contains a batch ID, cross-reference it with the Paystack settlement ID.
Refund deductions. Paystack may deduct refunds from settlement amounts. A settlement of NGN 4,925,000 minus a NGN 50,000 refund results in a deposit of NGN 4,875,000. Your matching should check for refund deductions when the deposit is slightly less than the settlement. See the refund accounting guide for more detail.
Investigating Unmatched Items
// Generate investigation report for unmatched items
async function generateInvestigationReport(results) {
var report = 'Bank Reconciliation Investigation Report
';
if (results.unmatched_deposits.length > 0) {
report += 'UNMATCHED BANK DEPOSITS (money in bank, no settlement match):
';
for (var i = 0; i < results.unmatched_deposits.length; i++) {
var d = results.unmatched_deposits[i];
report += ' ' + d.deposit.date.toISOString().split('T')[0]
+ ' ' + formatCurrency(d.deposit.amount, 'NGN')
+ ' ' + d.deposit.description
+ (d.likely_paystack ? ' [LIKELY PAYSTACK]' : '') + '
';
}
report += '
';
}
if (results.unmatched_settlements.length > 0) {
report += 'UNMATCHED SETTLEMENTS (settlement recorded, no bank deposit):
';
for (var j = 0; j < results.unmatched_settlements.length; j++) {
var s = results.unmatched_settlements[j];
var age = Math.round((Date.now() - new Date(s.settled_date)) / 86400000);
report += ' ' + s.settled_date
+ ' ' + formatCurrency(s.total_amount, s.currency)
+ ' Paystack ID: ' + s.paystack_settlement_id
+ ' Age: ' + age + ' days'
+ (age > 5 ? ' [INVESTIGATE]' : ' [MAY BE IN TRANSIT]') + '
';
}
report += '
';
}
report += 'MATCHED: ' + results.matched.length + '
';
report += 'UNMATCHED DEPOSITS: ' + results.unmatched_deposits.length + '
';
report += 'UNMATCHED SETTLEMENTS: ' + results.unmatched_settlements.length + '
';
return report;
}
For unmatched settlements older than 5 business days, contact Paystack support with the settlement ID and ask for the payment reference or proof of deposit. For unmatched bank deposits that mention Paystack, check whether they correspond to a settlement from a different date range than you searched.
How Much to Automate
Full automation of bank reconciliation is difficult because bank statement formats are inconsistent and many banks do not offer API access. A practical approach:
- Automate parsing: Build a parser for your bank's specific CSV format. This saves the most time.
- Automate matching: The matching algorithm handles the straightforward cases (exact amount, close date).
- Manual review for edge cases: Consolidated deposits, amount mismatches, and old unmatched items need human judgment.
- Automate reporting: Generate the investigation report automatically and email it to your finance team.
If your bank offers an API (some Nigerian fintech banks do, as do some traditional banks through open banking), you can fully automate the statement download and eliminate the manual CSV step entirely.
For Kenyan businesses that receive Paystack settlements via M-Pesa, see the M-Pesa reconciliation guide which covers a similar process with M-Pesa-specific details.
When to Run Bank Reconciliation
Bank reconciliation has a natural cadence:
- Weekly: Good for businesses processing 100+ transactions per day. Catches issues while they are fresh and easy to investigate.
- Monthly: The minimum recommended frequency. Aligns with month-end close procedures. See the month-end close guide.
- Ad hoc: Run whenever you notice unexpected bank balance changes or when preparing for financial audits.
Schedule it after your Paystack settlement data is complete for the period. If you reconcile the first week of August, your July settlements should all be finalized. Allow a few extra business days for the last settlement of the month to arrive in your bank.
Key Takeaways
- ✓Bank reconciliation is the final layer of verification. It confirms money actually arrived in your bank account, not just that Paystack says it was sent.
- ✓Match bank deposits against Paystack settlements by amount and approximate date. Allow 1-3 business day tolerance for processing delays.
- ✓Three outcomes per deposit: matched (both systems agree), unmatched deposit (money in bank but no settlement record), unmatched settlement (settlement recorded but no bank deposit).
- ✓Common causes of mismatches: weekend and holiday delays, bank fees reducing the deposited amount, multiple settlements consolidated into one bank transfer, and description format differences.
- ✓Automate what you can (parsing, matching) but expect some manual reconciliation for edge cases.
- ✓Run bank reconciliation at least monthly. Weekly is better for businesses processing 100+ transactions per day.
Frequently Asked Questions
- What if my bank statement is in PDF format only?
- Use a PDF parsing library or service to extract the transaction data. Tools like Tabula (open source) or commercial PDF extraction services can convert PDF tables to CSV. Alternatively, check if your bank offers CSV or Excel downloads through their internet banking portal.
- How much tolerance should I allow for amount matching?
- Start with 100 units in the smallest denomination (NGN 1, KES 1). This covers small rounding differences. If your bank charges deposit fees, increase the tolerance to cover the typical fee amount. Track the tolerance amounts so you can reconcile the difference (it is usually a bank fee).
- What if Paystack sends multiple settlements in one day?
- Match each settlement individually against bank deposits. Each settlement should correspond to one deposit. If your bank consolidates multiple transfers into one deposit, you will need to check whether the deposit amount equals the sum of multiple settlements.
- Should I match by amount only or also by description?
- Match primarily by amount and date. Use the description as a secondary check. Bank descriptions are inconsistent (they might say "PAYSTACK", "PS", or a batch reference), so relying solely on description matching would miss many legitimate deposits.
- What do I do if a settlement never arrives in my bank?
- First, verify the settlement exists in the Paystack dashboard. Then contact Paystack support with the settlement ID and ask for the transfer reference. Use that reference to trace the transfer through your bank. If Paystack confirms the transfer was sent, contact your bank with the reference to trace where the funds went.
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