Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Mpesa Laravel Package

itsmurumba/laravel-mpesa

A Laravel package for integrating Safaricom M-Pesa into your applications. It handles STK Push, C2B and B2C payments, transaction status queries, and callback processing behind a clean, configurable API so you can accept and send mobile money in minutes.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish config

composer require itsmurumba/laravel-mpesa
php artisan vendor:publish --tag=mpesa-config   # or --provider="Itsmurumba\Mpesa\MpesaServiceProvider"

The package auto-registers its service provider and Mpesa facade on Laravel 5.5+. On older versions, add the provider and 'Mpesa' => Itsmurumba\Mpesa\Facades\Mpesa::class to config/app.php.

Where to look first

  1. config/mpesa.php — this is the source of truth for env variable names, endpoint selection, and per-service credentials.
  2. The Daraja developer portal — you need live keys even for sandbox; the package only wraps HTTP calls.
  3. The package README’s Examples/ or usage snippets — the method signatures mirror Daraja payloads closely, so the README doubles as the mapping table.

Set credentials in .env

MPESA_ENV=sandbox
MPESA_CONSUMER_KEY=your_key
MPESA_CONSUMER_SECRET=your_secret
MPESA_SHORTCODE=174379
MPESA_PASSKEY=bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919
MPESA_CALLBACK_URL=https://your-app.test/api/mpesa/callback
# B2C / reversals / balance only:
MPESA_INITIATOR_NAME=testapi
MPESA_INITIATOR_PASSWORD=Safaricom999!*!

Then always run php artisan config:clear after editing .env on cached environments.

First use case — STK Push (Lipa na M-Pesa)

use Itsmurumba\Mpesa\Facades\Mpesa;

$response = Mpesa::stkPush([
    'amount'           => 1,
    'phone'            => '254712345678',
    'accountReference' => 'INV-1001',
    'transactionDesc'  => 'Order payment',
]);

// Persist $response['CheckoutRequestID'] against your order immediately.

Register a callback route that accepts the Daraja POST body:

Route::post('/api/mpesa/callback', [MpesaCallbackController::class, 'handle'])
    ->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken::class]);

The callback payload is { "Body": { "stkCallback": { ... } } } — treat that envelope as fixed and normalize early.

Implementation Patterns

Wrap the facade in a domain service, not controllers. Keep Mpesa:: calls in something like App\Services\MpesaService so you can swap envs, mock in tests, and centralize logging of raw responses. Controllers stay thin: validate → dispatch → persist request ID → return.

Store the request identifiers before returning to the user. Every initiate call returns MerchantRequestID and CheckoutRequestID. Persist both plus your internal order ID in a payments.mpesa_transactions table. The callback only carries CheckoutRequestID, so your matching key must exist before the provider responds.

Callback handling pattern — verify, match, idempotent update, respond 200 fast. Daraja retries on non-200, so:

public function handle(Request $request)
{
    $callback = $request->input('Body.stkCallback');
    $tx = MpesaTransaction::where('checkout_request_id', $callback['CheckoutRequestID'])->firstOrFail();

    if ($tx->status !== 'pending') {
        return response()->json(['ResultCode' => 0, 'ResultDesc' => 'Accepted']);
    }

    // Dispatch a job; return immediately.
    ProcessMpesaCallback::dispatch($tx, $callback);

    return response()->json(['ResultCode' => 0, 'ResultDesc' => 'Accepted']);
}

Normalize the callback into your own status enum. Daraja’s ResultCode (0, 1032, 1037, 1) maps to success / user-cancelled / timeout / insufficient funds. Don’t let raw codes leak into your domain models. Extract Amount, MpesaReceiptNumber, PhoneNumber, and TransactionDate from CallbackMetadata.Item[] — the array is unordered, so index by Name, not position.

Queue the reconciliation jobs. If a callback never arrives (user cancels, network), schedule a TransactionStatus query after ~2 minutes and again after ~30. The package exposes transactionStatus, accountBalance, and reversal — use them from a scheduled command, not in the request cycle.

