Skip to content

Mobile Payments (M-Pesa) ​

Accept M-Pesa mobile money payments through the STK Push (Lipa Na M-Pesa Online) integration.

M-Pesa is the most widely used mobile money service in East Africa. With Cashfin's M-Pesa integration, you can initiate payment requests directly to customers' phones, eliminating the need for them to manually enter paybill numbers or account references.

Use Cases

  • E-commerce Checkout: One-click payment at checkout
  • Bill Payments: Collect recurring payments for utilities or subscriptions
  • In-App Purchases: Enable seamless mobile purchases in your app
  • Point of Sale: Quick payments for physical stores

Endpoint ​

http
POST /business/payments/mobile/request

How It Works ​

  1. You send a payment request with customer's phone number and amount
  2. Cashfin sends an STK Push to the customer's phone
  3. Customer sees a prompt to enter their M-Pesa PIN
  4. Upon confirmation, payment is processed
  5. You receive a webhook notification

Request Body ​

FieldTypeRequiredDescription
amountnumberYesPayment amount in KES
phonestringYesCustomer's phone number (format: 254XXXXXXXXX)
channelstringYesPayment channel. Must be mpesa
referenceidstringNoYour reference ID for tracking

Phone Number Format

Phone numbers must be in international format without the + sign:

  • ✅ 254712345678
  • ❌ +254712345678
  • ❌ 0712345678

Example Request ​

bash
curl -X POST "https://api.cashfin.africa/business/payments/mobile/request" \
  -H "Authorization: cs_your_client_secret" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 1500,
    "phone": "254712345678",
    "channel": "mpesa",
    "referenceid": "ORDER-2024-001"
  }'
javascript
const paymentData = {
  amount: 1500,
  phone: "254712345678",
  channel: "mpesa",
  referenceid: "ORDER-2024-001",
};

const response = await fetch(
  "https://api.cashfin.africa/business/payments/mobile/request",
  {
    method: "POST",
    headers: {
      Authorization: "cs_your_client_secret",
      "Content-Type": "application/json",
    },
    body: JSON.stringify(paymentData),
  }
);

const data = await response.json();
console.log(data);
php
<?php
$paymentData = [
  'amount' => 1500,
  'phone' => '254712345678',
  'channel' => 'mpesa',
  'referenceid' => 'ORDER-2024-001'
];

$curl = curl_init();

curl_setopt_array($curl, [
  CURLOPT_URL => "https://api.cashfin.africa/business/payments/mobile/request",
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_POST => true,
  CURLOPT_POSTFIELDS => json_encode($paymentData),
  CURLOPT_HTTPHEADER => [
    "Authorization: cs_your_client_secret",
    "Content-Type: application/json"
  ],
]);

$response = curl_exec($curl);
$result = json_decode($response, true);

print_r($result);
python
import requests

payment_data = {
    'amount': 1500,
    'phone': '254712345678',
    'channel': 'mpesa',
    'referenceid': 'ORDER-2024-001'
}

response = requests.post(
    'https://api.cashfin.africa/business/payments/mobile/request',
    headers={
        'Authorization': 'cs_your_client_secret',
        'Content-Type': 'application/json'
    },
    json=payment_data
)

result = response.json()
print(result)

Response ​

Success Response ​

json
{
  "success": true,
  "message": "Payment created successfully",
  "data": {
    "checkoutrequestid": "ws_CO_15012024123456789",
    "merchantrequestid": "12345-67890-12345",
    "responsecode": "0",
    "responsedescription": "Success. Request accepted for processing",
    "transactionid": "txn_507f1f77bcf86cd799439011"
  }
}

Response Fields ​

FieldDescription
checkoutrequestidM-Pesa checkout request identifier
merchantrequestidMerchant request identifier
responsecode0 indicates success
responsedescriptionStatus message
transactionidCashfin transaction ID

Payment Confirmation ​

After the customer confirms payment on their phone, you'll receive a webhook:

json
{
  "id": "evt_507f1f77bcf86cd799439011",
  "type": "payment.completed",
  "created": "2024-01-15T10:35:00.000Z",
  "data": {
    "transactionid": "txn_507f1f77bcf86cd799439011",
    "checkoutrequestid": "ws_CO_15012024123456789",
    "amount": 1500,
    "phone": "254712345678",
    "mpesareceiptnumber": "RG12ABC345",
    "referenceid": "ORDER-2024-001",
    "status": "completed"
  }
}

Error Responses ​

Invalid Phone Number ​

json
{
  "success": false,
  "error": "Invalid phone number format"
}

Insufficient Setup ​

json
{
  "success": false,
  "error": "Payment method not configured"
}

M-Pesa Error ​

json
{
  "success": false,
  "error": "M-Pesa request failed",
  "details": "The initiator information is invalid"
}

M-Pesa Response Codes ​

