Bonaventure OgetoBy Bonaventure Ogeto|

Understanding Kobo, Pesewa and Cents: Paystack Amount Handling

Paystack requires all amounts in the smallest currency unit. For NGN, multiply Naira by 100 to get kobo. For GHS, multiply Cedis by 100 to get pesewas. For ZAR, KES, and USD, multiply by 100 to get cents. Always use Math.round() when converting to avoid floating-point errors, and always compare amounts as integers when verifying transactions.

Why the Smallest Currency Unit?

Payment processors worldwide use the smallest currency unit (sometimes called "minor units") to avoid decimal precision issues. Stripe uses cents. Flutterwave uses kobo. Paystack does the same.

The reason is simple: computers handle integers reliably but handle decimals unpredictably. The number 5000.50 can be stored as 5000.4999999999999 or 5000.50000000001 depending on how the floating-point representation works. But the integer 500050 is always exactly 500050. No ambiguity, no rounding, no surprises.

By requiring amounts as integers in the smallest unit, Paystack avoids a whole category of bugs where a customer is charged 4999.99 instead of 5000.00 because of a floating-point rounding error somewhere in the chain.

Here is the mapping for every Paystack-supported currency:

CurrencyCodeSmallest Unit1 Main Unit =5,000 display =
Nigerian NairaNGNKobo100 kobo500000
Ghanaian CediGHSPesewa100 pesewas500000
South African RandZARCent100 cents500000
Kenyan ShillingKESCent100 cents500000
US DollarUSDCent100 cents500000

The multiplier is 100 for all Paystack-supported currencies. This is not universal across all payment processors (some currencies have different minor unit ratios), but for Paystack it is always 100.

Conversion Helper Functions

Build two helper functions and use them everywhere in your codebase. Do not write amount * 100 inline. A helper function is easier to audit, easier to test, and makes the intent clear when someone reads your code six months later.

// Convert display amount (Naira, Cedis, Rand, etc.) to Paystack amount
function toMinorUnit(amount) {
  return Math.round(Number(amount) * 100);
}

// Convert Paystack amount back to display amount
function fromMinorUnit(amount) {
  return Number(amount) / 100;
}

// Format for display with currency symbol
function formatCurrency(amountInMinorUnit, currencyCode) {
  var symbols = {
    NGN: 'N',
    GHS: 'GH¢',
    ZAR: 'R',
    KES: 'KSh',
    USD: '$',
  };
  var symbol = symbols[currencyCode] || currencyCode + ' ';
  var displayAmount = fromMinorUnit(amountInMinorUnit);
  return symbol + displayAmount.toLocaleString('en', {
    minimumFractionDigits: 2,
    maximumFractionDigits: 2,
  });
}

// Examples
console.log(toMinorUnit(5000));       // 500000
console.log(toMinorUnit(19.99));      // 1999 (not 1998.9999...)
console.log(fromMinorUnit(500000));   // 5000
console.log(formatCurrency(500000, 'NGN')); // N5,000.00
console.log(formatCurrency(999, 'KES'));    // KSh9.99

The Math.round() in toMinorUnit is critical. Without it, 19.99 * 100 produces 1998.9999999999998 in JavaScript, which is not a valid integer and would cause a Paystack API error or an incorrect charge.

The Floating-Point Traps

JavaScript (and most programming languages) uses IEEE 754 floating-point arithmetic, which cannot represent all decimal numbers exactly. Here are the specific traps that catch Paystack developers:

Trap 1: Multiplication produces non-integers

// These look right but produce wrong results
console.log(19.99 * 100);   // 1998.9999999999998 (not 1999)
console.log(0.1 + 0.2);     // 0.30000000000000004 (not 0.3)
console.log(33.33 * 100);   // 3332.9999999999995 (not 3333)

// Fix: always use Math.round
console.log(Math.round(19.99 * 100));  // 1999
console.log(Math.round(33.33 * 100));  // 3333

Trap 2: Comparing display amounts after conversion

// BAD: convert to display values and compare
var expectedNaira = 5000;
var receivedKobo = 500000;
var receivedNaira = receivedKobo / 100;

// This works for 5000, but fails for amounts with decimals
// 1999 / 100 = 19.99 in JavaScript, but
// some amounts produce: 19.990000000000002

// GOOD: compare in minor units (integers)
var expectedKobo = toMinorUnit(5000); // 500000
if (receivedKobo === expectedKobo) {
  // Amounts match - safe to fulfill
}

Trap 3: String-to-number conversion from form inputs

// Form input values are strings
var inputValue = document.getElementById('amount').value; // "5000"

// BAD: implicit conversion
var kobo = inputValue * 100; // Works but fragile

// GOOD: explicit conversion with validation
var amount = Number(inputValue);
if (isNaN(amount) || amount <= 0) {
  throw new Error('Invalid amount');
}
var kobo = Math.round(amount * 100);

Trap 4: Double conversion

The most expensive bug is converting twice. If your frontend converts to kobo and your backend converts again, a 5,000 NGN charge becomes 50,000,000 kobo (500,000 Naira). This is rare but devastating when it happens. Establish a clear convention: either the frontend sends display amounts and the server converts, or the frontend sends kobo and the server passes it through. Document which one your system uses and validate on both sides.

Database Storage Patterns

Store amounts in your database as integers in the smallest currency unit. This eliminates conversion errors in queries, aggregations, and comparisons.

// Database schema (conceptual)
// orders table:
//   amount_minor INTEGER NOT NULL  -- kobo/pesewa/cents
//   currency VARCHAR(3) NOT NULL   -- 'NGN', 'GHS', 'ZAR', etc.

// When creating an order
var order = {
  amount_minor: toMinorUnit(req.body.displayAmount), // Store as integer
  currency: 'NGN',
};

// When initializing a Paystack transaction
var paystackBody = {
  email: customer.email,
  amount: order.amount_minor, // Already in kobo, no conversion needed
  currency: order.currency,
};

// When displaying to the customer
var displayText = formatCurrency(order.amount_minor, order.currency);
// "N5,000.00"

This pattern has three benefits:

  1. You never convert between the database and Paystack. The stored value is the Paystack value. No chance of accidental double conversion.
  2. SQL aggregations (SUM, AVG) work correctly on integers. Summing floating-point amounts across thousands of rows produces drift; summing integers does not.
  3. Comparison queries are reliable. WHERE amount_minor = 500000 always matches exactly. WHERE amount = 5000.00 can fail due to float precision in some databases.

Only convert to display format at the presentation layer: when rendering an HTML page, formatting an email, or building a JSON API response for the frontend.

Verifying Amounts Correctly

After payment, you call GET /transaction/verify/{reference} and Paystack returns the transaction details including the amount. The amount in the verify response is in the smallest currency unit, same as what you sent.

// Verification with correct amount comparison
app.get('/payment/callback', async function(req, res) {
  var reference = req.query.reference;

  var verifyResponse = await fetch(
    'https://api.paystack.co/transaction/verify/' + reference,
    {
      headers: {
        Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
      },
    }
  );

  var data = await verifyResponse.json();
  var transaction = data.data;

  // Look up the expected amount from your database (stored in minor units)
  var order = await db.orders.findByReference(reference);

  // Compare as integers - both are in minor units
  if (
    transaction.status === 'success' &&
    transaction.amount === order.amount_minor &&
    transaction.currency === order.currency
  ) {
    // Payment verified - safe to fulfill
    await fulfillOrder(order.id);
    res.redirect('/order/success?ref=' + reference);
  } else {
    // Amount mismatch or failed payment
    console.log('Expected: ' + order.amount_minor + ' ' + order.currency);
    console.log('Received: ' + transaction.amount + ' ' + transaction.currency);
    res.redirect('/order/failed?ref=' + reference);
  }
});

Always check three things during verification:

  1. status === 'success': The payment actually completed
  2. amount === expectedAmount: The customer paid the correct amount (both in minor units)
  3. currency === expectedCurrency: The payment was in the expected currency

Skipping the amount check is a common vulnerability. Without it, a customer could initialize a transaction for 100 kobo (1 Naira) by modifying the request, complete the payment, and your system would grant value for a 5,000 Naira order because it only checked that the status was "success." The amount comparison closes this hole.

Minimum and Maximum Amounts

Paystack enforces minimum transaction amounts that vary by currency. If you send an amount below the minimum, the API returns an error. The exact minimums can change, so check the Paystack documentation or test empirically. As a general guideline:

  • For NGN, the minimum is typically around 100 kobo (1 Naira)
  • For other currencies, similar low minimums apply

Maximum amounts depend on your account type, business category, and the payment channel. Card transactions may have different limits than bank transfers. Your Paystack dashboard shows your current limits under Settings.

