gathuku/laravelmpesa
Integrate Safaricom M-Pesa into Laravel apps with a simple package for Daraja API payments, including STK Push, C2B, B2C, transaction status, and callback handling. Streamline mobile money workflows with clean, configurable PHP code.
composer require gathuku/laravelmpesa
php artisan vendor:publish --provider="Gathuku\Mpesa\MpesaServiceProvider"
config/mpesa.php. The package expects at minimum:
consumer_key / consumer_secret — from the Safaricom developer portalshortcode — Paybill or Till numberpasskey — Lipa na M-Pesa Online passkeycallback_url — public HTTPS URL for Daraja callbacksenv — sandbox or production.env rather than the config file so they aren't committed. Map them in config/mpesa.php via env('MPESA_CONSUMER_KEY'), etc.composer require --ignore-platform-reqs. If you're on a modern Laravel (9/10/11) running PHP 8.1+, this is now installable normally — but note the package's own composer.json constraint is what controls this, so confirm the installed version with composer show gathuku/laravelmpesa before debugging auth errors.vendor/gathuku/laravelmpesa/src/. The package is small, so the source is faster to trust than blog posts. The v1.2.2 release is a compatibility patch, not a feature release — the public method signatures are unchanged, so any snippet you find for older versions should still apply. Confirm anyway.Typical first call (signature from the package's facade/service class — confirm in the source):
use Gathuku\Mpesa\Mpesa;
$mpesa = new Mpesa();
$response = $mpesa->stkPush($amount, $phone, $reference, $description);
STK Push flow. Always split the request from the result:
reference (e.g. InvoiceNumber or order ID) — this is what appears on the callback and the customer's SMS.CheckoutRequestID and MerchantRequestID against your order.CheckoutRequestID.Callback handling. Expose a route that bypasses CSRF:
// routes/api.php
Route::post('/mpesa/callback', [MpesaCallbackController::class, 'stk'])
->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken::class]);
In the controller, parse $request->input('Body.stkCallback'), read ResultCode (0 = success), and update the matching order. Return 200 and a JSON body {"ResultCode":0,"ResultDesc":"Accepted"} — Safaricom will retry aggressively if you don't acknowledge.
Service injection. Rather than instantiating new Mpesa() everywhere, bind a wrapper service through the container and inject it. This makes it trivial to swap in a fake for tests and to centralise logging of every Daraja request/response.
Status queries as your safety net. Callbacks get lost (network, deployment, misconfigured URL). Wire up a TransactionStatus lookup (CheckoutRequestID for STK, TransactionID for C2B/B2C) into a scheduled job that reconciles any order still marked "pending" after N minutes. Treat the callback as a fast path and the query as the source of truth.
B2C disbursements follow the same request/callback shape but require a separate initiator_name and security_credential (generated locally per the Daraja spec — the package does not generate this for you). Store the ConversationID and match the B2C result callback on it.
Environment isolation. Keep sandbox and production credentials in separate keys (e.g. MPESA_SANDBOX_*, MPESA_PROD_*) and select via APP_ENV. The package's env config flag toggles the base URL, but credentials are not environment-scoped by default — you must do that yourself.
Testing. Wrap the package in a repository class and bind a fake in tests/. Since the package hits a real HTTP endpoint, no built-in fakes exist; you'll want Http::fake() at the boundary of your wrapper, or your own interface. Upgrading to v1.2.2 purely for PHP 8 support should not change any of your fakes — there are no reported API changes.
composer diff or check the GitHub compare view) — a "PHP 8 support" patch sometimes bundles type-coercion fixes or strlen(null) guards that change behaviour on edge inputs. Run your payment tests in sandbox against the new version before promoting to production.http://localhost will silently fail. Use ngrok/expose in dev and set the URL in config/mpesa.php — Safaricom does not accept a callback_url sent per-request if it differs from what's registered for the shortcode in some flows.YYYYMMDDHHmmss in the Timestamp field used to build the password (base64(shortcode + passkey + timestamp)). If your server's timezone drifts from Africa/Nairobi, you'll get auth errors. Set date_default_timezone_set('Africa/Nairobi') or build timestamps explicitly. This is a common failure after a PHP 8 upgrade if your php.ini timezone differs from the old host's.2547XXXXXXXX. The package does not normalise 0712... for you. Add a helper to strip leading 0, +, and spaces. PHP 8's stricter string/null handling can surface new TypeErrors if you pass an unvalidated null phone through to the client — validate at the boundary.ResultCode 1032 / 1037 mean the user cancelled or timed out. Your callback handler must handle these as "failed" and not leave the order pending — a very common source of "money taken but order not fulfilled" tickets.TransactionStatus before fulfilling a high-value order, and (c) matching strictly on CheckoutRequestID that you generated.mpesa_logs table with CheckoutRequestID, ResultCode, ResultDesc, amount, and raw JSON.MpesaClient with stkPush, queryStatus, b2c) implemented by a class that wraps the package. When the package inevitably goes stale, you swap one implementation and every call site is untouched. With the PHP 8 unblock, the pressure to fork has eased slightly, but the wrapper still pays for itself.safaricom/mpesa (community) or a direct Guzzle wrapper before committing. The package buys you ~200 lines of convenience; the maintenance risk remains, just no longer a hard PHP 8 blocker.composer.json and plan a fork or migration when the next major PHP or Laravel release lands.How can I help you explore Laravel packages today?