Why Paystack for Nigerian products
Paystack is what most Nigerian users expect to see at checkout. It supports local cards, bank transfer, USSD, and mobile money natively — the payment methods your users actually have. The fee structure is straightforward: 1.5% per transaction, capped at ₦2,000 for local payments. If Nigeria or Ghana is your primary market, Paystack is the default choice, not something to evaluate against Stripe.
How Paystack payments work
The flow is the same regardless of your framework or language. Your server calls the Paystack Initialize API with the customer's email and amount — Paystack returns an authorization URL. You redirect the user there. They pay. Paystack sends them back to your callback URL with a reference. Your server verifies that reference with Paystack before fulfilling anything. That's it. Everything else — the checkout UI, 3D Secure, bank prompts — Paystack handles.
1. Get your API keys
From your Paystack dashboard, grab your public and secret keys. Keep the secret key on the server only — it should never reach a browser:
# Test keys (safe to use during development) PAYSTACK_SECRET_KEY=sk_test_... PAYSTACK_PUBLIC_KEY=pk_test_... # Paystack amounts are always in the smallest currency unit # ₦100 = 10000 (kobo), GH₵10 = 1000 (pesewas)
2. Initialize a transaction (server-side)
This is a plain HTTP call — works in any language or framework. Here it is in JavaScript, but the pattern is identical in Python, PHP, Go, or anything else:
// Works in Node.js, Deno, Bun, or any JS runtime
async function initializePaystackTransaction({ email, amountInKobo }) {
const 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,
amount: amountInKobo,
callback_url: `${process.env.APP_URL}/payment/verify`,
}),
});
const data = await response.json();
if (!data.status) throw new Error('Paystack initialization failed');
// Redirect user to data.data.authorization_url
return data.data.authorization_url;
}3. Verify the transaction (never skip this)
A user can manually visit your callback URL without paying. Always verify server-side before releasing anything:
async function verifyPaystackTransaction(reference) {
const response = await fetch(
`https://api.paystack.co/transaction/verify/${reference}`,
{
headers: {
Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
},
}
);
const data = await response.json();
if (data.data?.status === 'success') {
// Payment confirmed — fulfil the order
return { success: true, data: data.data };
}
return { success: false };
}4. Handle webhooks for subscriptions and async payments
For bank transfers especially, payment confirmation can come minutes after the redirect. Webhooks are how Paystack tells your server a payment landed. Verify the signature before trusting the payload:
import { createHmac } from 'crypto';
// Your webhook endpoint — register the URL in Paystack dashboard
async function handlePaystackWebhook(rawBody, signatureHeader) {
const hash = createHmac('sha512', process.env.PAYSTACK_SECRET_KEY)
.update(rawBody)
.digest('hex');
if (hash !== signatureHeader) {
throw new Error('Invalid Paystack webhook signature');
}
const event = JSON.parse(rawBody);
if (event.event === 'charge.success') {
const { reference, amount, customer } = event.data;
// Fulfil the order using reference as idempotency key
}
return { received: true };
}5. Paystack Popup (client-side alternative)
If you'd rather keep users on your page instead of redirecting, load the Paystack Popup script and trigger it from a button click. Your public key goes here — never the secret key:
<script src="https://js.paystack.co/v1/inline.js"></script>
<button onclick="payWithPaystack()">Pay ₦5,000</button>
<script>
function payWithPaystack() {
const handler = PaystackPop.setup({
key: 'pk_test_...', // Public key only
email: 'customer@email.com',
amount: 500000, // ₦5,000 in kobo
currency: 'NGN',
callback: function(response) {
// Verify response.reference server-side before fulfilling
verifyOnServer(response.reference);
},
onClose: function() {
console.log('Payment window closed');
},
});
handler.openIframe();
}
</script>Test credentials
Use these in test mode — none of these trigger real charges:
- Card: 4084 0840 8408 4081 | Expiry: any future date | CVV: 408 | PIN: 0000 | OTP: 123456 — successful payment
- Card: 4084 0840 8408 4081 with PIN: 1234 — declined transaction
- Bank transfer: account 0000000000, any bank — successful transfer simulation
- Use the Paystack dashboard Events tab to replay webhook events during testing
- All amounts in smallest unit: ₦1 = 100 kobo, GH₵1 = 100 pesewas
Common questions
Building a product that collects money in Nigeria or Africa?
We've integrated Paystack into SaaS tools, fintech products, and ecommerce stores — including multi-gateway setups for products serving both local and international users. Tell us what you're building.