Build validation into your payment flow:

function validatePaymentAmount(amountInMinorUnit, currency) {
  // Minimum amounts by currency (check Paystack docs for current values)
  var minimums = {
    NGN: 100,    // 1 Naira
    GHS: 100,    // 1 Cedi
    ZAR: 100,    // 1 Rand
    KES: 100,    // 1 Shilling
    USD: 100,    // 1 Dollar
  };

  var min = minimums[currency];
  if (!min) {
    return { valid: false, error: 'Unsupported currency: ' + currency };
  }

  if (!Number.isInteger(amountInMinorUnit)) {
    return { valid: false, error: 'Amount must be an integer in minor units' };
  }

  if (amountInMinorUnit < min) {
    return { valid: false, error: 'Amount below minimum of ' + min + ' for ' + currency };
  }

  return { valid: true };
}

Validate on your server before calling the Paystack API. This gives your customer a clear error message instead of a generic Paystack API error.

Multi-Currency Considerations

If your application supports multiple currencies, the conversion rules are the same (multiply by 100), but you need to be careful about a few things:

Always send the currency parameter

If you do not specify a currency when initializing a transaction, Paystack defaults to the primary currency of your account (usually NGN for Nigerian accounts). If your customer selected GHS and you forget to pass currency: 'GHS', they get charged in Naira. Always send the currency explicitly.

Store the currency alongside the amount

An amount of 500000 means very different things depending on the currency: 5,000 NGN, 5,000 GHS, 5,000 ZAR, 5,000 KES, or 5,000 USD. Always store and compare the currency code alongside the amount.

Do not convert between currencies yourself

If you need to show a price in GHS for a product priced in NGN, use a reliable exchange rate source and convert at the display layer. The Paystack transaction should always be in the currency the customer sees and agrees to. Paystack does not perform currency conversion for you.

For detailed guidance on multi-currency setups, see currency handling in Paystack and multi-currency checkout.

Stay Up to Date

Paystack occasionally updates supported currencies, minimum amounts, and API validation rules. We keep these guides current.

Sign up for the McTaba newsletter to get notified about changes that affect your payment integration.

Key Takeaways

  • Paystack always expects amounts in the smallest currency unit. For NGN that means kobo (1 Naira = 100 kobo). For GHS it means pesewas. For ZAR, KES, and USD it means cents.
  • The conversion formula is always: paystack_amount = display_amount * 100. When converting back: display_amount = paystack_amount / 100.
  • JavaScript floating-point arithmetic can produce incorrect results like 19.99 * 100 = 1998.9999999999998. Always wrap conversions with Math.round() to get the correct integer.
  • When verifying a transaction, compare amounts as integers (the kobo/pesewa/cent values), not as converted display values. Integer comparison eliminates floating-point mismatch risks.
  • Store amounts in your database as integers in the smallest unit. Convert to display format only at the presentation layer. This prevents rounding errors throughout your system.
  • Paystack returns an error if you send a non-integer amount. The request simply fails, which is better than silently rounding, but it means your code must get this right before sending.

Frequently Asked Questions

What happens if I send a decimal amount to the Paystack API?
Paystack returns an error because the amount field must be an integer. The API does not silently round or truncate decimal values. Your code must send a whole number representing the amount in the smallest currency unit.
Why does 19.99 * 100 not equal 1999 in JavaScript?
JavaScript uses IEEE 754 floating-point arithmetic, which cannot represent 19.99 exactly in binary. The result of 19.99 * 100 is 1998.9999999999998. Use Math.round(19.99 * 100) to get the correct value of 1999.
Should I store amounts as Naira or kobo in my database?
Store as kobo (or the relevant smallest unit). Integer storage avoids floating-point issues in SQL queries and aggregations, and it matches the value Paystack uses directly. Convert to display format (Naira) only when showing the amount to humans.
Is the multiplier always 100 for all Paystack currencies?
Yes. All Paystack-supported currencies (NGN, GHS, ZAR, KES, USD) use a 100x multiplier from the main unit to the minor unit. Some global currencies have different ratios (like Japanese Yen which has no minor unit), but those are not currently supported by Paystack.
How do I handle refund amounts?
Refund amounts follow the same rules. When calling the Paystack refund endpoint, the amount is in the smallest currency unit (kobo, pesewas, cents). A partial refund of 2,500 NGN from a 5,000 NGN transaction would use amount: 250000.

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