leefipay / mpesa
Official Laravel package for integrating applications with the LeefiPay M-Pesa Open API.
Requires
- php: ^8.1
- guzzlehttp/guzzle: ^7.5
- illuminate/contracts: ^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/events: ^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/http: ^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/routing: ^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^9.0|^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^7.0|^8.0|^9.0|^10.0|^11.0
- phpunit/phpunit: ^9.6|^10.5|^11.0|^12.0
README
Official Laravel package for integrating applications with the LeefiPay M-Pesa Open API.
API base: https://leefipay.com/api/v1/ · Docs: https://leefipay.com/developers
This package talks to LeefiPay’s public REST API (/api/v1) using Sanctum Bearer tokens. It does not call Safaricom Daraja directly — LeefiPay handles Daraja on your behalf.
Requirements
| Laravel | PHP | Status |
|---|---|---|
| 9.x | 8.1+ | Supported |
| 10.x | 8.1+ | Supported |
| 11.x | 8.2+ | Supported |
| 12.x | 8.2+ | Supported |
| 13.x | 8.3+ | Supported |
Compatibility is enforced by GitHub Actions: package PHPUnit tests (HTTP-faked) plus install into a fresh Laravel application for each matrix entry. Mark rows as Tested only after the Actions workflow is green on main.
Installation
composer require leefipay/mpesa
Publish config (optional):
php artisan vendor:publish --tag=leefipay-config
Configuration
LEEFIPAY_BASE_URL=https://leefipay.com LEEFIPAY_API_KEY=your-sanctum-secret-bearer-token LEEFIPAY_ENVIRONMENT=sandbox LEEFIPAY_TIMEOUT=30 LEEFIPAY_CONNECT_TIMEOUT=10 # Outbound webhooks (LeefiPay → your app) LEEFIPAY_WEBHOOKS_ENABLED=true LEEFIPAY_WEBHOOK_PATH=leefipay/webhooks/mpesa LEEFIPAY_WEBHOOK_SECRET=whsec_...
Create an API credential in the LeefiPay dashboard (API & Integrations or Developer console). Use the secret token as LEEFIPAY_API_KEY. The public key (lp_pk_…) is not used for request authentication.
LEEFIPAY_ENVIRONMENT is informational (sandbox vs production host is controlled by LEEFIPAY_BASE_URL).
Basic usage
use LeefiPay\Mpesa\Facades\LeefiPay; $response = LeefiPay::stkPush([ 'phone' => '254700000000', 'amount' => 100, 'payment_channel_id' => 1, 'reference' => 'ORDER-1001', // sent as account_reference 'notes' => 'Payment for Order 1001', 'idempotency_key' => 'order-1001', ]); if ($response->success()) { $response->reference(); $response->waitingUrl(); $response->checkoutRequestId(); }
Dependency injection:
use LeefiPay\Mpesa\Contracts\LeefiPayClientInterface; public function __construct(private LeefiPayClientInterface $leefiPay) {}
STK Push
Required fields (Open API):
| Field | Notes |
|---|---|
amount |
Integer KES ≥ 1 |
phone |
Customer MSISDN |
payment_channel_id |
Till / Paybill channel from LeefiPay |
Optional: customer_name, notes, idempotency_key / Idempotency-Key, account_reference (alias reference), mode (stk|manual|auto).
Transaction status & verify
$status = LeefiPay::paymentStatus('PAY-12'); $verified = LeefiPay::verifyTransaction([ 'reference' => 'PAY-12', // or 'uuid' / 'checkout_request_id' ]);
Payment channels
$channels = LeefiPay::paymentChannels();
Connection test
LeefiPay::testConnection(); LeefiPay::me(); LeefiPay::health(); // unauthenticated
Webhooks (LeefiPay → your app)
Register a webhook URL in the LeefiPay dashboard pointing to:
https://your-app.test/leefipay/webhooks/mpesa
Set LEEFIPAY_WEBHOOK_SECRET to the whsec_… secret.
The package verifies:
X-LeefiPay-Signature = HMAC-SHA256(timestamp + "." + rawBody, secret)
X-LeefiPay-Timestamp
X-LeefiPay-Event
Dispatched events:
LeefiPay\Mpesa\Events\WebhookReceivedLeefiPay\Mpesa\Events\PaymentSuccessful(payment.successful)LeefiPay\Mpesa\Events\PaymentFailed(payment.failed)LeefiPay\Mpesa\Events\InvoicePaid(invoice.paid)
use LeefiPay\Mpesa\Events\PaymentSuccessful; Event::listen(PaymentSuccessful::class, function ($event) { $event->webhookEvent->paymentReference(); });
Error handling
use LeefiPay\Mpesa\Exceptions\ApiException; use LeefiPay\Mpesa\Exceptions\AuthenticationException; use LeefiPay\Mpesa\Exceptions\ValidationException; use LeefiPay\Mpesa\Exceptions\RateLimitException; try { LeefiPay::stkPush([...]); } catch (ValidationException $e) { $e->errors(); } catch (AuthenticationException $e) { // Invalid / missing Bearer token } catch (RateLimitException $e) { // HTTP 429 } catch (ApiException $e) { $e->statusCode(); $e->errorCode(); // e.g. subscription_expired $e->response(); }
Credentials are never included in exception messages or package logs.
Retries
- GET requests may retry on connection failures when
LEEFIPAY_RETRY_ENABLED=true. - Payment POSTs (STK, cash, manual, retry) are never auto-retried. Use
idempotency_keyand retry yourself if needed.
Not currently supported
These are not exposed on the LeefiPay public Open API and are therefore not implemented:
- B2C
- C2B URL registration
- Account balance
- Transaction reversal
- Direct Safaricom Daraja OAuth / passkey usage
Security
- Store tokens and webhook secrets in environment variables only.
- Prefer HTTPS for
LEEFIPAY_BASE_URLand webhook URLs. - Rotate compromised credentials in the LeefiPay dashboard.
See SECURITY.md.
Testing
composer install
composer validate --strict
composer test
CI also installs the package into a temporary Laravel app for each supported major version (package discovery, config publish, container + facade resolution). No real LeefiPay / M-Pesa API calls are made.
Versioning
Semantic Versioning. Current development line: 0.1.x. Do not treat this as v1.0.0 until the full Laravel matrix is green in CI.
License
MIT — see LICENSE.