PHPackages                             felixmuhoro/laravel-mpesa-webhooks - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. felixmuhoro/laravel-mpesa-webhooks

ActiveLibrary[API Development](/categories/api)

felixmuhoro/laravel-mpesa-webhooks
==================================

Advanced M-Pesa webhook handling for Laravel — signature verification, idempotency, retry logic, logging, and a webhook dashboard.

v1.0.0(1mo ago)00MITPHPPHP ^8.1

Since Jun 4Pushed 1mo agoCompare

[ Source](https://github.com/felixmuhoro/laravel-mpesa-webhooks)[ Packagist](https://packagist.org/packages/felixmuhoro/laravel-mpesa-webhooks)[ Docs](https://github.com/felixmuhoro/laravel-mpesa-webhooks)[ RSS](/packages/felixmuhoro-laravel-mpesa-webhooks/feed)WikiDiscussions master Synced 1w ago

READMEChangelogDependencies (2)Versions (2)Used By (0)

laravel-mpesa-webhooks
======================

[](#laravel-mpesa-webhooks)

Advanced M-Pesa webhook handling for Laravel. Signature verification, IP allowlisting, idempotency, automatic retry, structured logging, and a built-in dashboard — all in one package.

Requires [`felixmuhoro/laravel-mpesa`](https://github.com/felixmuhoro/laravel-mpesa) for the underlying Daraja API client.

Requirements
------------

[](#requirements)

RequirementVersionPHP8.1+Laravel10 / 11 / 12 / 13felixmuhoro/laravel-mpesa^1.2Installation
------------

[](#installation)

```
composer require felixmuhoro/laravel-mpesa-webhooks
```

Publish the config and run the migration:

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

Configuration
-------------

[](#configuration)

```
# Disable during sandbox development, enable in production
MPESA_WEBHOOK_VERIFY_IP=true

# Comma-separated list overrides the default Safaricom production IPs
MPESA_WEBHOOK_IP_ALLOWLIST=196.201.214.200,196.201.214.206

# Only needed if you are proxying and signing callbacks yourself
MPESA_WEBHOOK_VERIFY_SIGNATURE=false
MPESA_WEBHOOK_SECRET=your-shared-secret

# Retry configuration
MPESA_WEBHOOK_MAX_ATTEMPTS=3
MPESA_WEBHOOK_BACKOFF_BASE=60

# Auto-prune processed logs older than N days (null = never)
MPESA_WEBHOOK_PRUNE_DAYS=90
```

Callback URLs
-------------

[](#callback-urls)

Register these URLs in your Daraja portal:

TypeURLSTK Push Result URL`https://your-domain.com/mpesa/webhook/stk`C2B Confirmation URL`https://your-domain.com/mpesa/webhook/c2b`B2C Result URL`https://your-domain.com/mpesa/webhook/b2c`Listening to Events
-------------------

[](#listening-to-events)

### STK Push

[](#stk-push)

```
use FelixMuhoro\MpesaWebhooks\Events\StkCallbackReceived;

class HandleStkCallback
{
    public function handle(StkCallbackReceived $event): void
    {
        if (! $event->wasSuccessful()) {
            $resultCode = $event->stkCallback['ResultCode'];
            $resultDesc = $event->stkCallback['ResultDesc'];
            return;
        }

        $amount     = $event->amount();
        $receipt    = $event->receiptNumber();
        $phone      = $event->phoneNumber();
        $checkoutId = $event->stkCallback['CheckoutRequestID'];

        Order::where('checkout_request_id', $checkoutId)->update([
            'status'        => 'paid',
            'mpesa_receipt' => $receipt,
            'paid_amount'   => $amount,
        ]);
    }
}
```

### C2B Confirmation

[](#c2b-confirmation)

```
use FelixMuhoro\MpesaWebhooks\Events\C2bConfirmationReceived;

class HandleC2bConfirmation
{
    public function handle(C2bConfirmationReceived $event): void
    {
        $transId   = $event->transactionId();
        $amount    = $event->amount();
        $phone     = $event->msisdn();
        $reference = $event->accountReference();
    }
}
```

### B2C Result

[](#b2c-result)

```
use FelixMuhoro\MpesaWebhooks\Events\B2cResultReceived;

class HandleB2cResult
{
    public function handle(B2cResultReceived $event): void
    {
        if (! $event->wasSuccessful()) {
            return;
        }

        $receipt      = $event->receiptNumber();
        $amount       = $event->amount();
        $originatorId = $event->originatorConversationId();
    }
}
```

### Generic Event

[](#generic-event)

`WebhookReceived` fires for every successfully verified and deduplicated webhook, regardless of type:

```
use FelixMuhoro\MpesaWebhooks\Events\WebhookReceived;

class AuditWebhooks
{
    public function handle(WebhookReceived $event): void
    {
        // $event->log         — WebhookLog model
        // $event->webhookType — 'stk_callback' | 'c2b_confirmation' | 'b2c_result' | 'unknown'
        // $event->payload
    }
}
```

Idempotency
-----------

[](#idempotency)

Safaricom resends the same callback if your endpoint does not respond with HTTP 200 quickly enough. The package deduplicates by idempotency key:

- **STK**: `CheckoutRequestID`
- **C2B**: `TransID`
- **B2C**: `OriginatorConversationID:TransactionID`

A duplicate webhook returns HTTP 200 immediately so Safaricom stops retrying, but does not re-fire events or re-process.

Retry Failed Webhooks
---------------------

[](#retry-failed-webhooks)

```
# Retry all retryable failures (respects back-off)
php artisan mpesa:retry-webhooks

# Retry a specific log entry
php artisan mpesa:retry-webhooks --id=42

# Filter by type
php artisan mpesa:retry-webhooks --type=stk_callback

# Override back-off and retry immediately
php artisan mpesa:retry-webhooks --force

# Limit batch size
php artisan mpesa:retry-webhooks --limit=10
```

Add to your scheduler for automatic recovery:

```
$schedule->command('mpesa:retry-webhooks')->everyFiveMinutes();
```

Dashboard
---------

[](#dashboard)

Visit `/mpesa/webhook/dashboard` (requires auth by default).

Shows all inbound webhooks with filtering by type and status, idempotency key, source IP, attempt count, and error messages.

```
// config/mpesa-webhooks.php
'dashboard' => [
    'enabled'    => true,
    'middleware' => ['web', 'auth:admin'],
    'per_page'   => 50,
],
```

WebhookLog Model
----------------

[](#webhooklog-model)

```
use FelixMuhoro\MpesaWebhooks\Models\WebhookLog;

WebhookLog::failed()->get();
WebhookLog::processed()->get();
WebhookLog::pending()->get();
WebhookLog::retryable()->get();
WebhookLog::byType('stk_callback')->latest()->get();
```

Security
--------

[](#security)

- IP allowlisting is enabled by default and checks against Safaricom's published egress IPs.
- Signature verification is opt-in for proxy setups.
- Rejected requests (wrong IP, bad signature, unparseable body) return HTTP 403.
- IP checks respect Laravel's `TrustProxies` configuration.

Testing
-------

[](#testing)

```
composer test
```

Uses in-memory SQLite and Orchestra Testbench — no external services required.

License
-------

[](#license)

MIT

###  Health Score

35

—

LowBetter than 77% of packages

Maintenance90

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity42

Maturing project, gaining track record

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

50d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/f4e43995a8ec026a33d25263c30f437674d6d8481092cb3acd64e96e4ba3f4e4?d=identicon)[felixmuhoro](/maintainers/felixmuhoro)

---

Tags

laravelmpesakenyasafaricom

### Embed Badge

![Health badge](/badges/felixmuhoro-laravel-mpesa-webhooks/health.svg)

```
[![Health](https://phpackages.com/badges/felixmuhoro-laravel-mpesa-webhooks/health.svg)](https://phpackages.com/packages/felixmuhoro-laravel-mpesa-webhooks)
```

###  Alternatives

[smodav/mpesa

M-Pesa API implementation

16467.9k1](/packages/smodav-mpesa)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
