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

Laravelmpesa Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require gathuku/laravelmpesa
php artisan vendor:publish --provider="Gathuku\Mpesa\MpesaServiceProvider"
  1. Publish the config and populate the file at config/mpesa.php. The package expects at minimum:
    • consumer_key / consumer_secret — from the Safaricom developer portal
    • shortcode — Paybill or Till number
    • passkey — Lipa na M-Pesa Online passkey
    • callback_url — public HTTPS URL for Daraja callbacks
    • env — sandbox or production
  2. Add the credentials to .env rather than the config file so they aren't committed. Map them in config/mpesa.php via env('MPESA_CONSUMER_KEY'), etc.
  3. Check your PHP version. As of v1.2.2 the package declares PHP 8 support (contributed by @jwambugu). Before this release you were stuck on PHP 7.x or forced to 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.
  4. Verify the actual methods by reading 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.
  5. First use case: STK Push (Lipa na M-Pesa Online). This is the 90% use case — a customer enters a phone number, you push a prompt, they enter their PIN, and Safaricom posts the result to your callback. Start here before touching B2C.

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);

Implementation Patterns

STK Push flow. Always split the request from the result:

  1. Generate a unique reference (e.g. InvoiceNumber or order ID) — this is what appears on the callback and the customer's SMS.
  2. Call the STK push method and store CheckoutRequestID and MerchantRequestID against your order.
  3. Return "check your phone" to the UI immediately — do not block the request waiting for the callback.
  4. Handle the callback on a dedicated route (see below) and match on 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.

Gotchas and Tips

  • Upgrading to v1.2.2 is low-risk but not zero-risk. The release is a single contribution adding PHP 8 support. Read the diff before deploying (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.
  • Don't assume PHP 8 support means "modern". v1.2.2 unblocks installation; it does not refresh the Daraja API surface. The package still predates several Daraja changes — verify payloads against the official docs as before.
  • Callback must be HTTPS and publicly reachable. 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.
  • Timestamp format is fragile. Daraja requires 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.
  • Phone number format. Daraja expects 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.
  • The OAuth token expires every hour. The package requests a fresh token per call — fine for low volume, wasteful under load. If you're doing anything resembling throughput, cache the token yourself and override the auth step via a decorator around the package.
  • 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.
  • No built-in signature verification. Daraja does not sign callbacks; anyone who knows your callback URL can POST a fake success. Mitigate by (a) using a hard-to-guess path, (b) always re-querying Daraja for TransactionStatus before fulfilling a high-value order, and (c) matching strictly on CheckoutRequestID that you generated.
  • Log everything. Wrap the package's calls and log the raw request and response. When a payment is disputed, the Daraja response body is the only evidence you'll have. Consider a mpesa_logs table with CheckoutRequestID, ResultCode, ResultDesc, amount, and raw JSON.
  • Extension point — replace the client. The cleanest escape hatch is to write a thin interface (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.
  • Still evaluate alternatives for greenfield. v1.2.2 is a maintenance patch, not a revival — the API coverage is unchanged from 2021. For new projects, compare against 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.
  • Watch for PHP 9 / Laravel 12 breakage. PHP 8 support today doesn't imply ongoing maintenance. Pin the version in composer.json and plan a fork or migration when the next major PHP or Laravel release lands.
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.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle