Kenyan checkout conversion improves when you present M-Pesa as the first and most visible payment option. In the Paystack API, you control this with the channels array parameter when initializing a transaction. Passing channels: ["mobile_money", "card", "bank_transfer"] shows M-Pesa first. On a custom checkout, put the M-Pesa option at the top, pre-selected. Show the phone number input field immediately. Make the card option available but secondary. This matches how Kenyan customers expect to pay.
Ask a random person in Nairobi how they would pay for something online. The answer is M-Pesa. Not Visa. Not Mastercard. Not PayPal. M-Pesa.
This is not an exaggeration or a generalization for effect. M-Pesa has over 30 million active users in Kenya. It handles a significant share of the country's GDP in transaction volume. People use it to buy groceries, pay rent, settle restaurant bills, purchase airtime, and pay school fees. It is the financial infrastructure of the country.
Now look at most payment checkout pages built by developers who learned to code with international tutorials. The default view shows a card number field, an expiry date, a CVV input, and maybe a billing address form. Somewhere below, maybe behind a tab or an accordion, there is an option to pay with "other methods" including mobile money.
This is backwards for Kenya. You are showing a payment method that a fraction of your customers will use (cards) as the primary option, and hiding the method that the vast majority prefer (M-Pesa) behind extra clicks.
Every extra click between the customer and the payment button is a chance for them to leave. In Kenya, that means every extra click between the customer and the M-Pesa option is lost revenue.
Paystack gives you direct control over which payment methods appear at checkout and in what order. When you initialize a transaction (via the API or the Inline JS popup), you pass a channels array.
// Initialize a transaction with M-Pesa first
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.email,
amount: 250000, // KES 2,500 in cents
currency: 'KES',
reference: `order_${orderId}_${Date.now()}`,
channels: ['mobile_money', 'card', 'bank_transfer'],
callback_url: 'https://yoursite.com/payment/callback',
}),
});
The order of the array matters. Paystack Checkout presents the first channel as the default view. If mobile_money is first, the customer sees the M-Pesa option immediately when the checkout opens. If card is first, they see card fields.
For the Paystack Inline JS popup (frontend integration), the same parameter works:
// Paystack Inline JS with M-Pesa first
const handler = PaystackPop.setup({
key: 'pk_live_YOUR_PUBLIC_KEY',
email: customer.email,
amount: 250000,
currency: 'KES',
ref: `order_${orderId}_${Date.now()}`,
channels: ['mobile_money', 'card', 'bank_transfer'],
onClose: function () {
console.log('Checkout closed');
},
callback: function (response) {
// Verify the transaction on your server
verifyTransaction(response.reference);
},
});
handler.openIframe();
Channel options you can use:
mobile_money - M-Pesa and Airtel Moneycard - Visa, Mastercardbank_transfer - Pesalink and bank transfersapple_pay - Apple Pay (when available)bank - USSD bankingqr - QR code paymentsIf you omit the channels parameter entirely, Paystack shows all available channels for your account and currency. The default order may not prioritize M-Pesa. Always set it explicitly for Kenyan transactions.
For a deeper look at channel restriction and ordering, see Restricting Payment Channels at Checkout in Paystack.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
If you are building a custom checkout (not using Paystack's popup), the design decisions are yours. Here is how to get them right for Kenyan users.
1. Pre-select M-Pesa.
When the checkout page loads, M-Pesa should be the active tab, the highlighted option, or the expanded section. The customer should see a phone number input field immediately. No clicking required to reach the M-Pesa option.
2. Show the phone number field prominently.
The M-Pesa checkout requires one piece of input: the customer's phone number. Show this field large and clear. Pre-fill it if you already have the customer's phone number from their account profile. Use the tel input type so mobile keyboards show the number pad.
<div class="checkout-mpesa active">
<div class="payment-method-header">
<img src="/images/mpesa-logo.svg" alt="M-Pesa" width="80" />
<span>Pay with M-Pesa</span>
</div>
<label for="mpesa-phone">M-Pesa Phone Number</label>
<input
type="tel"
id="mpesa-phone"
name="phone"
placeholder="0712 345 678"
pattern="^(07|01|2547|2541)d{7,8}$"
required
autocomplete="tel"
/>
<p class="helper-text">
You will receive an M-Pesa prompt on this number. Enter your PIN to pay.
</p>
<button type="submit" class="pay-button">
Pay KES 2,500 with M-Pesa
</button>
</div>
3. Make card payment secondary but accessible.
Do not hide the card option. Some customers (especially corporate users, international visitors, or people using company cards) will want to pay by card. Show it as a tab or a link below the M-Pesa section. "Pay with card instead" as a text link works well.
4. Show Pesalink for high-value orders.
If the order amount is above a certain threshold (say, KES 50,000), consider showing Pesalink prominently alongside M-Pesa. M-Pesa has per-transaction limits that might not cover the full amount. Pesalink handles larger transfers. You can use the order amount to dynamically adjust which payment methods are shown first.
// Dynamic channel ordering based on amount
function getChannelOrder(amountInCents) {
const amountKES = amountInCents / 100;
if (amountKES > 70000) {
// High value: Pesalink first, M-Pesa second
return ['bank_transfer', 'mobile_money', 'card'];
}
// Standard: M-Pesa first
return ['mobile_money', 'card', 'bank_transfer'];
}
The majority of your Kenyan customers will visit your checkout page on a phone. Not a laptop. Not a desktop. A phone with a screen between 5 and 6.5 inches, often on a mobile data connection that might be 3G or a weak 4G signal.
Design decisions that matter for mobile checkout in Kenya:
Large tap targets. The "Pay" button should be at least 48 pixels tall. The payment method tabs should be large enough to tap without accidentally hitting the wrong one. Test on an actual phone, not just the browser's responsive mode.
Minimal scrolling. The phone number input and the Pay button should both be visible without scrolling on a standard phone screen. If the customer has to scroll down to find the Pay button, you have too much content above the fold.
Fast loading. Every kilobyte matters on a metered mobile connection. Do not load heavy JavaScript frameworks just for a checkout page. Do not load images that are not essential. The M-Pesa logo and the Pay button are essential. An animated background is not.
Offline resilience. If the connection drops between the customer entering their phone number and tapping Pay, what happens? The request should be queued and retried, not lost. At minimum, show a clear error message: "Connection lost. Tap Pay again when you are back online." See Handling Slow Networks in Paystack Checkout Flows.
No unnecessary form fields. Every field you add to the checkout reduces conversion. For an M-Pesa payment, you need the phone number. That is it. Do not ask for billing address, city, or postal code. If you need the customer's email for a receipt, pre-fill it from their account or ask for it separately.
Show the amount clearly. The customer should see exactly how much they are about to pay, in KES, on the Pay button itself. "Pay KES 2,500" is better than "Pay Now." The amount on the button must match the amount on the STK push. If they see KES 2,500 on your page and KES 2,500 on the M-Pesa prompt, they trust the process. If the numbers are different (even because of a fee or rounding issue), they will cancel.
Trust is the quiet factor that determines whether a customer enters their M-Pesa PIN or closes the page. In Kenya, payment trust works differently from Western markets.
The M-Pesa logo. Show it. Customers recognize the M-Pesa green. It signals that this is a legitimate payment method, not a scam. Use the official M-Pesa logo (check Safaricom's brand guidelines for usage rules).
The Paystack badge. If you are using Paystack, show "Secured by Paystack" or similar. Paystack is increasingly recognized in Kenya as a legitimate payment processor. The badge adds credibility, especially for first-time buyers on your site.
Explain what happens next. Before the customer taps Pay, tell them: "You will receive a prompt on your phone. Enter your M-Pesa PIN to complete the payment." This is obvious to you as a developer. It is not obvious to every customer. Especially for customers who have never paid via STK push before (they are used to manual paybill payments), the explanation removes anxiety.
Show the business name. The customer should know who they are paying. Show your business name clearly. When the STK push arrives on their phone, it will show a paybill name (Paystack's, not necessarily yours). If this is different from the name on your website, explain it. "The M-Pesa prompt will show 'Paystack' as the payee. This is our payment processor."
HTTPS. This should go without saying, but your checkout must be on HTTPS. Beyond security, Chrome and other browsers show "Not Secure" warnings on HTTP pages. A "Not Secure" warning on a payment page kills trust instantly.
No dark patterns. Do not hide fees. Do not pre-check boxes that add charges. Do not make the cancel button hard to find. Kenyan customers are wary of online payment scams (with good reason). Any hint of deception sends them away.
Loading states for M-Pesa are fundamentally different from loading states for card payments. With a card, the customer submits their details and waits. The loading spinner means "we are processing your card." With M-Pesa, the loading state means "check your phone and do something."
The correct sequence of states:
State 1: Input. The customer enters their phone number and taps Pay. The button changes to a loading state to prevent double-tapping.
State 2: Waiting for prompt. Show: "Sending M-Pesa prompt to 0712 345 678..." with a spinner. This lasts 1 to 5 seconds while Paystack triggers the STK push.
State 3: Action required. Show: "Check your phone for the M-Pesa prompt. Enter your PIN to pay KES 2,500." Remove the spinner. Replace it with a phone icon or illustration. The customer needs to know that the waiting is on their end now, not yours. This is the critical state. If you show a spinner, the customer thinks your system is working. But your system is done. It is their turn to act.
State 4: Confirming. After the customer enters their PIN (you detect this via polling or webhook), show: "Confirming your payment..." with a brief spinner.
State 5: Success. Show: "Payment received. Thank you." with a confirmation number and next steps.
State 5 (alt): Timeout. If no confirmation arrives within your timeout window: "The M-Pesa prompt may have expired." Show a "Resend prompt" button and a "Pay with card instead" link.
// Frontend state management for M-Pesa checkout
const MPESA_STATES = {
INPUT: 'input',
SENDING: 'sending',
WAITING_FOR_PIN: 'waiting_for_pin',
CONFIRMING: 'confirming',
SUCCESS: 'success',
TIMEOUT: 'timeout',
ERROR: 'error',
};
function getMpesaMessage(state, phone, amount) {
switch (state) {
case MPESA_STATES.SENDING:
return `Sending M-Pesa prompt to ${phone}...`;
case MPESA_STATES.WAITING_FOR_PIN:
return `Check your phone for the M-Pesa prompt. Enter your PIN to pay KES ${(amount / 100).toLocaleString()}.`;
case MPESA_STATES.CONFIRMING:
return 'Confirming your payment...';
case MPESA_STATES.SUCCESS:
return 'Payment received. Thank you!';
case MPESA_STATES.TIMEOUT:
return 'The M-Pesa prompt may have expired. You can try again or pay with a different method.';
case MPESA_STATES.ERROR:
return 'Something went wrong. Please try again.';
default:
return '';
}
}
Getting these states right is the difference between a 60% M-Pesa completion rate and an 85% completion rate. The customer needs to know what is expected of them at every moment.
The STK push not arriving is a common scenario in Kenya. It happens because of network delays, phone being off, Safaricom congestion, or the phone having a pending M-Pesa session from a different transaction. Your checkout needs a plan for this.
Step 1: Wait a reasonable time. Give it 15 to 20 seconds before showing any failure message. The push can be delayed, especially during peak hours.
Step 2: Show a retry option. After the wait, show: "Did not receive the prompt?" with a "Resend" button. When the customer taps Resend, initiate a new charge with a fresh reference (not a retry of the same reference).
Step 3: Offer troubleshooting tips. Below the retry button, show brief tips:
Step 4: Offer alternative payment methods. After two or three failed attempts, shift the emphasis: "Having trouble with M-Pesa? You can also pay by card or Pesalink." Show the card payment form or the Pesalink option directly. Do not make the customer navigate back to the payment method selection.
Step 5: Provide a support path. Show a WhatsApp number or a live chat link. "Need help? Message us on WhatsApp." In Kenya, WhatsApp is the default customer support channel. If you have a phone number customers can call or text, show it. A customer who cannot pay but can reach support is better than a customer who leaves silently.
// Checkout retry logic
let retryCount = 0;
const MAX_RETRIES = 3;
async function handleRetry() {
retryCount++;
if (retryCount > MAX_RETRIES) {
// Show alternative payment methods
setState(MPESA_STATES.ERROR);
showAlternativePaymentMethods();
return;
}
setState(MPESA_STATES.SENDING);
try {
const result = await initiateNewMpesaCharge(phone, amount);
setState(MPESA_STATES.WAITING_FOR_PIN);
startPolling(result.reference);
} catch (err) {
setState(MPESA_STATES.ERROR);
}
}
The pattern is: retry with grace, then pivot to alternatives, then offer human help. Never leave the customer stuck with no options.
No matter how well you build your checkout, some payments will have issues. The customer was charged but did not get confirmation. The STK push timed out but the money left their wallet. The payment went through but the order was not updated.
Your customer support team needs tools and information to handle these cases quickly.
For the customer:
For the support team:
Common support scenarios and resolutions:
| Customer says | Likely cause | Resolution |
|---|---|---|
| "Money was deducted but I did not get my order" | Missed webhook or timeout edge case | Verify on Paystack. If success, grant value. |
| "I paid but the page still shows pending" | Redirect/webhook race condition or polling failure | Verify on Paystack. Update status. Refresh the customer's page. |
| "I did not receive the M-Pesa prompt" | Phone off, wrong number, network issue, pending session | Confirm the phone number. Retry. Offer card payment. |
| "I was charged twice" | Double-tap, retry without checking previous status | Check Paystack for duplicate transactions. Refund the duplicate. |
Fast payment support builds trust. A customer who had a payment issue but got it resolved in five minutes via WhatsApp becomes a loyal customer. A customer who had a payment issue and could not reach anyone leaves a bad review and never comes back.
You cannot improve what you do not measure. Track these metrics to understand your checkout performance in Kenya:
Log these events from your frontend and correlate them with Paystack's transaction data. The numbers will tell you whether your M-Pesa-first design is working and where the remaining friction lives.
For the technical details of channel configuration, see Accepting M-Pesa Payments Through Paystack Checkout. For optimizing the checkout experience beyond channel ordering, see Paystack Checkout Conversion: Engineering Decisions That Move the Number.
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