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.
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:
| Currency | Code | Smallest Unit | 1 Main Unit = | 5,000 display = |
|---|---|---|---|---|
| Nigerian Naira | NGN | Kobo | 100 kobo | 500000 |
| Ghanaian Cedi | GHS | Pesewa | 100 pesewas | 500000 |
| South African Rand | ZAR | Cent | 100 cents | 500000 |
| Kenyan Shilling | KES | Cent | 100 cents | 500000 |
| US Dollar | USD | Cent | 100 cents | 500000 |
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.
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.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
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.
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:
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.
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:
status === 'success': The payment actually completedamount === expectedAmount: The customer paid the correct amount (both in minor units)currency === expectedCurrency: The payment was in the expected currencySkipping 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.
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:
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.
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.
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