All blog

Accepting Payments in Africa: M-Pesa, Paystack & Flutterwave

Accept African payments with M-Pesa, Paystack, and Flutterwave: server-side verification, signed idempotent webhooks, and keeping API secret keys safe.

Business & SaaSStephen NdegwaPublished 2 min read
Share
Accepting Payments in Africa: M-Pesa, Paystack & Flutterwave

If you are building an application for African users, payments are not an afterthought — they are the product. And the payment landscape here is distinctive: mobile money is often more common than cards, and a handful of local providers dominate. This guide covers how to think about accepting payments through M-Pesa, Paystack, and Flutterwave in a cloud-hosted application.

Know your rails

  • M-Pesa — dominant in Kenya and several neighbouring markets. The Daraja API supports STK Push (prompting a user to approve a payment on their phone), C2B, B2C, and transaction status queries.
  • Paystack — strong across Nigeria, Ghana, and South Africa. Clean developer experience, cards plus bank transfer and mobile money.
  • Flutterwave — broad multi-country coverage and a wide range of payment methods, useful when you need one integration spanning several markets.

Many teams integrate more than one provider to maximise coverage and add redundancy — if one gateway has an outage, another can take over.

The universal flow

Despite different APIs, almost every integration follows the same shape:

  1. Initialize a transaction from your backend with an amount, currency, and reference.
  2. Redirect or prompt the user to complete payment (a hosted checkout page, or an STK Push to their phone).
  3. Receive a webhook from the provider confirming the outcome.
  4. Verify the transaction server-side before granting value.

Rule one: never trust the client

The single most important security principle in payments: always verify on your server. A redirect back to a “success” page proves nothing — it can be forged. Confirm every payment by calling the provider’s verification endpoint (or validating a signed webhook) from your backend before you ship goods, unlock a feature, or credit an account.

# Pseudocode: verify before granting value
event = validate_webhook_signature(request)
if event.status == "success":
    payment = provider.verify(event.reference)  # server-to-server
    if payment.amount == expected_amount:
        grant_value(payment.customer)

Handle webhooks well

Webhooks are how you learn about payments that complete asynchronously — which, with mobile money, is most of them. Three things make webhook handling robust:

  • Verify signatures so you know the request genuinely came from the provider.
  • Be idempotent — providers may deliver the same event more than once, so processing it twice must not charge or credit twice.
  • Respond fast — acknowledge quickly and do heavy work in a background job, or the provider may retry.

Keep secrets on the server

API secret keys belong in server-side environment variables — never in frontend code, never in a public repository. Use each provider’s public key for client-side initialization and keep the secret key strictly on your backend, where your cloud server can protect it.

Test thoroughly before going live

Every provider offers a sandbox or test mode with test credentials and phone numbers. Run your full flow — success, failure, timeout, and duplicate webhook — against the sandbox before switching to live keys. Payments are the one part of your app where a bug costs real money, so it is worth the extra care.

Get the fundamentals right — server-side verification, signed and idempotent webhooks, protected secrets — and you have a payment stack that is secure, reliable, and ready for real African customers.