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. Month-End Close for a Business Running on Paystack
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 20, 2026

Month-End Close for a Business Running on Paystack

Month-end close for a Paystack business follows this sequence: 1) Run final reconciliation for the last day of the month. 2) Verify all settlements for the month have arrived. 3) Confirm refund and chargeback totals. 4) Calculate total fees. 5) Verify that ledger totals match Paystack dashboard totals. 6) Produce revenue, expense, and cash flow reports. 7) Lock the period to prevent retroactive changes.

In this article

  1. 1. What Month-End Close Means
  2. 2. When to Run the Close
  3. 3. Step 1: Final Reconciliation
  4. 4. Step 2: Verify All Settlements Arrived
  5. 5. Step 3: Refund and Chargeback Review
  6. 6. Step 4: Fee Calculation and Verification
  7. 7. Step 5: Cross-Check Against Paystack Dashboard
  8. 8. Step 6: Produce Financial Reports
  9. 9. Step 7: Lock the Period

What Month-End Close Means

Month-end close is the process of finalizing your financial numbers for the month. When it is done, you can say with confidence: "In July 2026, we earned X in revenue, paid Y in processing fees, issued Z in refunds, and received W in cash deposits." These numbers go to your accountant, your investors, your tax filings, and your management reports.

For a business running on Paystack, the close involves reconciling three data sources:

  1. Your payments ledger: Your internal record of every charge, refund, and fee.
  2. The Paystack dashboard/API: Paystack's record of the same transactions.
  3. Your bank/M-Pesa statement: Proof that settlement money actually arrived.

If all three agree, your numbers are solid. If they disagree, you need to investigate and resolve the differences before closing the books.

When to Run the Close

Do not run the close on the first day of the new month. Wait 3-5 business days. Here is why:

  • Transactions from the last day of the month may not be settled until 2-3 business days later.
  • The last settlement of the month may not appear in your bank until early in the new month.
  • Webhooks for transactions at the end of the month might arrive in the first day or two of the new month.

For July 2026, start the close around August 4-5. By then, all July settlements should have arrived, late webhooks should have been processed, and your daily reconciliation jobs should have flagged any issues.

// Check if the month is ready for close
async function isMonthReadyForClose(year, month) {
  var lastDay = new Date(year, month, 0); // Last day of the month
  var today = new Date();
  var businessDaysSince = countBusinessDays(lastDay, today);

  if (businessDaysSince < 3) {
    return {
      ready: false,
      reason: 'Only ' + businessDaysSince + ' business days since month end. Wait for settlements.',
    };
  }

  // Check for unresolved reconciliation issues
  var issues = await db.query(
    'SELECT COUNT(*) as count FROM reconciliation_issues '
    + 'WHERE resolved = false AND created_at >= $1 AND created_at < $2',
    [year + '-' + String(month).padStart(2, '0') + '-01',
     year + '-' + String(month + 1).padStart(2, '0') + '-01']
  );

  if (parseInt(issues.rows[0].count) > 0) {
    return {
      ready: false,
      reason: issues.rows[0].count + ' unresolved reconciliation issues.',
    };
  }

  return { ready: true };
}

function countBusinessDays(from, to) {
  var count = 0;
  var current = new Date(from);
  while (current < to) {
    current.setDate(current.getDate() + 1);
    var day = current.getDay();
    if (day !== 0 && day !== 6) count++;
  }
  return count;
}

Get weekly developer tips

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

No spam. Unsubscribe anytime.

Step 1: Final Reconciliation

Run your daily reconciliation job for every day of the month. If it has been running daily, check the results for any days that had issues. If it missed any days, run it retroactively now.

// Run reconciliation for every day of the month
async function monthlyReconciliation(year, month) {
  var firstDay = new Date(year, month - 1, 1);
  var lastDay = new Date(year, month, 0);
  var results = [];

  var current = new Date(firstDay);
  while (current <= lastDay) {
    var dateStr = current.toISOString().split('T')[0];
    var dayResult = await reconcile(dateStr);
    results.push({
      date: dateStr,
      matched: dayResult.matched,
      missing: dayResult.missing_from_ledger.length,
      mismatches: dayResult.amount_mismatches.length,
    });

    current.setDate(current.getDate() + 1);
  }

  // Summary
  var totalMissing = results.reduce(function(sum, r) { return sum + r.missing; }, 0);
  var totalMismatches = results.reduce(function(sum, r) { return sum + r.mismatches; }, 0);

  return {
    days_checked: results.length,
    total_missing: totalMissing,
    total_mismatches: totalMismatches,
    clean: totalMissing === 0 && totalMismatches === 0,
    details: results,
  };
}

If there are outstanding issues, resolve them before proceeding. The close cannot happen until reconciliation is clean for every day of the month.

Step 2: Verify All Settlements Arrived

