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/requestHow It Works
- You send a payment request with customer's phone number and amount
- Cashfin sends an STK Push to the customer's phone
- Customer sees a prompt to enter their M-Pesa PIN
- Upon confirmation, payment is processed
- You receive a webhook notification
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Payment amount in KES |
phone | string | Yes | Customer's phone number (format: 254XXXXXXXXX) |
channel | string | Yes | Payment channel. Must be mpesa |
referenceid | string | No | Your 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
| Field | Description |
|---|---|
checkoutrequestid | M-Pesa checkout request identifier |
merchantrequestid | Merchant request identifier |
responsecode | 0 indicates success |
responsedescription | Status message |
transactionid | Cashfin 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
| Code | Description |
|---|---|
0 | Success - Request accepted |
1 | Insufficient balance |
2 | Less than minimum amount |
3 | More than maximum amount |
4 | Would exceed daily limit |
5 | Would exceed minimum balance |
6 | Unresolved primary party |
7 | Unresolved receiver party |
8 | Would exceed maximum balance |
11 | Debit party name mismatch |
12 | Credit 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: 254712345678Retry 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
- Verify phone number format (254XXXXXXXXX)
- Check if customer's phone is on and has M-Pesa
- Ensure amount is within M-Pesa limits (10 - 150,000 KES)
Payment Pending Too Long
- Customer may have cancelled or not responded
- Check webhook logs for status updates
- Implement a timeout mechanism
Duplicate Payment Requests
- Use idempotency keys
- Track checkout request IDs
- Implement request deduplication on your end