Paystack is a payment gateway that lets you accept card, bank transfer, mobile money, and USSD payments across African countries through a REST API. You initialize a transaction from your server, redirect the customer to Paystack checkout, then verify the payment server-side before granting value. Webhooks give you real-time confirmation.
Paystack is a payment gateway for Africa. It sits between your application and the banks, card networks, and mobile money providers. You send Paystack an API request. Paystack collects the money from the customer. Paystack settles the money to your bank account. That is the whole job.
Paystack was founded in Lagos in 2015 and acquired by Stripe in 2020. It operates in Nigeria, Ghana, South Africa, and Kenya, with more countries in various stages of rollout. Each country has different payment channels available. In Nigeria, you get cards, bank transfers, USSD, and bank accounts. In Kenya, you get cards, M-Pesa, and Pesalink. The API is the same across countries, but the payment channels and settlement currencies change.
Here is what Paystack is not:
The API is REST-based, uses JSON, and authenticates with secret keys. If you have ever called a REST API, you already know the transport layer. The payment-specific concepts (authorization, verification, webhooks, settlements) are what this guide covers.
For a deeper look at how Paystack processes a payment internally, read How Paystack Accept Payments Actually Works Under the Hood.
Every Paystack payment follows the same five steps. It does not matter whether the customer pays with a Visa card in Lagos, an M-Pesa account in Nairobi, or a bank transfer in Accra. The flow is identical from your server's perspective.
POST /transaction/initialize with the amount (in the smallest currency unit), the customer's email, and an optional reference. Paystack responds with an authorization_url and an access_code.authorization_url. This opens Paystack's hosted checkout page, where the customer picks a payment channel and completes the payment. You never see their card number or M-Pesa PIN.callback_url with a reference query parameter. Separately, Paystack sends a charge.success event to your webhook URL.GET /transaction/verify/:reference and checks that the status is "success" and the amount matches what you expected. Only then do you grant value (deliver the product, activate the subscription, top up the wallet).The verification step is not optional. A user can fabricate a redirect to your callback URL with a spoofed reference. If you grant value based only on the redirect, you will give away your product for free. Always verify server-side.
Here is the flow as a diagram you can keep in your head:
Your Server Paystack Customer
| | |
|-- POST /initialize --------->| |
|<-- authorization_url --------| |
| | |
| redirect customer to authorization_url |
|-----------------------------------------------------> |
| |<--- customer pays -------|
| | |
|<-- webhook: charge.success --| |
| customer redirected to callback_url |
|<---------------------------------------------------------|
| | |
|-- GET /verify/:reference --->| |
|<-- transaction details ------| |
| | |
| (grant value if verified) | |
For the full breakdown of each step, including edge cases like abandoned checkouts and pending transactions, see Accepting Payments with Paystack: Complete Guide.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
This is the minimum code to initialize a Paystack transaction. It uses Node.js with the built-in fetch API (Node 18+). No SDK, no npm package. Just HTTP.
// initialize-transaction.js
// Requires: Node 18+ (for built-in fetch)
// Run: PAYSTACK_SECRET_KEY=sk_test_xxx node initialize-transaction.js
const PAYSTACK_SECRET_KEY = process.env.PAYSTACK_SECRET_KEY;
async function initializeTransaction(email, amountInKobo, reference) {
const response = await fetch('https://api.paystack.co/transaction/initialize', {
method: 'POST',
headers: {
Authorization: `Bearer ${PAYSTACK_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email,
amount: amountInKobo,
reference,
callback_url: 'https://yoursite.com/payment/callback',
}),
});
const data = await response.json();
if (!data.status) {
throw new Error(`Paystack error: ${data.message}`);
}
return data.data;
// { authorization_url, access_code, reference }
}
// Example: charge 5,000 Naira (amount is in kobo, so 5000 * 100)
initializeTransaction('customer@example.com', 500000, `ref_${Date.now()}`)
.then((result) => {
console.log('Redirect customer to:', result.authorization_url);
})
.catch(console.error);
Key points about this code:
500000. 1,000 KES is 100000. Get this wrong and you will charge your customer 100x too much or too little. Read more in Understanding Kobo, Pesewa, and Cents.sk_test_ key (for testing) or sk_live_ key (for production). Never expose this key in frontend code. If you are wondering why, read Why You Must Never Call the Paystack API from Your Frontend.After calling this endpoint, you get back an authorization_url. Redirect the customer there. Paystack handles the rest of the checkout UI. The customer picks their payment channel (card, bank transfer, USSD, M-Pesa), enters their details, and pays.
For an alternative approach where the checkout appears as a popup on your own page instead of a redirect, look at Paystack Popup InlineJS Explained Line by Line. For the difference between redirect and inline, see Redirect Checkout vs Inline Checkout.
After the customer pays, Paystack redirects them to your callback_url with ?trxref=YOUR_REFERENCE&reference=YOUR_REFERENCE in the query string. Your job is to take that reference and verify the transaction with Paystack's API.
Here is the verification code:
// verify-transaction.js
// Requires: Node 18+
// Run: PAYSTACK_SECRET_KEY=sk_test_xxx node verify-transaction.js REF_HERE
const PAYSTACK_SECRET_KEY = process.env.PAYSTACK_SECRET_KEY;
async function verifyTransaction(reference) {
const response = await fetch(
`https://api.paystack.co/transaction/verify/${encodeURIComponent(reference)}`,
{
method: 'GET',
headers: {
Authorization: `Bearer ${PAYSTACK_SECRET_KEY}`,
},
}
);
const data = await response.json();
if (!data.status) {
throw new Error(`Verification failed: ${data.message}`);
}
return data.data;
}
async function handleCallback(reference, expectedAmountInKobo) {
const transaction = await verifyTransaction(reference);
// Check 1: Was the payment successful?
if (transaction.status !== 'success') {
console.log('Payment was not successful. Status:', transaction.status);
return false;
}
// Check 2: Does the amount match what we expected?
if (transaction.amount !== expectedAmountInKobo) {
console.log(
'Amount mismatch. Expected:',
expectedAmountInKobo,
'Got:',
transaction.amount
);
return false;
}
// Check 3: Is the currency correct?
// (Important if you operate in multiple countries)
if (transaction.currency !== 'NGN') {
console.log('Unexpected currency:', transaction.currency);
return false;
}
// All checks passed. Grant value.
console.log('Payment verified. Customer email:', transaction.customer.email);
return true;
}
// Example usage in an Express route:
// app.get('/payment/callback', async (req, res) => {
// const { reference } = req.query;
// const verified = await handleCallback(reference, 500000);
// if (verified) {
// res.redirect('/thank-you');
// } else {
// res.redirect('/payment-failed');
// }
// });
Three things must be true before you grant value:
transaction.status is "success" (not "abandoned", not "failed", not "pending")transaction.amount matches the amount you expected for that ordertransaction.currency is the currency you expectedSkipping the amount check is one of the most common payment integration bugs in Africa. An attacker can initialize their own transaction for 1 kobo, complete it, and pass your reference back. If you only check the status without verifying the amount, you grant full value for 1 kobo. For a thorough breakdown, see Verify Transaction: The Paystack Call You Must Never Skip and Verifying Amount and Currency Server-Side.
Also be aware of the double grant bug: your webhook and your callback handler can both fire for the same transaction. Make sure your grant-value logic is idempotent. The simplest approach is to check your database for an existing fulfilled order before granting again.
The callback redirect relies on the customer's browser. If they close the tab, lose connectivity, or their phone dies mid-redirect, you never get the callback. Webhooks are your safety net. Paystack sends a POST request to your webhook URL for every significant event, whether or not the customer's browser makes it back to your site.
The most important event is charge.success. It fires when a payment succeeds. For most applications, this is the only event you need to handle. See charge.success: The Only Event Most Apps Actually Need for a focused guide.
Here is how to handle a Paystack webhook with signature verification:
// webhook-handler.js
// Express.js webhook endpoint with signature verification
// IMPORTANT: You must read the raw body for signature verification.
// If you use express.json() globally, it will consume the body before
// you can hash it. Use express.raw() for the webhook route instead.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
// Parse the webhook body as raw buffer, not JSON
app.post(
'/webhooks/paystack',
express.raw({ type: 'application/json' }),
(req, res) => {
const PAYSTACK_SECRET_KEY = process.env.PAYSTACK_SECRET_KEY;
// Step 1: Verify the signature
const hash = crypto
.createHmac('sha512', PAYSTACK_SECRET_KEY)
.update(req.body)
.digest('hex');
const signature = req.headers['x-paystack-signature'];
if (hash !== signature) {
console.error('Invalid webhook signature. Ignoring request.');
return res.status(401).send('Invalid signature');
}
// Step 2: Respond with 200 immediately
// Paystack will retry if you do not respond within a few seconds.
res.status(200).send('OK');
// Step 3: Parse and process the event
const event = JSON.parse(req.body.toString());
if (event.event === 'charge.success') {
const transaction = event.data;
console.log('Payment received:', transaction.reference);
console.log('Amount:', transaction.amount, transaction.currency);
// TODO: Verify the transaction via the API (belt and suspenders)
// TODO: Update your database
// TODO: Send confirmation email
}
}
);
app.listen(3000, () => console.log('Webhook server running on port 3000'));
Critical rules for webhook handlers:
x-paystack-signature header contains an HMAC SHA-512 hash of the request body, using your secret key. If the hash does not match, someone is sending you a fake webhook. See Verifying the x-paystack-signature Header Correctly.For testing webhooks locally, you need a way to expose your local server to the internet. The two most common approaches are ngrok and Cloudflare Tunnel. For every event type Paystack sends and what to do with each, see Every Paystack Webhook Event Explained.
For the full engineering guide on webhooks, including queueing strategies, retry behavior, and deployment on serverless platforms, see Paystack Webhooks: Complete Engineering Guide.
Paystack has a built-in subscriptions system. You do not need to build your own billing loop or store card tokens manually. The flow works like this:
authorization_code tied to their payment method. You use this authorization to create a subscription on a plan.The authorization_code is central to recurring billing. When a customer pays you for the first time, Paystack returns an authorization object in the transaction data. This object contains a reusable authorization_code if the card supports it. For a detailed breakdown, see Authorization Codes: How Paystack Recurring Billing Really Works.
Common recurring billing patterns:
invoice.payment_failed events. You should email the customer and give them a window to update their payment method before disabling their access. See Building a Dunning Email Sequence.For subscription-related webhook events (subscription.create, subscription.disable, invoice.create, invoice.update), see Handling Subscription and Invoice Events.
Transfers let you send money from your Paystack balance to bank accounts or mobile money wallets. This is how you pay vendors, refund customers outside the normal refund flow, pay drivers, or distribute earnings to creators.
The transfer flow has three steps:
recipient_code.recipient_code, the amount, and a reason. Depending on your account settings, the transfer either processes immediately or waits for approval in the Paystack dashboard.transfer.success, transfer.failed, or transfer.reversed events to your webhook URL. See Handling Transfer Events for the full breakdown.Important details for transfers:
For bulk payouts (paying many recipients at once), see Bulk Transfers with the Paystack API. For building a complete payout system, see Building an Automated Payout System on Paystack.
If you are building a marketplace, a multi-vendor store, or any platform where money flows to multiple parties per transaction, Paystack split payments handle the distribution for you.
Splits work at the transaction level. When you initialize a transaction, you can specify how the payment should be divided:
Each vendor or service provider gets a Paystack subaccount. You create subaccounts via the API, specifying the vendor's bank account details and their settlement schedule. When a customer pays, Paystack splits the money automatically and settles each party's share separately.
This removes a significant compliance and operational burden. Without splits, you would collect all the money, hold it, and manually transfer each vendor's share. With splits, Paystack settles directly to each party. You never hold the vendor's money.
Example use cases:
For the full implementation pattern, see Building a Multi-Vendor Store with Paystack.
Kenya is a different payment market from Nigeria or Ghana. Mobile money dominates. Cash is common for in-person transactions. Card penetration is low compared to West Africa. When you build on Paystack in Kenya, the engineering decisions change accordingly.
As of the last update, Paystack in Kenya supports:
For the full rundown of what works and what does not, start with Paystack in Kenya: What It Supports and What It Does Not.
This is the question every Kenyan developer asks: "Can I do STK Push with Paystack?" The answer is nuanced. Paystack collects M-Pesa payments, but the experience is different from a direct Daraja STK Push.
With direct Daraja, you send an STK Push and the customer sees a prompt on their phone immediately. With Paystack, the customer goes through the Paystack checkout page, selects M-Pesa as their payment method, enters their phone number, and then receives the push. It is one extra step.
The tradeoff: Paystack gives you a unified API for cards and M-Pesa, handles reconciliation and settlements, and removes the complexity of managing Daraja credentials and callbacks. Daraja gives you a more direct M-Pesa experience and lower per-transaction fees, but you manage the entire integration yourself.
For the full engineering comparison, read Paystack vs Daraja: When to Use a Gateway and When to Go Direct. For the side-by-side comparison of user experience and technical tradeoffs, see M-Pesa Through Paystack vs Direct Daraja STK Push.
If you decide to use Paystack for M-Pesa collection, see:
If you decide to go with direct Daraja instead, Migrating from Paystack to Direct Daraja covers the switch. The reverse path is covered in Migrating from Direct Daraja to Paystack.
Some teams run both. See Running Paystack and Daraja Side by Side for the architecture pattern.
Pesalink is a bank-to-bank instant transfer network in Kenya. Paystack supports it as a payment channel, meaning customers can pay by transferring directly from their bank account. For the developer guide, see Pay with Pesalink on Paystack.
Paystack settles to Kenyan bank accounts and M-Pesa wallets. Settlement timelines vary by payment channel and your account configuration. For details, see Kenya Settlement to Bank or M-Pesa Wallet and Settlement Timelines and What Engineers Should Assume.
Where you place M-Pesa in your checkout flow affects your conversion rate. If cards appear first and M-Pesa is buried, you lose Kenyan customers. This is a design decision with engineering implications. See Why Kenyan Checkout Conversion Depends on Mobile Money Placement.
These guides use Kenyan payment channels and business contexts:
Kenya has specific data protection and tax requirements that affect payment integrations. The Kenya Data Protection Act governs how you store and process customer payment data. Digital Service Tax applies to certain online services. For engineering implications, see Kenya Data Protection Act and Payment Data Storage and Kenya Digital Service Tax and Online Payment Products.
KRA's eTIMS system for electronic tax invoices may also interact with your payment receipt flow. See KRA eTIMS and Payment Receipt Integration.
When you use Paystack's hosted checkout (redirect or Inline JS), Paystack handles PCI compliance. Card numbers go directly to Paystack's servers. They never touch your backend. You do not need to fill out a PCI Self-Assessment Questionnaire for SAQ-A or become PCI certified yourself, as long as you use the hosted checkout.
If you use the Charge API to build a fully custom checkout, the PCI requirements change. You are collecting card details on your own page and sending them to Paystack. This puts you in SAQ-A-EP territory at minimum. For most African startups, the hosted checkout is the right choice. Save the custom checkout for when you have a compliance team and a specific UX requirement that the hosted checkout cannot satisfy.
Security practices you should follow regardless of checkout type:
For IP allowlisting on your webhook endpoint, see Webhook IP Allowlisting.
Paystack provides a full test environment. Use your sk_test_ and pk_test_ keys. No real money moves. No real cards are charged. Test keys and live keys are separate; you cannot accidentally mix them if you use environment variables properly.
Test mode details:
For automated testing in your CI pipeline, see Testing Webhooks in CI Without a Public URL and Simulating Webhook Events for Automated Tests.
For end-to-end testing with browser automation, see End-to-End Payment Tests with Playwright and End-to-End Payment Tests with Cypress.
For testing failure paths (what happens when a payment fails, when a webhook times out, when the network drops mid-checkout), see Chaos Testing Payment Failure Paths.
A common testing mistake: forgetting to test the unhappy paths. Your integration works when payments succeed. But what happens when a payment fails? When the customer abandons checkout? When your webhook handler throws an error? When the amount is wrong? Test all of these before going live. See CI Pipelines for Payment Code for a practical setup.
These are the errors that show up in almost every Paystack integration. You will hit at least three of them.
Your secret key is wrong, missing, or you are using a test key against the live API (or vice versa). Check your Authorization header. Make sure it says Bearer sk_test_xxx or Bearer sk_live_xxx. Not Basic. Not Token. Not the public key. See Paystack 401 Unauthorized on Transaction Initialize.
Each currency has a minimum transaction amount. If you try to charge below the minimum, Paystack rejects the request. Remember that amounts are in the smallest currency unit (kobo, pesewas, cents). See Paystack Amount Too Low Error.
Your signature check fails because your framework parsed the JSON body before you could hash the raw bytes. This is the single most common webhook bug across all languages and frameworks. See The Raw Body Problem.
Your callback URL is not accessible from the internet, is using HTTP instead of HTTPS, or is returning an error. In development, make sure you are using a tunnel (ngrok, Cloudflare Tunnel) and that the URL is correctly configured in your dashboard or the API request. See Callback URLs: Per-Transaction vs Dashboard.
Your webhook handler and callback handler both grant value for the same transaction. Or your webhook handler processes the same event twice because Paystack retried. Make your handlers idempotent. See Idempotent Webhook Handlers and The Double Grant Bug.
Some payment channels (bank transfers, USSD) can leave transactions in a pending state for minutes or even hours. Your code must handle this state. Do not treat pending as success. Do not treat pending as failure. Poll the verify endpoint or wait for the webhook. See Pending Transactions: Handling the Grey Zone.
For a comprehensive error reference, including every gateway response code and what it means, see Paystack Gateway Responses. For debugging tools, see Debugging with the Transaction Timeline and Debugging with the Paystack CLI.
Everything above covers the API mechanics. The real work is turning those mechanics into products people pay for. The patterns change depending on what you are building.
E-commerce checkout. You need to initialize transactions, verify payments, handle abandoned carts, and issue refunds. If you sell physical goods, you also need to tie payment confirmation to inventory and shipping. See Kenyan E-Commerce Checkout.
SaaS subscription. You need plans, subscriptions, dunning (handling failed recurring charges), and plan upgrades/downgrades. See Building a SaaS Billing Page and Building a Membership Site.
Marketplace with payouts. You need split payments (subaccounts) for real-time splitting, or transfers for batch payouts. You need to handle vendor onboarding, settlement reporting, and disputes. See Building a Two-Sided Marketplace.
Wallet or prepaid system. You need to accept deposits (normal Paystack transactions), track balances in your database, and handle withdrawals (transfers). See Building a Wallet Top-Up Flow.
Invoicing tool. Paystack has a Payment Requests API that creates invoices with payment links. See Payment Requests and Invoices via API. For a Kenya-specific invoicing tool, see Freelancer Invoicing Tool for Kenyan Developers.
The builds hub has complete project guides that walk you through architecture, database design, API integration, and deployment for specific product types. Browse them at the builds section of the Paystack content cluster.
Payment integration is a high-demand skill in African tech. Every business that sells online needs it. Most developers avoid it because payment code feels risky. That gap is an opportunity.
Ways to earn from Paystack integration skills:
To build a portfolio that demonstrates payment integration skills, see Building a Payments Portfolio That Gets You Hired.
The code examples in this guide use plain Node.js with fetch. Paystack's API is language-agnostic, so you can use it from any backend that makes HTTP requests. The hub has framework-specific integration guides:
For webhook handling in specific frameworks, the webhook hub covers the common body-parsing and CSRF issues:
For deploying webhook handlers on serverless platforms:
This guide covers the full surface area of Paystack at a working level. Each section links to deeper guides on specific topics. Here is a summary of the content hubs in the Paystack cluster, organized by what you are trying to do:
Join the McTaba newsletter. Practical engineering content on payments, M-Pesa, USSD, WhatsApp, and AI for African developers.
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