Check that every transaction from the month has been included in a settlement and that the settlement has been deposited to your bank or M-Pesa account.

-- Unsettled transactions from the month
SELECT
  reference,
  gross_amount,
  net_amount,
  currency,
  created_at
FROM payment_ledger
WHERE event_type = 'charge.success'
  AND created_at >= '2026-07-01'
  AND created_at < '2026-08-01'
  AND settled_at IS NULL
ORDER BY created_at;

-- If any rows are returned, these transactions have not been settled.
-- Investigate: are they pending settlement, reversed, or missing?

A small number of unsettled transactions from the last 1-2 days of the month is normal. They should settle in the first few days of the new month. If transactions from mid-month are still unsettled, something is wrong.

Step 3: Refund and Chargeback Review

-- Monthly refund and chargeback summary
SELECT
  event_type,
  currency,
  COUNT(*) as count,
  SUM(gross_amount) as total_amount
FROM payment_ledger
WHERE event_type IN ('refund.full', 'refund.partial', 'chargeback.created', 'chargeback.reversed')
  AND created_at >= '2026-07-01'
  AND created_at < '2026-08-01'
GROUP BY event_type, currency
ORDER BY event_type;

Review each refund and chargeback. Confirm they are recorded correctly in the ledger. Check that the order statuses match (orders with refund ledger entries should have "refunded" or "partially_refunded" status). For chargebacks, verify the dispute status and outcome.

For detailed refund accounting, see the refund accounting guide. For chargeback details, see the chargeback accounting guide.

Step 4: Fee Calculation and Verification

-- Monthly fee summary by channel and currency
SELECT
  channel,
  currency,
  COUNT(*) as transaction_count,
  SUM(gross_amount) as total_gross,
  SUM(fee_amount) as total_fees,
  SUM(net_amount) as total_net,
  ROUND(SUM(fee_amount)::numeric * 100 / NULLIF(SUM(gross_amount), 0), 2) as effective_rate
FROM payment_ledger
WHERE event_type = 'charge.success'
  AND created_at >= '2026-07-01'
  AND created_at < '2026-08-01'
GROUP BY channel, currency
ORDER BY total_gross DESC;

Compare the total fees against what you expect based on your Paystack fee agreement. If the effective rate looks different from what you agreed to, investigate. It might be due to fee caps hitting on large transactions, a change in the fee structure, or a recording error.

For the detailed treatment of fees in your ledger, see the gross vs net recording guide.

Step 5: Cross-Check Against Paystack Dashboard

Log into the Paystack dashboard and compare key numbers against your ledger:

  • Total successful transactions count for the month.
  • Total volume (gross revenue).
  • Total fees.
  • Total refunds.
  • Total settlements.
// Generate cross-check report
async function crossCheckReport(year, month) {
  var from = year + '-' + String(month).padStart(2, '0') + '-01';
  var to = year + '-' + String(month + 1).padStart(2, '0') + '-01';

  var ledgerData = await db.query(
    'SELECT currency, '
    + 'COUNT(*) FILTER (WHERE event_type = 'charge.success') as charges, '
    + 'SUM(gross_amount) FILTER (WHERE event_type = 'charge.success') as gross, '
    + 'SUM(fee_amount) FILTER (WHERE event_type = 'charge.success') as fees, '
    + 'SUM(gross_amount) FILTER (WHERE event_type LIKE 'refund%') as refunds '
    + 'FROM payment_ledger '
    + 'WHERE created_at >= $1 AND created_at < $2 '
    + 'GROUP BY currency',
    [from, to]
  );

  var report = 'MONTH-END CROSS-CHECK: ' + from + '

';
  report += 'Compare these numbers against the Paystack dashboard:

';

  for (var i = 0; i < ledgerData.rows.length; i++) {
    var row = ledgerData.rows[i];
    report += row.currency + ':
';
    report += '  Charges: ' + row.charges + '
';
    report += '  Gross Revenue: ' + formatCurrency(parseInt(row.gross), row.currency) + '
';
    report += '  Fees: ' + formatCurrency(parseInt(row.fees), row.currency) + '
';
    report += '  Refunds: ' + formatCurrency(parseInt(row.refunds || 0), row.currency) + '

';
  }

  return report;
}

Small differences (1-2 transactions) may be due to timezone differences between your system and Paystack. A transaction that happened at 23:55 UTC on July 31 might be recorded as August 1 in one system and July 31 in the other. As long as you can explain the difference, it is acceptable.

Step 6: Produce Financial Reports

Generate the four key reports for the month:

  1. Gross Revenue Report: Total amount charged to customers, by currency and by product/category if applicable.
  2. Net Revenue Report: Gross revenue minus fees minus refunds minus chargebacks. This is your actual income.
  3. Fee Expense Report: Total Paystack fees for the month, broken down by channel.
  4. Cash Flow Report: Total settlements actually deposited to your bank/M-Pesa, versus total revenue earned. The difference is unsettled balance (money still with Paystack).