Separate flows by audience. STK Push covers customer-to-business online checkout. Register C2B URLs once (registerUrls) for paybill/till payments; handle the validation and confirmation endpoints similarly. Use B2C for payouts — those need InitiatorName/SecurityCredential and are typically the flow that breaks in production because developers forgot the initiator password is the encrypted credential, not the plain password.

Multi-tenant / multi-shortcode. Don’t rely on config() alone if you serve multiple paybills. Either bind a resolved config object per request, or fork config at runtime (config(['mpesa.shortcode' => $tenant->shortcode])) inside your service before each call. Keep the passkey tied to the shortcode, not global.

Testing. Fake the facade in feature tests:

Mpesa::shouldReceive('stkPush')->once()->andReturn(['CheckoutRequestID' => 'ws_CO_123']);

Integration tests against the sandbox are the only way to validate callback shape — write one happy-path and one cancelled-user test with the sandbox test numbers (254708374149 etc.) documented in the Daraja portal.

Observability. Log the full request and response (redacting SecurityCredential and consumer secret) into a mpesa_api_logs table or a dedicated log channel. Every production incident with M-Pesa is answered by “what did Daraja actually return,” and it is not recoverable retroactively.

Gotchas and Tips

  • Callback URL must be public HTTPS with no query string. Daraja will silently fail to deliver otherwise. Local dev needs ngrok/expose; a localhost URL in .env is the most common silent failure.
  • Amounts are integers. STK Push rejects decimals. Round and validate before calling, not after.
  • Phone format is strict: 2547XXXXXXXX or 2541XXXXXXXX. 07... and +254... fail.
  • config:cache breaks env reading. If credentials change, run php artisan config:clear (and optimize:clear locally). Stale cached tokens cause 400.002.02 errors that look like auth issues.
  • Access tokens are cached by the package but expire in 3600s. If you build a long-running worker, don’t cache the token yourself beyond that window; call through the facade each time.
  • Sandbox shortcode/passkey are public constants: 174379 and the well-known passkey. Don’t commit them as “secrets” and then assume production will work — production uses your real shortcode and passkey from the Daraja portal.
  • ResultCode 1032 means cancelled by user — do not retry. 1037 is timeout — retry once. 0 is success. Treat anything else as a soft failure and reconcile via transactionStatus.
  • Reversals and B2C are time-boxed and permission-gated. You need the initiator role granted in the portal; a 404 from /mpesa/b2c/v1/paymentrequest usually means the product isn’t enabled, not a code bug.
  • CallbackMetadata is absent on failures. Guard $callback['CallbackMetadata']['Item'] — accessing it on a cancelled transaction throws.
  • Timezone: Daraja timestamps are East Africa Time. Use Carbon::parse($date, 'Africa/Nairobi') before converting for display or persistence.
  • Idempotency on your side. Network errors after the request leaves your app are ambiguous; persist an initiating state before the HTTP call and check CheckoutRequestID on retries. Otherwise a user double-tapping “Pay” produces two STK prompts.
  • CSRF: callback routes must be excluded from VerifyCsrfToken. Forget this and Daraja gets a 419, which it interprets as a permanent failure and stops retrying.
  • Respond to callbacks with { "ResultCode": 0, "ResultDesc": "Accepted" } — 200 with a different body is fine for STK, but C2B confirmation expects that shape.
  • Extension points are thin. The package is largely a thin HTTP wrapper — there are no events or middleware to hook. Use a Laravel service wrapper for extensibility rather than trying to subclass the facade.
  • Version alignment: the package advertises Laravel 5–12; if you’re on PHP 8.3+ with Laravel 11/12, confirm composer resolved a recent tag rather than a stale one, since Daraja endpoint URLs changed over time.
  • Debugging tip: call Mpesa::accountBalance() in a tinker session against sandbox first. If auth works there, any subsequent failure is a payload problem, not a credential problem — halves your debugging surface.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus