To accept multiple currencies with Paystack, pass the currency parameter when initializing a transaction. Paystack supports NGN, GHS, KES, ZAR, and USD. Each currency has its own minimum amount and available payment channels. Settlement happens in the currency of your Paystack business registration, not necessarily the currency the customer paid in.
Paystack processes payments in five currencies as of this writing: Nigerian Naira (NGN), Ghanaian Cedi (GHS), South African Rand (ZAR), Kenyan Shilling (KES), and US Dollar (USD). Which currencies are available to you depends on where your Paystack business is registered and what Paystack has enabled for your account.
A Nigerian-registered business can accept NGN by default. To accept GHS, KES, ZAR, or USD, you typically need to enable those currencies in your Paystack dashboard or contact Paystack support. The same applies to businesses registered in other countries.
This is not a "sign up and accept everything" situation. If you plan to accept multiple currencies, check your dashboard settings first. Attempting to initialize a transaction in a currency that is not enabled for your account returns an error.
For a full breakdown of how each currency handles amounts, see the currency handling guide.
The currency is set when you initialize the transaction. Pass it as the currency parameter:
var response = await fetch('https://api.paystack.co/transaction/initialize', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: 'customer@example.com',
amount: 500000, // 5,000 in the specified currency's smallest unit
currency: 'KES', // Explicitly set the currency
reference: 'order_' + orderId + '_' + Date.now(),
callback_url: process.env.APP_URL + '/payment/callback',
}),
});
If you omit the currency parameter, Paystack defaults to your business's registered currency. This is a quiet trap for multi-currency setups. A Ghanaian customer browsing your site sees prices in GHS, clicks pay, and gets charged in NGN because your backend forgot to pass the currency. The amount is correct numerically but in the wrong currency.
Always pass the currency explicitly. Never rely on the default.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
If your product serves customers across multiple African countries, you need to determine which currency to use for each checkout. There are three common approaches, and each has tradeoffs.
Approach 1: Let the customer choose
Show a currency selector on your pricing page or checkout form. This is the simplest and most transparent approach. The customer knows exactly what currency they are paying in.
// Frontend sends the selected currency to your backend
var checkoutData = {
email: customerEmail,
amount: prices[selectedCurrency], // Look up price for selected currency
currency: selectedCurrency, // 'NGN', 'GHS', 'KES', etc.
};
Approach 2: Detect from the user profile
If users have accounts, store their preferred currency at signup (based on their country) and use it for all transactions:
// On your server
var user = await db.query('SELECT currency FROM users WHERE id = $1', [userId]);
var currency = user.rows[0].currency; // 'GHS'
// Use this currency when initializing
Approach 3: IP-based detection
Use a geolocation service to detect the customer's country from their IP address and infer the currency. This is the most automatic approach, but it has a big caveat: VPNs, travelers, and diaspora customers will get the wrong currency. Use it only as a default that the customer can override.
var COUNTRY_CURRENCY_MAP = {
NG: 'NGN',
GH: 'GHS',
KE: 'KES',
ZA: 'ZAR',
};
function getCurrencyFromCountry(countryCode) {
return COUNTRY_CURRENCY_MAP[countryCode] || 'USD';
}
The safest pattern is Approach 2 with Approach 3 as a fallback: use the stored preference if available, detect from IP if not, and always let the customer override the currency before they pay.
Each currency has its own minimum transaction amount and its own set of available payment channels. This means your checkout logic needs to adapt based on the currency.
Minimum amounts
Paystack enforces a minimum amount per currency. These minimums can change, so do not hard-code them as your only validation. Instead, validate against your own business minimums (which should be higher than Paystack's) and handle Paystack's error response gracefully if the amount is too low:
async function initializeWithValidation(email, amountSmallestUnit, currency) {
var response = await fetch('https://api.paystack.co/transaction/initialize', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: email,
amount: amountSmallestUnit,
currency: currency,
}),
});
var data = await response.json();
if (!data.status && data.message) {
// Paystack may return something like "Amount is below minimum for KES"
// Parse this and show a user-friendly message
return { success: false, error: data.message };
}
return { success: true, data: data.data };
}
Available channels per currency
Not every payment channel works with every currency. Here is the general picture:
If you restrict channels using the channels array, make sure the channels you specify are valid for the currency. Requesting mobile_money for an NGN transaction will not work.
var CHANNELS_BY_CURRENCY = {
NGN: ['card', 'bank', 'ussd', 'bank_transfer', 'qr'],
GHS: ['card', 'mobile_money'],
KES: ['card'],
ZAR: ['card'],
USD: ['card'],
};
function getChannelsForCurrency(currency) {
return CHANNELS_BY_CURRENCY[currency] || ['card'];
}
This mapping is approximate. Paystack adds new channels and expands existing ones. Check the payment channels guide for the latest.
Here is the part that confuses most developers: the currency your customer pays in is not necessarily the currency you receive in your bank account.
Paystack settles funds in the currency of your business registration. If your business is registered in Nigeria, you receive NGN settlements. If a Ghanaian customer pays in GHS, Paystack converts the funds to NGN at their prevailing rate before settling to your bank account.
What this means for your code:
amount: 5000 (pesewas) and currency: "GHS".For your application logic, this usually does not matter. You charge 50 GHS, the customer pays 50 GHS, and you deliver the product. What lands in your bank account is an accounting concern, not an application concern.
But for reporting and reconciliation, store both the charged amount and currency, and the settlement amount if you pull it from the Settlement API. Otherwise your revenue reports will not make sense when you mix currencies.
Here is a complete checkout endpoint that handles multiple currencies, validates amounts, and selects the right channels:
var PRICE_TABLE = {
basic_plan: { NGN: 500000, GHS: 5000, KES: 100000, ZAR: 10000, USD: 1000 },
pro_plan: { NGN: 1500000, GHS: 15000, KES: 300000, ZAR: 30000, USD: 3000 },
};
var CHANNELS_BY_CURRENCY = {
NGN: ['card', 'bank_transfer', 'ussd'],
GHS: ['card', 'mobile_money'],
KES: ['card'],
ZAR: ['card'],
USD: ['card'],
};
app.post('/api/checkout', async (req, res) => {
var email = req.body.email;
var plan = req.body.plan;
var currency = req.body.currency;
// Validate the plan exists
var prices = PRICE_TABLE[plan];
if (!prices) {
return res.status(400).json({ error: 'Unknown plan: ' + plan });
}
// Validate the currency is supported for this plan
var amount = prices[currency];
if (!amount) {
return res.status(400).json({
error: 'Currency ' + currency + ' is not available. Choose from: ' + Object.keys(prices).join(', '),
});
}
var channels = CHANNELS_BY_CURRENCY[currency] || ['card'];
var reference = plan + '_' + req.user.id + '_' + Date.now();
var response = await fetch('https://api.paystack.co/transaction/initialize', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: email,
amount: amount,
currency: currency,
channels: channels,
reference: reference,
callback_url: process.env.APP_URL + '/payment/callback',
metadata: {
plan: plan,
user_id: req.user.id,
currency: currency,
amount: amount,
},
}),
});
var data = await response.json();
if (data.status) {
res.json({
authorization_url: data.data.authorization_url,
reference: data.data.reference,
});
} else {
res.status(400).json({ error: data.message });
}
});
The price table defines amounts in the smallest currency unit for each plan and currency. The server looks up the amount from this table. The customer can choose a currency on the frontend, but the actual amount comes from the server. This prevents a tampered frontend from sending the NGN price with a USD currency code.
When verifying a multi-currency transaction, check both the amount and the currency. This is easy to forget when all your early transactions were in a single currency:
async function verifyTransaction(reference, expectedAmount, expectedCurrency) {
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.status || data.data.status !== 'success') {
return { verified: false, reason: 'Transaction not successful' };
}
if (data.data.amount !== expectedAmount) {
return { verified: false, reason: 'Amount mismatch: expected ' + expectedAmount + ' got ' + data.data.amount };
}
if (data.data.currency !== expectedCurrency) {
return { verified: false, reason: 'Currency mismatch: expected ' + expectedCurrency + ' got ' + data.data.currency };
}
return { verified: true, data: data.data };
}
Without the currency check, an attacker could initialize a transaction for 1000 USD (which is $10) instead of 1000 NGN (which is about 10 Naira). If you only check that the amount is 1000, both pass. But $10 is worth far more than 10 Naira. The currency check catches this.
Store the expected currency in your database alongside the expected amount when you create the order. Do not re-derive it from the frontend request during verification.
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