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