To accept payments with Paystack, create a server-side POST request to the Initialize Transaction endpoint with the customer email, amount in the smallest currency unit (kobo for NGN, pesewas for GHS), and a callback URL. Paystack returns an authorization URL. Redirect the customer there or open it in a popup. After payment, verify the transaction server-side before granting value.
Paystack gives you two distinct API paths for collecting money from customers. Every feature, every channel, and every payment type flows through one of these two endpoints. Understanding the difference before you write any code saves you from rebuilding later.
Path 1: Initialize Transaction
You POST to /transaction/initialize with the customer's email, amount, and (optionally) a callback URL. Paystack returns an authorization_url. You send the customer there. Paystack handles the entire checkout experience: collecting card details, running 3DS verification, showing bank transfer instructions, or prompting for a USSD code. When the customer finishes, Paystack redirects them back to your callback URL with a reference you can verify.
This is what most integrations use. It is fast to set up, PCI-compliant by default (card details never touch your server), and Paystack keeps updating the checkout UI without you changing a line of code.
Path 2: The Charge API
You POST to /charge with the customer's payment details (card token, bank account, mobile money number, or USSD code) directly. You build the entire checkout UI yourself. You handle OTP prompts, PIN collection, and 3DS redirects by responding to Paystack's status codes and following up with /charge/submit_otp, /charge/submit_pin, or /charge/submit_phone.
The Charge API exists for apps that need full control over the payment experience: custom mobile UIs, background charges on saved cards, or payment flows where redirecting to an external page breaks the user experience. It requires more work and more careful security handling.
Which one should you pick?
The rest of this guide covers both paths. We start with Initialize Transaction because it is the faster path to a working integration, then cover the Charge API for when you need more control.
The Initialize Transaction flow has four steps: initialize on your server, redirect or popup, customer pays, you verify. Here is each step with working code.
Step 1: Initialize the transaction from your server
Your backend sends a POST request to Paystack with the customer's email and the amount. This must happen server-side because the request includes your secret key.
// Node.js + Express example
const express = require('express');
const app = express();
app.use(express.json());
app.post('/api/pay', async (req, res) => {
const { email, amountInNaira } = req.body;
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: amountInNaira * 100, // Convert to kobo
callback_url: 'https://yoursite.com/payment/callback',
reference: `order_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
}),
});
const data = await response.json();
if (data.status) {
// data.data.authorization_url is where the customer pays
// data.data.access_code is used for inline/popup checkout
// data.data.reference is the transaction reference
res.json({
authorization_url: data.data.authorization_url,
access_code: data.data.access_code,
reference: data.data.reference,
});
} else {
res.status(400).json({ error: data.message });
}
});
Paystack responds with three important values:
authorization_url: A full URL (like https://checkout.paystack.com/abc123) where the customer completes paymentaccess_code: A short code you can pass to Paystack's inline JavaScript library for popup checkoutreference: The transaction reference (either the one you provided or one Paystack generated)Step 2: Send the customer to pay
You have two options here. The first is a full-page redirect:
// Frontend: redirect to Paystack checkout
window.location.href = authorizationUrl;
The second is the inline popup, which keeps the customer on your page:
<!-- Load Paystack inline JS -->
<script src="https://js.paystack.co/v2/inline.js"></script>
<script>
const popup = new PaystackPop();
popup.checkout({
accessCode: 'ACCESS_CODE_FROM_YOUR_SERVER',
onSuccess: (transaction) => {
// transaction.reference contains the reference
// Verify on your server before granting value
verifyOnServer(transaction.reference);
},
onCancel: () => {
console.log('Customer closed the popup');
},
});
</script>
Both options lead to the same Paystack checkout page. The redirect version takes the customer away from your site entirely. The popup version opens Paystack's checkout in an overlay on top of your page. Choose redirect if you want simplicity; choose popup if you want the customer to stay on your page.
Step 3: Customer completes payment
On the Paystack checkout page, the customer picks a payment method and completes the transaction. Paystack handles card validation, 3DS challenges, bank transfer instructions, and everything else. You do not need to write any code for this step.
After payment, two things happen:
callback_url with ?trxref=REFERENCE&reference=REFERENCE appended as query parameterscharge.success webhook to your webhook URL (if configured in your Paystack dashboard)Step 4: Verify the transaction
This is the step most tutorials gloss over and the one that matters most. Before you grant any value (deliver a product, credit an account, activate a feature), you must verify the transaction server-side.
app.get('/payment/callback', async (req, res) => {
const reference = req.query.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.status &&
data.data.status === 'success' &&
data.data.amount === expectedAmountInKobo &&
data.data.currency === 'NGN'
) {
// Payment confirmed. Grant value.
// Mark this reference as fulfilled in your database
// to prevent double-granting if the webhook also fires.
} else {
// Payment failed or amounts do not match.
// Show an error page.
}
});
Three checks are non-negotiable in the verify step: the transaction status must be "success", the amount must match what you expected, and the currency must be correct. Skipping any of these opens you up to underpayment attacks or currency mismatch exploits. Read more in our verification and reconciliation guide.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
The Charge API gives you direct control over the payment flow. Instead of redirecting to Paystack's checkout, you collect payment details in your own UI and send them to /charge. This is useful for mobile apps, background charges on saved cards, and payment flows where a redirect would break the experience.
Charging a saved card (authorization code)
After a customer pays successfully through Initialize Transaction, the verify response includes an authorization object with an authorization_code. You can store this code and use it to charge the customer again without them re-entering their card details.
const response = await fetch('https://api.paystack.co/transaction/charge_authorization', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
authorization_code: 'AUTH_abc123def456',
email: 'customer@example.com',
amount: 500000, // 5,000 NGN in kobo
reference: `sub_renewal_${customerId}_${Date.now()}`,
}),
});
const data = await response.json();
// Check data.data.status === 'success'
Charging with the Charge endpoint
The /charge endpoint handles multiple payment types. The flow is not always one request and done. Depending on the payment method and the bank's requirements, Paystack may ask you to submit additional information (OTP, PIN, phone number, birthday) before the charge completes.
// Mobile money charge example
const response = await fetch('https://api.paystack.co/charge', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: 'customer@example.com',
amount: 10000, // 100 GHS in pesewas
currency: 'GHS',
mobile_money: {
phone: '0551234567',
provider: 'MTN', // MTN, VOD (Vodafone), TGO (AirtelTigo)
},
}),
});
const data = await response.json();
if (data.data.status === 'send_otp') {
// Prompt customer for OTP, then submit it
// POST /charge/submit_otp with { otp, reference }
}
if (data.data.status === 'success') {
// Charge completed immediately
}
The Charge API returns a status field that tells you what to do next:
success: Charge completed. Verify and grant value.send_otp: The bank sent an OTP to the customer. Collect it and POST to /charge/submit_otp.send_pin: The customer needs to enter their card PIN. Collect it and POST to /charge/submit_pin.send_phone: Collect the phone number linked to the card and POST to /charge/submit_phone.send_birthday: Collect the cardholder's birthday and POST to /charge/submit_birthday.open_url: Redirect the customer to the URL in data.data.url for 3DS verification.pending: The transaction is being processed. Poll /charge/{reference} or wait for a webhook.Building a full custom checkout on the Charge API requires handling all of these statuses. For a complete walkthrough, see building a fully custom checkout on the Charge API.
Paystack supports multiple payment channels, and new ones get added as Paystack expands across Africa. Here is what is available and when each one makes sense.
Card
Visa, Mastercard, and Verve cards. This is the default channel and the one most customers in Nigeria use for online payments. Cards go through 3DS verification for security. Paystack handles the entire card flow in their checkout UI, so you do not touch card numbers.
Bank Transfer
Paystack generates a temporary bank account number. The customer transfers the exact amount to that account from their banking app. Once the transfer hits, Paystack confirms the payment. This works well for customers who do not have cards or prefer not to enter card details online. The generated account number typically expires after a set period (usually 30 minutes to a few hours, depending on configuration).
USSD
The customer dials a USSD code (like *737*amount*ref# for GTBank) from their phone to authorize the payment. USSD payments do not require a smartphone or internet connection on the customer's end. This channel works well in areas with limited connectivity.
Mobile Money
Available in Ghana (MTN, Vodafone, AirtelTigo) and other markets where mobile money is the primary payment method. The customer enters their mobile money number and confirms on their phone. Mobile money is the dominant payment channel in Ghana and is growing across East and West Africa.
QR Code
Paystack generates a QR code that the customer scans with their banking app to authorize payment. This is useful for in-person payments or kiosk scenarios where typing card details is impractical.
Apple Pay
Available for customers with Apple devices that support Apple Pay. Requires domain verification setup on your end. Provides a fast checkout experience for iOS and Safari users.
Restricting channels
By default, Paystack shows all available channels to the customer. You can restrict which channels appear by passing a channels array when initializing:
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: 'customer@example.com',
amount: 100000, // 1,000 NGN
channels: ['card', 'bank_transfer'], // Only show card and bank transfer
}),
});
Valid channel values include: card, bank, ussd, mobile_money, bank_transfer, qr, apple_pay. For a detailed look at channel selection strategy, see restricting payment channels at checkout.
This trips up nearly every developer on their first Paystack integration: amounts are in the smallest currency unit, not the main unit.
amount: 500000.amount: 5000.amount: 20000.amount: 100000.amount: 1000.The formula is always the same: amount = display_price * 100.
If you forget to multiply by 100, a customer trying to pay 5,000 NGN gets charged 50 NGN instead. If you multiply twice, they get charged 500,000 NGN. Both are bad. Build a helper function and use it everywhere:
// Convert human-readable amount to Paystack amount
function toSmallestUnit(amount) {
return Math.round(amount * 100);
}
// Convert Paystack amount back to human-readable
function fromSmallestUnit(amount) {
return amount / 100;
}
// Usage
const paystackAmount = toSmallestUnit(5000); // 500000
const displayAmount = fromSmallestUnit(500000); // 5000
Use Math.round() in the conversion to avoid floating-point issues. JavaScript floating-point arithmetic can produce results like 19.99 * 100 = 1998.9999999999998, which would cause the charge to fail because Paystack expects an integer.
When verifying a transaction, the amount in the verify response is also in the smallest unit. Compare it directly against the amount you initialized with. Do not convert and compare display values, because floating-point rounding can cause false mismatches.
For the full breakdown of currency handling across all Paystack-supported currencies, see kobo, pesewa, and cents explained and currency handling in Paystack.
Metadata lets you attach extra information to a transaction that Paystack stores and returns in verify responses and webhooks. This is how you link a Paystack transaction to your internal records without building a separate mapping table.
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: 'customer@example.com',
amount: 250000, // 2,500 NGN
metadata: {
order_id: 'ORD-2026-00142',
product_name: 'Annual Subscription',
user_id: 'usr_8f3k2n',
cancel_action: 'https://yoursite.com/payment/cancelled',
},
}),
});
The metadata field accepts any JSON object. Whatever you put in it comes back in the verify response at data.metadata and in webhook payloads at data.metadata. Use it to store order IDs, user IDs, cart contents, or anything else your fulfillment logic needs.
Custom fields
Custom fields are a special metadata structure that Paystack displays on the checkout page and in the transaction details on your dashboard. Use them when you need the customer to see specific information during checkout, or when your support team needs to quickly identify what a transaction was for.
metadata: {
custom_fields: [
{
display_name: 'Order Number',
variable_name: 'order_number',
value: 'ORD-2026-00142',
},
{
display_name: 'Delivery Address',
variable_name: 'delivery_address',
value: '15 Adeola Odeku, Victoria Island, Lagos',
},
],
}
Each custom field needs three properties: display_name (what the customer and your team see), variable_name (the machine-readable key), and value (the actual data). Custom fields appear on the Paystack checkout page below the payment form, so they are useful for order confirmation details.
You can combine regular metadata and custom fields in the same request. Regular metadata is for your backend logic; custom fields are for human-readable display. For advanced patterns, see passing custom data through a transaction and collecting extra checkout information.
Every Paystack transaction has a reference: a unique string that identifies it. If you do not provide one when initializing, Paystack generates a random one for you. You should always generate your own. Here is why.
References prevent duplicate charges
If your frontend sends the initialize request twice (user double-clicks the pay button, network retry, browser refresh), and you are generating references yourself, both requests use the same reference. Paystack treats the second request as a duplicate and returns the same transaction instead of creating a new one. If you let Paystack generate random references, each request creates a separate transaction, and the customer might get charged twice.
Designing good references
A good reference is unique, deterministic (the same inputs always produce the same reference), and readable when you are debugging at 2 AM. Here are patterns that work:
// Pattern 1: Entity-based (best for orders)
const reference = `order_${orderId}_${Date.now()}`;
// Example: order_142_1721472000000
// Pattern 2: User + action (best for subscriptions or top-ups)
const reference = `sub_${userId}_${billingPeriod}`;
// Example: sub_usr8f3k_2026-07
// Pattern 3: Idempotency key style
const reference = `pay_${orderId}`;
// Example: pay_ORD-2026-00142
// Same order always gets the same reference
Pattern 3 is the strongest for preventing duplicate charges. If order ORD-2026-00142 should only be paid once, using pay_ORD-2026-00142 as the reference means any second attempt to initialize payment for that order returns the existing transaction instead of creating a new one.
Reference constraints
Store the reference in your database alongside the order or payment record. When the webhook or callback arrives, look up the reference to find which order to fulfill. For deeper patterns on reference design and idempotency, see transaction reference design patterns and idempotency in payment flows.
After a customer completes payment on Paystack's checkout page, Paystack redirects them back to your site. Where they land depends on how you configured the callback URL.
Per-transaction callback URL
You can set a callback_url in the initialize request. This overrides the default callback URL in your Paystack dashboard for this specific transaction:
body: JSON.stringify({
email: 'customer@example.com',
amount: 100000,
callback_url: 'https://yoursite.com/payment/callback',
})
After payment, Paystack redirects to: https://yoursite.com/payment/callback?trxref=REF123&reference=REF123
Dashboard default callback URL
In your Paystack dashboard under Settings, you can set a default callback URL. Any transaction that does not include a callback_url in the initialize request uses this default. Set it to a generic payment-complete page on your site.
Handling the redirect
When the customer arrives at your callback URL, extract the reference query parameter and verify the transaction immediately:
// Express route handling the callback redirect
app.get('/payment/callback', async (req, res) => {
const { reference } = req.query;
if (!reference) {
return res.status(400).send('Missing reference');
}
// Verify the transaction
const verifyResponse = await fetch(
`https://api.paystack.co/transaction/verify/${reference}`,
{
headers: {
Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
},
}
);
const data = await verifyResponse.json();
if (data.data.status === 'success') {
// Look up the order associated with this reference
// Mark it as paid (if not already marked by webhook)
// Show the customer a success page
res.redirect('/order/success?ref=' + reference);
} else {
res.redirect('/order/failed?ref=' + reference);
}
});
Important: the redirect is not confirmation
The fact that a customer landed on your callback URL does not mean they paid. Anyone can type that URL into a browser with a fake reference. The verify call is what confirms payment. Never grant value based on the redirect alone.
Also, the redirect and the webhook can arrive in any order. Sometimes the webhook arrives before the customer is redirected. Sometimes the redirect happens first. Your code must handle both scenarios without double-granting value. The standard approach: use a database flag (like order.paid = true) and check it in both the webhook handler and the callback handler. Whichever fires first sets the flag; whichever fires second sees the flag is already set and skips fulfillment.
For a full comparison of per-transaction vs dashboard callback URLs and the edge cases each creates, see callback URL configuration patterns.
This guide is the hub for everything about accepting payments with Paystack. Each topic mentioned above has a dedicated deep-dive article with full code examples, edge cases, and production patterns. Here is how the articles in this cluster are organized:
Core concepts
Checkout
Payment channels
Amounts, currencies, and data
References, callbacks, and idempotency
Advanced payment types
After payment
Once payment is collected, your next steps are setting up webhooks to get real-time notifications and verifying and reconciling transactions to make sure every payment is accounted for. For the full picture of how all of these fit together, see the Paystack engineering guide.
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