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. Paystack Fees in Your Ledger: Gross vs Net Recording
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 20, 2026

Paystack Fees in Your Ledger: Gross vs Net Recording

Record the gross amount (what the customer paid), the fee amount (what Paystack deducted), and the net amount (what you receive) as separate columns in your ledger. This is the "gross recording with fee breakdown" approach. It lets you reconcile against both the customer invoice (gross) and your bank statement (net). Never record only the net amount, as you lose visibility into fee calculations.

In this article

  1. 1. The Problem: Which Amount Do You Record?
  2. 2. The Three-Column Approach
  3. 3. Why Recording Only Net Fails
  4. 4. Why Recording Only Gross Also Fails
  5. 5. Fee Reporting and Analysis
  6. 6. Fee Caps and Special Cases
  7. 7. Fee Treatment on Refunds
  8. 8. Passing Fees to the Customer

The Problem: Which Amount Do You Record?

A customer pays NGN 50,000 for your product. Paystack processes the payment and charges a processing fee. You receive the net amount in your settlement. Your ledger needs to answer three different questions:

  1. How much did the customer pay? The gross amount. You need this for invoices, receipts, and customer-facing records.
  2. How much did we pay in processing fees? The fee amount. You need this for expense tracking and profitability analysis.
  3. How much money actually arrived in our bank? The net amount. You need this for bank reconciliation and cash flow tracking.

If you record only one of these, you cannot answer the other two questions without recalculating from Paystack's fee structure, which may change over time. Store all three at the time of the transaction, and you never need to recalculate.

For the overall ledger design, see the payments ledger design guide.

The Three-Column Approach

Your ledger should have three separate money columns for every entry:

CREATE TABLE payment_ledger (
  id              BIGSERIAL PRIMARY KEY,
  reference       VARCHAR(100) NOT NULL,
  event_type      VARCHAR(50) NOT NULL,
  gross_amount    BIGINT NOT NULL,   -- What the customer paid
  fee_amount      BIGINT NOT NULL DEFAULT 0,   -- What Paystack kept
  net_amount      BIGINT NOT NULL,   -- What we receive
  currency        VARCHAR(3) NOT NULL,
  -- ... other columns
  UNIQUE(reference, event_type)
);

For a charge of NGN 50,000 (5,000,000 kobo) with a Paystack fee of NGN 750 (75,000 kobo):

  • gross_amount = 5,000,000
  • fee_amount = 75,000
  • net_amount = 4,925,000
// Recording with all three columns
async function recordTransaction(txn, orderId) {
  var grossAmount = txn.amount;        // From Paystack: data.amount
  var feeAmount = txn.fees;            // From Paystack: data.fees
  var netAmount = grossAmount - feeAmount;

  await db.query(
    'INSERT INTO payment_ledger '
    + '(reference, event_type, gross_amount, fee_amount, net_amount, '
    + 'currency, channel, paystack_id, order_id, metadata) '
    + 'VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) '
    + 'ON CONFLICT (reference, event_type) DO NOTHING',
    [
      txn.reference, 'charge.success',
      grossAmount, feeAmount, netAmount,
      txn.currency, txn.channel, txn.id, orderId,
      JSON.stringify(txn),
    ]
  );
}

The fee value comes directly from the Paystack verification response (data.fees). Do not calculate it yourself. Paystack's fee structure is complex (different rates for different channels, volumes, and countries). Always read the fee from the API response.

Get weekly developer tips

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

No spam. Unsubscribe anytime.

Why Recording Only Net Fails

Some developers record only the net amount because "that is what we actually receive." This creates several problems:

Customer disputes. A customer says "I paid NGN 50,000." Your records show NGN 49,250. The customer thinks you shortchanged them. You have to explain that Paystack took a fee. But without the gross amount in your records, you have to recalculate it, which requires knowing the exact fee rate that applied at the time of the transaction.

Invoicing. When you issue a receipt or invoice, the amount should be what the customer paid (gross), not what you received (net). The fee is your cost of doing business, not something the customer should see on their receipt.

