
Pesapal Integration: Card Payments Across Africa
M-Pesa is unbeatable for mobile money in Kenya — but the moment you need to take a Visa or Mastercard, from a customer in Nairobi or a traveller paying from abroad, you need a gateway built for cards and mobile money in one place. That's where Pesapal fits, and why I reach for it when a product has to accept cards across African markets and beyond.
How does Pesapal handle card payments?
Pesapal is a pan-African payment gateway: you integrate once, then redirect the customer to Pesapal's hosted payment page where they pay with Visa, Mastercard, or Amex — or with mobile money like M-Pesa and Airtel — and you confirm the result with a status query. Because card details are entered on Pesapal's page, not yours, the PCI compliance and 3-D Secure burden stays with them, not you.
That last point is the quiet reason Pesapal is pleasant to work with for cards: you get card acceptance without ever touching a card number.
Why Pesapal is a strong fit for cards in Africa (and beyond)
A few things make it a good default when cards matter:
- One integration, many methods. The same checkout offers cards, mobile money, and bank options. In African markets, where one customer wants to tap M-Pesa and the next wants to swipe a Visa, forcing a single method loses sales. Pesapal lets you offer both without wiring up two gateways.
- Cards handled on a hosted page. The customer enters card details on Pesapal's secure page and 3-D Secure runs there, so you sidestep most PCI-DSS scope. Your server only ever sees a tracking ID and a status.
- Multi-country, multi-currency. Pesapal operates across Kenya, Uganda, Tanzania, Rwanda, Zambia, Malawi, and Zimbabwe, settling in local currencies or USD. International customers can pay by card in USD while local customers pay in their own currency or mobile money — which is exactly what "Africa and beyond" needs.
If you only ever take M-Pesa, a direct Daraja integration is simpler. The moment cards or multiple countries enter the picture, an aggregator like Pesapal earns its place.
The integration flow
Pesapal's API 3.0 follows a clear sequence. First, authenticate to get a short-lived bearer token:
import axios from "axios";
const BASE = "https://pay.pesapal.com/v3"; // sandbox: https://cybqa.pesapal.com/pesapalv3
async function getToken() {
const { data } = await axios.post(`${BASE}/api/Auth/RequestToken`, {
consumer_key: process.env.PESAPAL_CONSUMER_KEY,
consumer_secret: process.env.PESAPAL_CONSUMER_SECRET,
});
return data.token; // short-lived — cache briefly or fetch per checkout
}Register an IPN URL once and reuse the ipn_id it returns — this is where
Pesapal will notify you:
async function registerIpn(token) {
const { data } = await axios.post(
`${BASE}/api/URLSetup/RegisterIPN`,
{
url: `${process.env.BASE_URL}/api/pesapal/ipn`,
ipn_notification_type: "POST",
},
{ headers: { Authorization: `Bearer ${token}` } },
);
return data.ipn_id; // store and reuse
}Then submit the order. You get back a redirect_url — send the customer there to
choose card or mobile money and pay:
async function createPayment({ token, ipnId, orderRef, amount, email, phone }) {
const { data } = await axios.post(
`${BASE}/api/Transactions/SubmitOrderRequest`,
{
id: orderRef, // your unique merchant reference
currency: "KES", // or "USD" for international card payments
amount,
description: "Order payment",
callback_url: `${process.env.BASE_URL}/payment/return`,
notification_id: ipnId,
billing_address: { email_address: email, phone_number: phone },
},
{ headers: { Authorization: `Bearer ${token}` } },
);
return data; // { order_tracking_id, merchant_reference, redirect_url }
}
// → redirect the customer to data.redirect_url (Pesapal's hosted page)Confirming a card payment the right way
Here's the detail that catches people, and it's by design for security:
Pesapal's IPN and callback don't include the payment status. They hand you an
OrderTrackingId and nothing more — you then ask Pesapal what actually
happened:
app.post("/api/pesapal/ipn", async (req, res) => {
const { OrderTrackingId } = req.body;
// Acknowledge quickly
res.status(200).json({
orderNotificationType: req.body.OrderNotificationType,
orderTrackingId: OrderTrackingId,
status: 200,
});
// Then verify — the notification is only a trigger
const token = await getToken();
const { data } = await axios.get(
`${BASE}/api/Transactions/GetTransactionStatus?orderTrackingId=${OrderTrackingId}`,
{ headers: { Authorization: `Bearer ${token}` } },
);
// data.payment_status_description: "Completed" | "Failed" | "Invalid" | "Reversed"
// data.confirmation_code, data.amount, data.payment_method
// → update your payment record, idempotently, keyed on OrderTrackingId
});Never mark an order paid off the redirect back to your site or the raw IPN — only
GetTransactionStatus tells you the truth. This is the same principle that
should shape how you store and confirm payments:
verify, then commit atomically.
What makes card payments run smoothly
A few habits keep the card flow reliable:
- Store the
order_tracking_idagainst your order the moment you create the payment — it's the key you'll match every notification and status check to. - Pick the currency deliberately. For international card customers, charging in USD avoids confusing conversions; for local customers, their own currency and mobile money feel native.
- Let Pesapal own 3-D Secure. The hosted page handles the card challenge flow, so you don't build or maintain it.
- Be idempotent. Pesapal can notify more than once — dedupe on
OrderTrackingIdso a repeat notification never double-processes an order. - Reconcile. Some customers abandon the hosted page; sweep for orders still pending after a while and re-query their status.
Cards and mobile money in one checkout — the African reality
The biggest practical win isn't card support alone — it's not having to choose. A checkout in this region works best when it offers M-Pesa to the customer who lives on mobile money and a Visa field to the one who doesn't, in the same flow. Pesapal's hosted page does that out of the box, which is why, for products selling across African markets and to international customers, it's often the pragmatic pick over wiring several providers together yourself.
Always confirm the exact request and response fields against the current official Pesapal API 3.0 documentation before you go live — endpoints and payload shapes are the source of truth, and this is the pattern, not a substitute for their reference.
Need cards and mobile money built into your product?
Accepting both cards and mobile money cleanly — across countries and currencies, with payments that reconcile correctly — is exactly the kind of work I do in Node.js. If you're adding Pesapal, M-Pesa, or a mix to your product and want it built right, reach out, or find me on Fiverr and Upwork.


