To send a Paystack transfer to an M-Pesa wallet, create a mobile money transfer recipient using the phone number in international format (254XXXXXXXXX). For Paybills, use the Paybill number as the account identifier with an optional account number. For Tills, use the Till number. Each type follows the standard transfer flow: create recipient, initiate transfer, and track via webhooks.
In Kenya, M-Pesa is not just a mobile wallet. It is the financial backbone of the country. Over 90% of Kenyan adults use it. When you build a payout system for the Kenyan market, M-Pesa support is not optional.
Paystack supports transfers to three types of M-Pesa destinations:
Each destination type has slightly different recipient creation requirements, but the transfer initiation process is the same across all three. For the overall transfer lifecycle, see how Paystack transfers work end to end.
To send money to someone's M-Pesa wallet, create a mobile money recipient with their phone number in international format.
const createMpesaRecipient = async (name, phoneNumber) => {
// Normalize phone number to 254XXXXXXXXX
var normalized = normalizeKenyanPhone(phoneNumber);
var response = await fetch('https://api.paystack.co/transferrecipient', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'mobile_money',
name: name,
account_number: normalized,
bank_code: 'MPESA',
currency: 'KES',
}),
});
var data = await response.json();
if (!data.status) {
throw new Error('M-Pesa recipient creation failed: ' + data.message);
}
return data.data;
};
// Phone number normalization
function normalizeKenyanPhone(phone) {
// Remove spaces, dashes, and plus signs
var cleaned = phone.replace(/[s-+]/g, '');
// Convert 07xx to 2547xx
if (cleaned.indexOf('07') === 0 || cleaned.indexOf('01') === 0) {
return '254' + cleaned.substring(1);
}
// Already in 254 format
if (cleaned.indexOf('254') === 0 && cleaned.length === 12) {
return cleaned;
}
throw new Error('Invalid Kenyan phone number: ' + phone);
}
Once you have the recipient, initiate the transfer exactly as you would for a bank transfer:
var transfer = await fetch('https://api.paystack.co/transfer', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
source: 'balance',
amount: 500000, // 5,000 KES in cents
recipient: recipientCode,
reason: 'Driver payout - Trip #4521',
reference: 'payout_driver_42_trip4521',
}),
});
M-Pesa wallet transfers are near-instant. The recipient sees the money and gets the standard M-Pesa SMS notification within seconds of the transfer being processed.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
Paybills are M-Pesa business accounts identified by a shortcode (e.g., 888880). Many organizations use Paybills with account numbers to route payments internally. For example, a school's Paybill might use student IDs as account numbers.
const createPaybillRecipient = async (name, paybillNumber, accountNumber) => {
var response = await fetch('https://api.paystack.co/transferrecipient', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'mobile_money',
name: name,
account_number: paybillNumber,
bank_code: 'MPESA',
currency: 'KES',
metadata: {
account_number: accountNumber, // Internal routing number
},
}),
});
var data = await response.json();
return data.data;
};
Note: The exact field mapping for Paybill account numbers may vary depending on how Paystack implements Paybill transfers in Kenya. Check the current Paystack documentation for Kenya-specific recipient creation parameters, as the API evolves. The important thing is to capture both the Paybill number and the account number, because many Paybills reject payments without an account number.
Till numbers (also called Buy Goods numbers) are used by merchants for payments. They are simpler than Paybills because they do not use account numbers. The payment goes directly to the merchant identified by the Till number.
const createTillRecipient = async (merchantName, tillNumber) => {
var response = await fetch('https://api.paystack.co/transferrecipient', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'mobile_money',
name: merchantName,
account_number: tillNumber,
bank_code: 'MPESA',
currency: 'KES',
}),
});
var data = await response.json();
return data.data;
};
Till transfers are useful when your platform needs to pay suppliers or service providers who operate physical businesses and only accept M-Pesa via Till number. The settlement is similar to wallet transfers in terms of speed.
Phone number formatting is the single most common cause of M-Pesa transfer failures. Users enter numbers in all sorts of formats: 0712345678, +254712345678, 254 712 345 678, 07-1234-5678. Your system needs to normalize all of these to 254XXXXXXXXX.
function validateAndNormalizePhone(input) {
if (!input || typeof input !== 'string') {
return { valid: false, error: 'Phone number is required' };
}
// Remove all non-digit characters
var digits = input.replace(/D/g, '');
// Handle different input formats
if (digits.length === 10 && (digits.indexOf('07') === 0 || digits.indexOf('01') === 0)) {
// Local format: 0712345678 => 254712345678
digits = '254' + digits.substring(1);
} else if (digits.length === 9 && (digits.indexOf('7') === 0 || digits.indexOf('1') === 0)) {
// Without leading zero: 712345678 => 254712345678
digits = '254' + digits;
} else if (digits.length === 12 && digits.indexOf('254') === 0) {
// Already international: 254712345678
// Keep as is
} else {
return {
valid: false,
error: 'Phone number must be a valid Kenyan number (e.g., 0712345678 or 254712345678)',
};
}
// Validate it starts with a known Safaricom prefix
var safaricomPrefixes = ['2547', '2541'];
var hasValidPrefix = false;
for (var i = 0; i < safaricomPrefixes.length; i++) {
if (digits.indexOf(safaricomPrefixes[i]) === 0) {
hasValidPrefix = true;
break;
}
}
if (!hasValidPrefix) {
return { valid: false, error: 'Number does not appear to be a Safaricom M-Pesa number' };
}
return { valid: true, normalized: digits };
}
Run this validation before creating the recipient and again before displaying the number to the admin for confirmation. Show the normalized number to the user: "We will send the payout to 254712345678. Is this correct?" This catches transposition errors that pass format validation but send money to the wrong person.
M-Pesa transfers fail for reasons beyond the standard bank transfer failures.
Unregistered number
The phone number is not registered for M-Pesa. This happens when someone gives you their Airtel or Telkom number instead of their Safaricom number. The transfer fails because M-Pesa only works with Safaricom numbers.
Wallet limit exceeded
M-Pesa accounts have transaction and balance limits based on the account tier. If your transfer would push the recipient over their wallet limit, it fails. You cannot know the recipient's current balance or tier, so you cannot predict this failure. It requires the recipient to withdraw or upgrade their account.
Subscriber suspended
The M-Pesa account has been suspended by Safaricom due to fraud concerns, SIM replacement in progress, or other compliance reasons. The transfer fails, and the recipient needs to resolve the issue with Safaricom.
System timeout
Safaricom's M-Pesa system is occasionally slow or unavailable. This is a transient failure. Retry after 15-30 minutes.
For all M-Pesa failures, check the failure reason in the transfer.failed webhook. Transient failures (system timeout) can be retried. Permanent failures (unregistered number, suspended account) require recipient action. For a complete failure handling framework, see managing transfer failures and reversals.
In Kenya, you often have a choice: pay to M-Pesa or pay to a bank account. The right choice depends on the use case.
Use M-Pesa wallet transfers when:
Use bank transfers when:
Many platforms offer recipients the choice. During onboarding, ask: "Do you want to be paid via M-Pesa or bank transfer?" Create the appropriate recipient type and store the preference. Some recipients want M-Pesa for daily payouts (drivers) and bank for monthly settlements (restaurants). Support both.
For bank-specific transfers in Kenya, see transfers to Kenyan bank accounts via Paystack. For instant bank transfers, see Pesalink transfers through Paystack.
KES 2,999
The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.
Start Paystack Course