Bonaventure OgetoBy Bonaventure Ogeto|

Transfers to M-PESA Wallets, Paybills and Tills

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.

The M-Pesa Transfer Landscape

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:

  • Personal wallets: Regular M-Pesa accounts tied to a phone number. This is how you pay individuals: drivers, freelancers, vendors, employees. The money lands directly in their M-Pesa wallet.
  • Paybills: Business accounts with a shortcode (e.g., 888880). Used by companies, schools, utilities, and other organizations. Paybills often use account numbers to route payments internally.
  • Tills: Merchant payment numbers (e.g., 5xxxxxx). Used at physical retail locations or by small businesses. Simpler than Paybills because they do not use account numbers.

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.

Transfers to M-Pesa Personal Wallets

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.

Transfers to Paybill Business Numbers

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.

Transfers to Till Numbers

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 and Validation

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-Specific Failure Modes

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.

M-Pesa vs Bank Transfers: When to Use Which

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:

  • The recipient is an individual (driver, freelancer, casual worker)
  • Speed matters (M-Pesa is near-instant, bank transfers can take hours)
  • The recipient does not have a bank account (common in Kenya)
  • Transfer amounts are moderate (M-Pesa has wallet limits)

Use bank transfers when:

  • The recipient is a business with a corporate bank account
  • Transfer amounts are large (above M-Pesa wallet limits)
  • The recipient prefers bank settlement for accounting purposes
  • You need Pesalink for real-time bank-to-bank transfers

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.

Build for the Kenyan Market

M-Pesa transfers are essential for any product serving the Kenyan market. Getting the details right (phone formatting, Paybill account numbers, failure handling) is what separates a working product from a frustrating one.

The McTaba bootcamp covers M-Pesa integration in depth. You build real payment flows for the Kenyan market.

Learn M-Pesa integration at McTaba

Key Takeaways

  • Paystack supports transfers to three M-Pesa destination types in Kenya: personal wallets, Paybill business accounts, and Till numbers.
  • M-Pesa wallet recipients use the phone number in international format: 254XXXXXXXXX (no leading zero, no plus sign).
  • M-Pesa wallet transfers are near-instant. The recipient gets the standard M-Pesa SMS notification within seconds.
  • Paybill transfers may require both a business number and an account number, since many Paybills route payments internally using account numbers.
  • Phone number formatting is the most common source of M-Pesa transfer failures. Validate and normalize to 254XXXXXXXXX before creating recipients.
  • M-Pesa has transaction limits that depend on the recipient account tier. Large transfers may fail if they exceed the recipient wallet limit.

Frequently Asked Questions

Can I send M-Pesa transfers to Airtel Money or T-Kash numbers?
Paystack M-Pesa transfers are specific to Safaricom M-Pesa. To send to Airtel Money or Telkom T-Kash, you would need to use a different integration or transfer to a bank account that the recipient can then withdraw from.
What is the maximum amount I can send to an M-Pesa wallet?
M-Pesa wallet limits are set by Safaricom and depend on the recipient account tier. These limits change periodically. Check Safaricom current M-Pesa tariff and limits for the latest figures. If your transfer exceeds the recipient wallet balance limit, it fails.
Do M-Pesa transfers work on weekends and holidays?
Yes. M-Pesa operates 24/7, including weekends and public holidays. This is one of the advantages of M-Pesa over bank transfers, which may be delayed during non-business hours.
How do I know if a phone number is registered for M-Pesa?
You cannot check M-Pesa registration status through the Paystack API. The best approach is to attempt the transfer and handle the failure if the number is unregistered. You can also ask the recipient to confirm their M-Pesa number during onboarding.

Ready to build real-world apps?

Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.

Apply to the McTaba Marathon