felixmuhoro/laravel-mpesa

Modern, fully-typed M-Pesa Daraja 2.0 integration for Laravel 10 / 11 / 12. STK Push, C2B, B2C, callbacks, events, and an exhaustive result-code dictionary — battle-tested in production.

Maintainers

Package info

github.com/felixmuhoro/laravel-mpesa

pkg:composer/felixmuhoro/laravel-mpesa

Statistics

Installs: 7

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.2.0 2026-04-18 03:20 UTC

This package is auto-updated.

Last update: 2026-04-18 03:21:51 UTC


README

Latest Version on Packagist Total Downloads License

A modern, fully-typed M-Pesa Daraja 2.0 integration for Laravel 10 / 11 / 12 / 13. Battle-tested in production against real customer traffic — including the undocumented error codes Safaricom's own docs don't mention.

Why this package

Most M-Pesa Laravel packages on Packagist were built for Laravel 7/8 and return raw arrays. This one is different:

  • Laravel 10 / 11 / 12 / 13 first-class — PHP 8.1+ enums, readonly DTOs, typed properties
  • Exhaustive result-code dictionary — 15+ Safaricom codes mapped including the undocumented 4999 (still processing, NOT failed)
  • Correct async handling — STK query correctly distinguishes "payment pending" from "payment failed" so you never mark a successful payment as failed because you polled too early
  • Events-drivenPaymentSuccessful, PaymentFailed, StkPushInitiated dispatched on every terminal state
  • Secure callbacks — IP allow-listing (Safaricom's 12 production IPs preloaded) + optional query-string shared-secret middleware
  • HTTP Faking friendly — uses Laravel's Illuminate\Http\Client\Factory, so tests never hit real Daraja

Installation

composer require felixmuhoro/laravel-mpesa

Publish the config:

php artisan vendor:publish --tag=mpesa-config
php artisan vendor:publish --tag=mpesa-migrations
php artisan migrate

Add credentials to .env:

MPESA_ENVIRONMENT=sandbox               # or "production"
MPESA_CONSUMER_KEY=your-consumer-key
MPESA_CONSUMER_SECRET=your-consumer-secret

# STK Push
MPESA_STK_SHORT_CODE=174379
MPESA_STK_PASSKEY=bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919
MPESA_STK_CALLBACK_URL=https://yourapp.com/mpesa/callback/stk

# Optional: callback shared secret
MPESA_CALLBACK_SECRET_KEY=some-long-random-string

Get sandbox credentials free at developer.safaricom.co.ke.

Usage

1. STK Push (Lipa Na M-Pesa Online)

use FelixMuhoro\Mpesa\Facades\Mpesa;

$response = Mpesa::stkPush(
    phone: '0712345678',
    amount: 100,
    reference: 'ORDER-1234',
    description: 'Payment for order 1234'
);

if ($response->accepted()) {
    // Save $response->checkoutRequestId so you can match the callback later
    session(['mpesa_checkout' => $response->checkoutRequestId]);
}

Phone numbers are accepted in any Kenyan format — 0712..., 712..., 254712..., +254 712 345 678 — all normalise to Safaricom's required 2547XXXXXXXX.

2. STK Query (check payment status)

$result = Mpesa::stkQuery($checkoutRequestId);

if ($result->isCompleted()) {
    // Mark order paid
} elseif ($result->isPending()) {
    // Retry in a few seconds — the customer hasn't acted yet
} elseif ($result->isFailed()) {
    // Customer cancelled / wrong PIN / etc. — $result->message has details
}

3. Callbacks — handle via events

The package ships routes at /mpesa/callback/stk, /mpesa/callback/c2b/confirm, etc., already protected by IP allow-listing + optional shared-secret middleware.

Your job is to listen for events:

// app/Providers/EventServiceProvider.php
use FelixMuhoro\Mpesa\Events\PaymentSuccessful;
use FelixMuhoro\Mpesa\Events\PaymentFailed;

protected $listen = [
    PaymentSuccessful::class => [MarkOrderPaid::class],
    PaymentFailed::class     => [NotifyCustomerOfFailure::class],
];
// app/Listeners/MarkOrderPaid.php
public function handle(PaymentSuccessful $event): void
{
    Order::where('checkout_request_id', $event->payload->checkoutRequestId)
        ->update([
            'status'        => 'paid',
            'mpesa_receipt' => $event->payload->mpesaReceiptNumber,
            'paid_amount'   => $event->payload->amount,
            'paid_at'       => now(),
        ]);
}

4. C2B — receive paybill / till payments

Register your confirmation + validation URLs once:

Mpesa::c2bRegisterUrls(
    confirmationUrl: route('mpesa.callback.c2b.confirm'),
    validationUrl:   route('mpesa.callback.c2b.validate'),
);

Listen for the same events (the C2B confirmation controller also dispatches PaymentSuccessful).

In sandbox you can simulate an inbound payment:

Mpesa::c2bSimulate('0712345678', 50, 'BILL-99');

5. B2C — send money to customers

Mpesa::b2cSend(
    phone: '0712345678',
    amount: 500,
    commandId: 'BusinessPayment',   // or SalaryPayment / PromotionPayment
    remarks: 'Referral bonus',
);

6. Account balance, status queries, reversals

Mpesa::accountBalance();
Mpesa::transactionStatus('LKXXXX1234');
Mpesa::reverse('LKXXXX1234', 100, 'Wrong recipient');

Handling result codes

Any time you receive a result code from Safaricom you can normalise it:

use FelixMuhoro\Mpesa\Enums\ResultCode;

ResultCode::isCompleted('0');        // true
ResultCode::isFailed('1032');        // true — customer cancelled
ResultCode::isPending('4999');       // true — undocumented "still processing"
ResultCode::isPending('random-code');// true — unknown codes are treated as pending

ResultCode::resolve('1');
// ['status' => 'failed', 'message' => 'Insufficient M-Pesa balance...', 'code' => '1']

Callback security

Production callbacks are protected out of the box:

  • IP allow-listing — Safaricom's 12 production callback IPs are preloaded in config. Set MPESA_CALLBACK_ALLOWED_IPS="" to disable (NOT recommended in production).
  • Shared secret — set MPESA_CALLBACK_SECRET_KEY=... and include ?key=... in the callback URL you register with Safaricom.

Both are layered — requests that fail either check throw InvalidCallbackException.

Testing

The package ships PHPUnit tests that mock Daraja responses using Http::fake():

composer install
composer test

Supported Laravel / PHP versions

Package PHP Laravel
1.x 8.1 – 8.4 10, 11, 12, 13

Credits

License

MIT — see LICENSE.