CodeDescription
0Success - Request accepted
1Insufficient balance
2Less than minimum amount
3More than maximum amount
4Would exceed daily limit
5Would exceed minimum balance
6Unresolved primary party
7Unresolved receiver party
8Would exceed maximum balance
11Debit party name mismatch
12Credit party name mismatch

Integration Examples ​

Complete Payment Flow ​

javascript
// 1. Initiate Payment
async function initiatePayment(order) {
  const response = await fetch(
    "https://api.cashfin.africa/business/payments/mobile/request",
    {
      method: "POST",
      headers: {
        Authorization: API_KEY,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        amount: order.total,
        phone: order.customerPhone,
        referenceid: order.id,
        description: `Payment for Order #${order.number}`,
      }),
    }
  );

  const result = await response.json();

  if (result.success) {
    // Store checkout request ID for tracking
    await updateOrderPaymentStatus(
      order.id,
      "pending",
      result.data.checkoutrequestid
    );
    return { success: true, message: "Check your phone for M-Pesa prompt" };
  }

  return { success: false, error: result.error };
}

// 2. Handle Webhook
app.post("/webhooks/cashfin", async (req, res) => {
  const event = req.body;

  if (event.type === "payment.completed") {
    const { referenceid, mpesareceiptnumber, amount } = event.data;

    // Update order status
    await updateOrderPaymentStatus(referenceid, "paid", mpesareceiptnumber);

    // Notify customer
    await sendPaymentConfirmation(referenceid, amount, mpesareceiptnumber);
  }

  res.status(200).json({ received: true });
});

Phone Number Formatting ​

javascript
function formatPhoneNumber(phone) {
  // Remove all non-digits
  phone = phone.replace(/\D/g, "");

  // Handle different formats
  if (phone.startsWith("0")) {
    phone = "254" + phone.substring(1);
  } else if (phone.startsWith("+")) {
    phone = phone.substring(1);
  } else if (!phone.startsWith("254")) {
    phone = "254" + phone;
  }

  return phone;
}

// Usage
const formatted = formatPhoneNumber("0712345678"); // Returns: 254712345678

Retry with Exponential Backoff ​

javascript
async function initiatePaymentWithRetry(paymentData, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await fetch(
        "https://api.cashfin.africa/business/payments/mobile/request",
        {
          method: "POST",
          headers: {
            Authorization: API_KEY,
            "Content-Type": "application/json",
          },
          body: JSON.stringify(paymentData),
        }
      );

      const result = await response.json();

      if (result.success) {
        return result;
      }

      // Don't retry on validation errors
      if (response.status === 400) {
        throw new Error(result.error);
      }
    } catch (error) {
      if (attempt === maxRetries - 1) throw error;

      // Exponential backoff
      await new Promise((resolve) =>
        setTimeout(resolve, Math.pow(2, attempt) * 1000)
      );
    }
  }
}

Best Practices ​

1. Validate Phone Numbers ​

Always validate phone numbers before sending:

javascript
function isValidKenyanPhone(phone) {
  const cleaned = phone.replace(/\D/g, "");
  return /^254[17]\d{8}$/.test(cleaned);
}

2. Handle Timeouts ​

STK Push has a timeout period (typically 60-90 seconds):

javascript
// Set a timeout for user response
const PAYMENT_TIMEOUT = 90000; // 90 seconds

async function waitForPayment(checkoutRequestId) {
  return new Promise((resolve, reject) => {
    const timeout = setTimeout(() => {
      reject(new Error("Payment timeout - customer did not respond"));
    }, PAYMENT_TIMEOUT);

    // Listen for webhook or poll status
    onPaymentComplete(checkoutRequestId, (result) => {
      clearTimeout(timeout);
      resolve(result);
    });
  });
}

3. Idempotency ​

Use idempotency keys to prevent duplicate payments:

bash
curl -X POST "https://api.cashfin.africa/business/payments/mobile/request" \
  -H "Authorization: cs_your_client_secret" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: payment-order-001-attempt-1" \
  -d '{"amount": 1500, "phone": "254712345678"}'

4. Test in Sandbox ​

Always test with sandbox credentials first:

javascript
const API_URL =
  process.env.NODE_ENV === "production"
    ? "https://api.cashfin.africa"
    : "https://sandbox.api.cashfin.africa";

const API_KEY =
  process.env.NODE_ENV === "production"
    ? process.env.CASHFIN_LIVE_KEY
    : process.env.CASHFIN_TEST_KEY;

Troubleshooting ​

STK Push Not Received ​

  1. Verify phone number format (254XXXXXXXXX)
  2. Check if customer's phone is on and has M-Pesa
  3. Ensure amount is within M-Pesa limits (10 - 150,000 KES)

Payment Pending Too Long ​

  1. Customer may have cancelled or not responded
  2. Check webhook logs for status updates
  3. Implement a timeout mechanism

Duplicate Payment Requests ​

  1. Use idempotency keys
  2. Track checkout request IDs
  3. Implement request deduplication on your end

Cashfin Business API Documentation