Tax reporting. In many jurisdictions, your revenue for tax purposes is the gross amount. The processing fee is a deductible expense. If you only record the net, your revenue is understated and your expenses are not tracked.

Fee analysis. You cannot track how much you are paying in processing fees over time. You cannot compare fee rates across channels (card vs bank transfer vs USSD) to optimize your checkout flow. You cannot detect if Paystack changed their fee structure.

Why Recording Only Gross Also Fails

Recording only the gross amount creates the opposite problem: settlement reconciliation breaks.

When Paystack deposits NGN 4,925,000 into your bank, and your ledger shows NGN 5,000,000 in charges, there is a NGN 75,000 gap. If you did not record the fee, you have to recalculate it to explain the difference. For a single transaction, that is doable. For a settlement batch of 200 transactions across different channels with different fee rates, it is a headache.

The three-column approach avoids this entirely. Your settlement reconciliation compares the sum of net_amount values against the bank deposit. They should match. If they do not, the difference is immediately visible without any fee recalculation.

-- Settlement reconciliation with three columns is clean
SELECT
  SUM(net_amount) as expected_settlement
FROM payment_ledger
WHERE event_type = 'charge.success'
  AND settled_at IS NULL
  AND created_at >= '2026-07-20'
  AND created_at < '2026-07-21';

-- Compare this against the actual bank deposit
-- If they match, you are done. No fee calculation needed.

Fee Reporting and Analysis

With fees stored in your ledger, you can run powerful analytics:

-- Monthly fee summary by channel
SELECT
  DATE_TRUNC('month', created_at) as month,
  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_fee_rate
FROM payment_ledger
WHERE event_type = 'charge.success'
GROUP BY DATE_TRUNC('month', created_at), channel, currency
ORDER BY month DESC, total_gross DESC;

-- Daily fee total for expense tracking
SELECT
  DATE(created_at) as date,
  currency,
  SUM(fee_amount) as daily_fees
FROM payment_ledger
WHERE event_type = 'charge.success'
  AND created_at >= NOW() - INTERVAL '30 days'
GROUP BY DATE(created_at), currency
ORDER BY date DESC;

The effective_fee_rate calculation shows the actual percentage Paystack charged across all your transactions for each channel. If one channel consistently has a higher effective fee rate, you might want to steer customers toward cheaper channels (if the business case supports it).

Keep in mind that Paystack fee structures can include flat components and percentage components, and may have caps. The effective rate varies by transaction size. Small transactions have a higher effective rate because the flat fee component is a larger proportion of the total.

Fee Caps and Special Cases

Paystack fee structures may include caps (maximum fee per transaction) and special arrangements for high-volume merchants. Your ledger does not need to know the fee structure. It just records what Paystack actually charged, which is in the data.fees field of the verification response.

// Always read the fee from the API, never calculate it
async function getTransactionFee(reference) {
  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.data) {
    return {
      gross: data.data.amount,
      fee: data.data.fees,
      net: data.data.amount - data.data.fees,
    };
  }

  return null;
}

// BAD: Calculating fees yourself
// var fee = Math.min(amount * 0.015 + 10000, 200000);
// This breaks when Paystack changes their fee structure.

If you need to show estimated fees to customers before they pay (for example, to show "You will be charged NGN 50,000 + processing fee"), use Paystack's fee calculation as an estimate but always record the actual fee from the API response after the transaction completes.

Fee Treatment on Refunds

When you refund a transaction, the fee treatment is an important accounting question. There are two common scenarios:

Scenario 1: Fee not returned. You charged the customer NGN 50,000. Paystack took a fee. You refund the full NGN 50,000. Paystack does not return the fee. Your net loss is the fee amount.

Scenario 2: Fee returned. Some fee agreements return the processing fee on full refunds. Your refund ledger entry should reflect this:

