Initialize Transaction vs Charge API: Which Paystack Endpoint to Use
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.
The Two Endpoints at a Glance
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.
Initialize Transaction in Detail
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:
- Card number collection (Paystack's checkout page does this)
- OTP or PIN prompts (Paystack handles them inside their checkout)
- 3DS redirects (handled transparently within the checkout flow)
- Bank transfer instructions (Paystack shows the generated account number)
- Mobile money prompts (Paystack handles the provider-specific flow)
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.
The Charge API in Detail
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 todata.data.urlfor 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.
Decision Matrix: Which Endpoint for Which Use Case
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.
Technical Comparison
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.
Using Both Endpoints Together
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.
Key Takeaways
- ✓Initialize Transaction (POST /transaction/initialize) creates a hosted checkout session. You get an authorization URL and an access code. The customer completes payment on Paystack's page. This is the fastest path to accepting payments.
- ✓The Charge API (POST /charge) lets you send payment details directly. You build the checkout UI. You handle OTP prompts, PIN collection, and 3DS redirects. More control, more responsibility.
- ✓Initialize Transaction is PCI-compliant by default because card details never touch your server. The Charge API requires you to handle sensitive data carefully if you are collecting card details.
- ✓For charging saved cards (authorization codes), use POST /transaction/charge_authorization. This is a separate endpoint from /charge and is simpler for recurring payments.
- ✓The Charge API supports multi-step flows: send_otp, send_pin, send_phone, send_birthday, and open_url. You must handle all of these status codes if you go the custom checkout route.
- ✓Start with Initialize Transaction. Add Charge API flows later only for specific use cases that require it. The two endpoints are not mutually exclusive.
Frequently Asked Questions
- Can I use Initialize Transaction for mobile apps?
- Yes. You initialize the transaction on your server and open the authorization URL in a WebView or in-app browser on the mobile device. The customer completes payment on Paystack's checkout page inside the WebView. When they are redirected to your callback URL, you intercept the redirect in the WebView and verify the transaction. This is simpler than building a full custom checkout with the Charge API.
- Is the Charge API faster than Initialize Transaction?
- Not in terms of processing speed. Both endpoints go through the same banking infrastructure. The Charge API can feel faster for returning customers on saved cards because there is no redirect. But for first-time payments, the Charge API adds complexity (handling OTP, PIN, 3DS) that Initialize Transaction handles automatically.
- What happens if I use the wrong endpoint?
- You will not break anything. Using Initialize Transaction when you could use the Charge API just means the customer goes through the hosted checkout (which is fine). Using the Charge API when Initialize Transaction would suffice means you write more code to handle edge cases. The payment still works either way. Pick the one that matches your UX requirements.
- Does the Charge API support all payment channels?
- The Charge API supports card, bank, mobile_money, ussd, and bank_transfer. However, each channel requires a different request format with channel-specific fields. Initialize Transaction supports all channels through a single request format because Paystack's checkout handles channel selection and input collection.
- Can I switch from Initialize Transaction to the Charge API later?
- Yes. The two endpoints are independent. You can start with Initialize Transaction today and add Charge API flows later for specific use cases. Your existing transactions and authorization codes work the same regardless of which endpoint created them. Many production apps use Initialize Transaction for web checkout and the Charge API for mobile or background charges.
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