Bonaventure OgetoBy Bonaventure Ogeto|

Why Kenyan Checkout Conversion Depends on Mobile Money Placement

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.

M-Pesa Is the Default Payment Method in Kenya

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.

The Paystack Channels Array

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 Money
  • card - Visa, Mastercard
  • bank_transfer - Pesalink and bank transfers
  • apple_pay - Apple Pay (when available)
  • bank - USSD banking
  • qr - QR code payments

If 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.

Designing a Custom Checkout for Kenya

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'];
}

Mobile-First Design for Kenyan Checkout

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 Signals for Kenyan Customers

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

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.

What to Do When the Push Does Not Arrive

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:

  • "Make sure your phone has network signal."
  • "Close any other M-Pesa prompts on your phone and try again."
  • "Make sure you have enough M-Pesa balance for KES X,XXX."

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.

Customer Support Flows for Payment Issues

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:

  • Show the transaction reference prominently on the success page and in the confirmation email/SMS. When a customer contacts support, the first question should be "What is your reference number?" not "Can you describe what happened?"
  • If the payment is still pending, show the reference on the waiting screen too. "Your reference is ORD-4582. Save this in case you need it."

For the support team:

  • Build an admin tool that lets support staff look up a payment by reference, phone number, or email. The tool should show the Paystack status, your database status, and highlight any discrepancy.
  • Give support staff the ability to trigger a manual verification (call the Paystack Verify API) from the admin panel. If the payment succeeded on Paystack but your system missed it, they can update the status and grant the customer their value.
  • Show the M-Pesa phone number used for the payment. When a customer calls and says "I paid from 0712345678 at 2 PM," the support agent can search by that phone number and time window.

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.

Measuring Checkout Conversion

You cannot improve what you do not measure. Track these metrics to understand your checkout performance in Kenya:

  • Checkout initiation rate: What percentage of customers who see the checkout page actually start the payment process (enter their phone number or card details)?
  • Channel selection breakdown: What percentage choose M-Pesa vs card vs Pesalink? If M-Pesa is below 70% in Kenya, your checkout design might be pushing customers toward the wrong method.
  • STK push completion rate: Of the customers who initiated an M-Pesa charge, what percentage completed the PIN entry? A low rate here suggests the push is not arriving (network issues) or the customer is abandoning at the PIN step.
  • Retry rate: How often do customers need to retry the STK push? A high retry rate suggests reliability issues.
  • Time to completion: How long between checkout initiation and successful payment? M-Pesa should average 15 to 30 seconds. If it is consistently over 60 seconds, investigate network or provider issues.
  • Fallback rate: How often do customers switch from M-Pesa to card (or vice versa) during a single checkout session? This tells you whether the primary method is working.
  • Drop-off by step: Where exactly are customers leaving? After seeing the checkout? After entering their phone number? After the STK push was sent but before entering the PIN? Each drop-off point has a different fix.

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.

Key Takeaways

  • M-Pesa dominates payment behavior in Kenya. If your checkout defaults to card input fields, you are forcing the majority of your customers to hunt for the payment method they want to use.
  • The Paystack channels array parameter lets you control which payment methods appear and in what order. Pass mobile_money first to prioritize M-Pesa in Paystack Checkout.
  • On custom checkouts, pre-select the M-Pesa tab and show the phone number input immediately. Do not make the customer click through to find it.
  • Mobile-first design is not optional in Kenya. Most of your customers are on phones with small screens and variable connection speeds. Your checkout must work well on a 5-inch screen over a 3G connection.
  • Trust signals matter. Show the M-Pesa logo, display the Safaricom STK push message clearly, and tell the customer exactly what will happen when they tap Pay.
  • Loading states for M-Pesa are different from card loading states. The customer needs to act on their phone, not just wait. Your UI must communicate this.
  • Have a plan for when the STK push does not arrive. Show a clear retry option, offer alternative payment methods, and provide a customer support path.

Frequently Asked Questions

How do I make M-Pesa the default payment method on Paystack Checkout?
Pass the channels array with mobile_money as the first element when initializing the transaction: channels: ["mobile_money", "card", "bank_transfer"]. Paystack Checkout will show M-Pesa as the default view when it opens. If you want M-Pesa as the only option, pass channels: ["mobile_money"].
Does the channels array order actually affect which method customers use?
Yes. The first channel in the array is shown as the default view in Paystack Checkout. Customers tend to use whatever is shown first unless they have a specific preference for another method. Putting mobile_money first in Kenya means most customers will never need to switch tabs. They see M-Pesa, enter their phone number, and pay.
Should I hide card payments entirely for Kenyan customers?
No. Some Kenyan customers prefer cards, especially for higher amounts, corporate purchases, or international services. Show M-Pesa first but keep card available as a secondary option. The only case where you might hide cards is if your product specifically targets a demographic that exclusively uses M-Pesa (for example, a rural agricultural payments app).
What if M-Pesa is having downtime during checkout?
When M-Pesa is down (Safaricom maintenance or outages), STK pushes will fail. Your checkout should detect this (after one or two failed attempts) and automatically shift emphasis to card or Pesalink. Show a message: "M-Pesa is currently experiencing delays. You can pay by card or bank transfer instead." Do not leave customers stuck waiting for a prompt that will never arrive.
How do I pre-fill the phone number for M-Pesa checkout?
If you have the customer's phone number from their account profile, pass it in the Charge API request or pre-fill the phone input field on your custom checkout. For the Paystack Checkout popup, you cannot pre-fill the phone number field directly. For the Charge API, pass the phone number in the mobile_money.phone field. For a custom checkout, set the input value and let the customer confirm or change it.

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