Use Initialize Transaction (/transaction/initialize) for most integrations. It gives you a hosted checkout page, handles PCI compliance, and works with all payment channels. Use the Charge API (/charge) only when you need a fully custom checkout UI, are charging saved authorization codes, or need to handle mobile money without redirecting the customer.
Paystack's payment API has two main entry points. Every payment you accept goes through one of them.
POST /transaction/initialize
You send the amount, customer email, and optional parameters. Paystack returns an authorization URL and an access code. You send the customer to that URL (full redirect) or open a popup using the access code (Paystack Inline JS). The customer picks a payment method and completes the transaction on Paystack's hosted page. When they finish, Paystack redirects them back to your callback URL and fires a webhook.
POST /charge
You send the amount, customer email, and the payment details (card object, bank account, mobile money number, or USSD details). Paystack processes the charge directly. There is no redirect to a Paystack-hosted page. Depending on the bank's requirements, Paystack may respond with a status that requires follow-up actions: submitting an OTP, a PIN, a phone number, or redirecting for 3DS verification.
There is also a third endpoint worth mentioning here: POST /transaction/charge_authorization. This one charges a saved authorization code (a card token from a previous successful payment). It is technically part of the Initialize Transaction family but works without a checkout page.
Here is a complete Initialize Transaction flow with all the common parameters.
const express = require('express');
const app = express();
app.use(express.json());
app.post('/api/pay', async (req, res) => {
var email = req.body.email;
var amountInNaira = req.body.amount;
var orderId = req.body.orderId;
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: Math.round(amountInNaira * 100),
reference: 'order_' + orderId + '_' + Date.now(),
callback_url: 'https://yoursite.com/payment/callback',
channels: ['card', 'bank_transfer'],
currency: 'NGN',
metadata: {
order_id: orderId,
custom_fields: [
{
display_name: 'Order ID',
variable_name: 'order_id',
value: orderId,
},
],
},
}),
});
var data = await response.json();
if (data.status) {
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 });
}
});
What you get back:
authorization_url: Full URL to Paystack's checkout page. Redirect the customer here, or use it as a link.access_code: Short code to pass to Paystack Inline JS for a popup checkout experience.reference: The transaction reference. If you provided one, Paystack echoes it back. If not, Paystack generates one.What you do not need to handle:
This is why Initialize Transaction is the recommended starting point. You write one POST request, and Paystack handles every payment channel and every bank-specific flow inside their checkout.
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 puts you in control of the checkout experience. You collect payment details in your own UI and send them to Paystack directly. Here is a mobile money example.
app.post('/api/charge/mobile-money', async (req, res) => {
var email = req.body.email;
var phone = req.body.phone;
var amount = req.body.amount;
var 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: email,
amount: Math.round(amount * 100),
currency: 'GHS',
mobile_money: {
phone: phone,
provider: 'MTN',
},
}),
});
var data = await response.json();
// The Charge API returns a status that tells you what to do next
var status = data.data.status;
var chargeReference = data.data.reference;
if (status === 'success') {
// Rare for first attempt, but possible
res.json({ status: 'complete', reference: chargeReference });
} else if (status === 'send_otp') {
// Customer received an OTP on their phone
// You need to collect it and submit it
res.json({ status: 'otp_required', reference: chargeReference });
} else if (status === 'pay_offline') {
// Customer needs to approve on their device
res.json({
status: 'pending_approval',
reference: chargeReference,
display_text: data.data.display_text,
});
} else {
res.json({ status: status, reference: chargeReference });
}
});
// Handle OTP submission
app.post('/api/charge/submit-otp', async (req, res) => {
var otp = req.body.otp;
var reference = req.body.reference;
var response = await fetch('https://api.paystack.co/charge/submit_otp', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
otp: otp,
reference: reference,
}),
});
var data = await response.json();
if (data.data.status === 'success') {
res.json({ status: 'complete', reference: reference });
} else {
res.json({ status: data.data.status });
}
});
The multi-step nature of the Charge API
The Charge API is not a single request-response cycle. After your initial POST to /charge, the response status field tells you what happens next:
success: Charge completed. Verify and fulfill.send_otp: Collect the OTP from the customer and POST to /charge/submit_otp.send_pin: Collect the card PIN 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 data.data.url for 3DS verification.pay_offline: The customer needs to complete the payment on their device (common for mobile money).pending: Wait for a webhook or poll /charge/{reference}.Each follow-up request may return yet another status. A card charge might go: send_pin then send_otp then open_url then success. Your code must handle the full state machine. For a complete implementation, see building a fully custom checkout.
Here is a straightforward decision guide. Find your use case and use the recommended endpoint.
| Use Case | Endpoint | Why |
|---|---|---|
| First-time payment on a web app | Initialize Transaction | Fastest setup, PCI compliant, all channels supported |
| E-commerce checkout | Initialize Transaction | Customers expect a standard checkout flow |
| Subscription first payment | Initialize Transaction | Capture the authorization code from the verify response for future renewals |
| Subscription renewal | charge_authorization | No UI needed, charge the saved card directly |
| Saved card payment ("Pay with Visa ending 4081") | charge_authorization | Customer already authenticated, just charge the token |
| Custom mobile app checkout | Charge API | Mobile apps often need full control over the payment UI |
| Mobile money without redirect (Ghana) | Charge API | Collect phone number in your UI and charge directly |
| Background/automated charges | charge_authorization | No human interaction, server-to-server |
| Kiosk or POS-style payment | Charge API | Custom hardware UI, no browser available |
| Payment page (no code) | Neither (use Paystack Dashboard) | Paystack Payment Pages are a dashboard feature, no API needed |
The general rule: if the customer is present and it is their first payment, use Initialize Transaction. If the customer is not present or has paid before, use charge_authorization. If you need a custom checkout UI, use the Charge API.
Here are the practical differences that affect your code.
PCI compliance
Initialize Transaction: Paystack handles all card data. Your server never sees card numbers. You are PCI compliant by default.
Charge API: If you are collecting card details in your UI and passing them to /charge, card data passes through your system. You take on PCI compliance responsibilities. Most developers using the Charge API use it for non-card channels (mobile money, bank) or for saved authorization codes, which avoids this issue.
Payment channels
Initialize Transaction: Supports all channels through the hosted checkout. The customer picks their preferred method. You do not need separate code for each channel.
Charge API: Each payment method has a different request format. Card charges send a card object. Mobile money charges send a mobile_money object. Bank charges send a bank object. You write different code paths for each channel you want to support.
Error handling
Initialize Transaction: Errors at the checkout level (wrong card number, declined transaction) are handled by Paystack's UI. The customer sees an error message on the checkout page and can retry. Your code only sees the final result (success or failure) when you verify.
Charge API: You handle every error state yourself. If the bank sends send_otp, you need a UI to collect the OTP. If it sends open_url, you need to redirect. If the charge fails, you need to show the error message. You build the entire error-handling flow.
Integration time
Initialize Transaction: A working integration takes 30 to 60 minutes. One POST request on the server, one redirect or popup on the frontend, one verify call on the callback.
Charge API: A complete custom checkout (handling all status codes, all channels, all error states) can take days. Each payment method has its own flow, and you need to test each one.
The two endpoints are not mutually exclusive. Many production applications use both. Here is a common pattern.
First payment: Initialize Transaction
When a customer pays for the first time (signs up for a subscription, makes their first purchase), use Initialize Transaction. The customer goes through Paystack's hosted checkout, enters their card details, and completes 3DS verification. You verify the transaction and extract the authorization code from the response.
Subsequent payments: charge_authorization
For subscription renewals, repeat purchases, or "pay with saved card," charge the stored authorization code. No checkout UI needed.
// First payment: use Initialize Transaction
app.post('/api/subscribe', async (req, res) => {
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: req.body.email,
amount: 500000,
reference: 'sub_first_' + req.body.userId + '_' + Date.now(),
metadata: { user_id: req.body.userId, plan: 'monthly' },
}),
});
var data = await response.json();
res.json({ authorization_url: data.data.authorization_url });
});
// After verification, store the authorization code
// Then for renewals:
async function renewSubscription(customer) {
var 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: customer.authCode,
email: customer.email,
amount: 500000,
reference: 'sub_renew_' + customer.id + '_' + new Date().toISOString().slice(0, 7),
}),
}
);
var data = await response.json();
if (data.data.status !== 'success') {
// Card failed. Email the customer to update their payment method.
// Next attempt should use Initialize Transaction again.
await notifyCardFailed(customer);
}
return data;
}
This pattern gives you the best of both worlds: a smooth first payment experience with Paystack's hosted checkout, and frictionless renewals with saved cards. The two endpoints complement each other rather than compete.
For the complete guide to building on each endpoint, see the accept payments guide and Charge API for custom checkouts.
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