// Refund with fee tracking
async function recordRefundWithFee(originalReference, refundAmount, feeReturned) {
  // Fetch original charge to get the fee
  var original = await db.query(
    'SELECT gross_amount, fee_amount, currency FROM payment_ledger '
    + 'WHERE reference = $1 AND event_type = $2',
    [originalReference, 'charge.success']
  );

  var charge = original.rows[0];
  var isFullRefund = refundAmount === charge.gross_amount;
  var returnedFee = (isFullRefund && feeReturned) ? charge.fee_amount : 0;

  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',
    [
      originalReference,
      isFullRefund ? 'refund.full' : 'refund.partial',
      'debit',
      refundAmount,
      returnedFee,
      refundAmount - returnedFee,
      charge.currency,
      JSON.stringify({ fee_returned: feeReturned, returned_fee_amount: returnedFee }),
    ]
  );
}

The fee_amount on the refund entry represents the fee that was returned to you, not the fee that was charged on the original transaction. This matters for settlement reconciliation: if the fee is returned, the settlement deduction is the refund amount minus the returned fee. If the fee is not returned, the deduction is the full refund amount.

Passing Fees to the Customer

Some businesses pass the Paystack processing fee to the customer. The customer pays the product price plus the processing fee. This is a business decision with accounting implications.

// If you pass fees to the customer
// Product price: NGN 50,000 (5,000,000 kobo)
// Estimated fee: NGN 750 (75,000 kobo)
// Total charged: NGN 50,750 (5,075,000 kobo)

// In your ledger, the gross amount is 5,075,000 (what the customer paid)
// The fee is 75,000 (what Paystack took)
// The net is 5,000,000 (what you receive, which is your product price)

// Your revenue is the product price, not the gross amount
// The fee is not your expense in this model; it is a pass-through

This changes how you report revenue. If the customer pays the fee, your revenue is the product price (net amount), not the gross amount. Your fee expense is zero because the customer covered it. Record this clearly in your ledger metadata so your finance team knows the fee model.

Be aware that in some jurisdictions, passing fees to customers may have regulatory implications. Check local regulations before implementing this model. For the full picture, see the verification and reconciliation guide.

Key Takeaways

  • ✓Always record three values per transaction: gross amount (what the customer paid), fee amount (what Paystack kept), and net amount (what you receive).
  • ✓The gross amount is what appears on the customer invoice and in the Paystack verification response as data.amount.
  • ✓The fee amount comes from data.fees in the verification response. It is what Paystack charges for processing.
  • ✓The net amount is gross minus fees. It is what Paystack settles to your bank account.
  • ✓Recording only the net amount makes it impossible to tell customers what they paid. Recording only the gross makes settlement reconciliation impossible.
  • ✓Fee amounts should not be hardcoded in your system. Always read them from the Paystack API response, as fee structures can change.

Frequently Asked Questions

Should I store the fee percentage or the fee amount?
Store the fee amount in the smallest currency unit, exactly as Paystack returns it. Do not store a percentage. The actual fee depends on complex rules (flat + percentage, caps, channel-specific rates) that change over time. The amount is what actually happened.
What if the fee in my ledger does not match what Paystack deducted from the settlement?
This is a reconciliation issue. The fee in data.fees from the verification response should match what Paystack deducts. If there is a discrepancy, it could indicate a rounding difference, a fee structure change, or an additional fee (like a chargeback fee) that was applied separately.
Do Paystack fees vary by payment channel?
Yes. Card, bank transfer, USSD, and mobile money typically have different fee structures. Your ledger captures the actual fee per transaction, so you can analyze effective rates per channel without knowing the fee rules.
How do I estimate fees before the transaction for pricing purposes?
Check the current Paystack pricing page for your country and plan. Use those rates as estimates in your pricing model. But always record the actual fee from the API response, as the actual fee may differ from your estimate due to caps, special rates, or rate changes.
Can I negotiate lower Paystack fees?
High-volume merchants may be able to negotiate custom fee structures with Paystack. Contact Paystack business team if you process significant volume. Your fee analysis from the ledger gives you the data to support your negotiation.

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

Designing a Payments Ledger for a Paystack Integration

A payments ledger is your source of truth for every financial event in your Paystack integration. This guide walks through the database schema, write patterns, and query strategies that make reconciliation, refund tracking, and financial reporting reliable from day one.

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

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

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

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.