Multi-Currency Checkout with Paystack
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.
Which Currencies Paystack Supports
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.
Specifying Currency at Initialization
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.
Routing Currency by Customer Location
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.
Currency-Specific Minimums and Channels
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:
- Card: Available for all currencies
- Bank transfer: Primarily available for NGN
- Mobile money: Available for GHS (MTN, Vodafone, AirtelTigo)
- USSD: Available for NGN
- Apple Pay: Availability varies by market
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.
Settlement and Currency Conversion
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:
- The transaction amount is in the currency the customer paid in. If they paid 50 GHS, the verify response shows
amount: 5000(pesewas) andcurrency: "GHS". - The settlement amount is in your business currency. This may be a different number entirely.
- The conversion rate is set by Paystack at the time of settlement. You do not control it and it is not guaranteed to match any public exchange rate.
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.
Building the Multi-Currency Checkout
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.
Verifying Multi-Currency Transactions
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.
Key Takeaways
- ✓Pass the currency parameter explicitly when initializing transactions. If you omit it, Paystack defaults to the currency of your business registration.
- ✓Each currency has different minimum transaction amounts and different available payment channels. Mobile money is available for GHS but not NGN. Bank transfer works for NGN but not all other currencies.
- ✓Settlement happens in your registered business currency. If you accept GHS payments on a Nigerian Paystack account, the settlement involves conversion at Paystack-determined rates.
- ✓Detect the customer currency from their location, their profile, or let them choose. Do not guess based on IP alone for the final charge; let the user confirm.
- ✓Amounts are always in the smallest unit of the specified currency. 1000 in NGN is 10 Naira; 1000 in KES is 10 Shillings. The multiplier is always 100.
- ✓Store both the charged currency and the charged amount alongside every transaction. You will need both for refunds, reporting, and reconciliation.
Frequently Asked Questions
- Can I accept all five Paystack currencies on a single account?
- It depends on your account configuration. By default, you can accept your registered business currency. To accept additional currencies, you may need to enable them in your Paystack dashboard or contact Paystack support. Not all currency combinations are available for all business types.
- Does Paystack convert currencies automatically for settlement?
- Yes. If a customer pays in a currency different from your business registration currency, Paystack handles the conversion at settlement time. The conversion rate is determined by Paystack and is not necessarily the same as public exchange rates. You receive the settled amount in your registered currency.
- Why do different currencies have different payment channels?
- Payment infrastructure varies by country. USSD and bank transfer work in Nigeria because Nigerian banks support those channels. Mobile money is dominant in Ghana because MTN, Vodafone, and AirtelTigo have widespread mobile money platforms there. Paystack exposes the channels that are available in each market.
- Should I let customers pick their currency or auto-detect it?
- The safest approach is to auto-detect a default (from user profile or IP geolocation) but let the customer override it before paying. IP-based detection fails for VPN users, travelers, and diaspora customers. Forcing the wrong currency leads to abandoned checkouts or confused support tickets.
- How do I handle pricing when I accept multiple currencies?
- Define a price table on your server with fixed prices in each currency. Do not convert on the fly using live exchange rates unless you have a specific reason. Fixed prices are predictable for customers and simpler to implement. Update the price table periodically if exchange rates shift significantly.
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