For detailed report generation, see the financial reports guide.

Step 7: Lock the Period

After the close is complete, lock the period. This prevents anyone from accidentally (or intentionally) making changes to the closed month's data that would invalidate your reports.

// Lock a monthly period
async function lockPeriod(year, month) {
  await db.query(
    'INSERT INTO period_locks (period_year, period_month, locked_at, locked_by) '
    + 'VALUES ($1, $2, NOW(), $3)',
    [year, month, 'month_end_close']
  );

  console.log('Period locked: ' + year + '-' + String(month).padStart(2, '0'));
}

// Check period lock before modifying ledger entries
async function isLedgerWriteAllowed(entryDate) {
  var entryMonth = entryDate.getMonth() + 1;
  var entryYear = entryDate.getFullYear();

  var lock = await db.query(
    'SELECT id FROM period_locks WHERE period_year = $1 AND period_month = $2',
    [entryYear, entryMonth]
  );

  if (lock.rows.length > 0) {
    return { allowed: false, reason: 'Period is locked' };
  }

  return { allowed: true };
}

If you need to make a correction to a locked period, add a correction entry in the current (unlocked) period with a note explaining why. This preserves the audit trail. Never unlock a period to modify historical entries.

With the period locked and reports produced, the month-end close is complete. Share the reports with your accountant, update your financial dashboards, and move on to the new month.

Key Takeaways

  • ✓Month-end close is a structured process that confirms your financial numbers for the month are accurate and complete before reporting them.
  • ✓Wait 3-5 business days after month end to start the close. This gives time for the last settlement of the month to arrive in your bank.
  • ✓The close checklist has seven steps: reconciliation, settlement verification, refund and chargeback review, fee calculation, cross-check with Paystack, reporting, and period lock.
  • ✓Produce four key reports: gross revenue, net revenue (after fees), refund summary, and cash received (settlements actually deposited).
  • ✓Differences between revenue earned and cash received are explained by timing (unsettled transactions), fees, and refunds.
  • ✓Lock the period after close to prevent retroactive changes that would invalidate your reports.

Frequently Asked Questions

How long should month-end close take?
If your daily reconciliation has been running cleanly, the close should take 1-2 hours for a small business and a half-day for a larger one. Most of the time is spent investigating any discrepancies found during the process. If reconciliation has not been running, the close can take days.
What if I find a discrepancy during the close that I cannot resolve?
Document the discrepancy with as much detail as possible. If the amount is small and clearly explained (a timing difference, a rounding issue), note it as a known variance and close the month. If the amount is material, escalate to your accountant and Paystack support before closing.
Do I need to do month-end close if I have daily reconciliation?
Yes. Daily reconciliation catches day-to-day issues. Month-end close produces the final numbers for the period, cross-checks them against the Paystack dashboard and your bank statement, and locks the period. They serve different purposes.
What if a refund from July is processed in August?
The refund is recorded in August (when it was processed), not in July (when the original charge happened). This is standard accrual accounting. Your August report will show the refund. Your July report stays unchanged and locked.
Should I use cash basis or accrual basis accounting?
Most businesses should use accrual basis: revenue is recognized when the charge succeeds, not when the settlement arrives. Your accountant can advise on what is appropriate for your business size and jurisdiction. Your ledger supports both approaches since it records both created_at (when the charge happened) and settled_at (when the cash arrived).

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

Paystack Payment Verification and Reconciliation: Complete Guide

The complete engineering reference for verifying Paystack transactions server-side, preventing double grants, designing a payments ledger, reconciling settlements against your database, and building automated sweeper jobs that catch missing transactions.

Read more

Reconciling Paystack Settlements Against Your Database

Paystack batches your daily transactions into settlements and deposits the net amount to your bank. Reconciling those settlements against your own records catches missing webhooks, unrecorded refunds, and fee discrepancies before they become accounting nightmares. This guide covers the full reconciliation workflow with code.

Read more

Reconciling Paystack Against Your Bank Statement

Settlement reconciliation confirms Paystack sent the money. Bank reconciliation confirms the money actually arrived. This guide covers how to match Paystack settlements against your bank statement, handle timing delays, and flag deposits that do not match any settlement.

Read more

Building Financial Reports from Paystack Data

Your accountant, your investors, and your tax authority all want financial reports. This guide shows how to build them from your payments ledger: revenue summaries, fee analysis, refund tracking, channel breakdowns, and cash flow projections, all driven by the data you already collect from Paystack.

Read more

Double-Entry Bookkeeping for African Payment Integrations

Double-entry bookkeeping is not just for accountants. When you integrate Paystack (or any African payment gateway), recording every transaction as matching debit and credit entries catches errors that single-entry systems miss. This guide shows how to apply the principle in code, with practical PostgreSQL schema and Node.js examples.

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.