rndwiga / mpesa-sdk
An expressive mpesa implementation
Requires
- php: ^8.1
- ext-curl: *
- ext-json: *
- ext-openssl: *
- ext-zlib: *
- monolog/monolog: ^3.9
- phpseclib/phpseclib: ^3.0
- psr/log: ^3.0
- ramsey/uuid: ^4.9
- vlucas/phpdotenv: ^5.6
Requires (Dev)
- mockery/mockery: ^1.6.0
- pestphp/pest: ^3.8.0
This package is auto-updated.
Last update: 2025-06-29 23:55:25 UTC
README
An opinionated fluent Safaricom M-Pesa SDK that provides a simple and elegant way to integrate with the Safaricom M-Pesa API.
Table of Contents
- Tech Stack
- Installation
- Requirements
- Configuration
- Features
- Usage
- Handling Callbacks
- Utilities
- Error Handling
- Custom Implementations
- Examples
- Testing
- Contributing
- License
Tech Stack
Server: Nginx, Apache, PHP 8.1+
Installation
Install the package via Composer:
composer require rndwiga/mpesa-sdk
Or clone the project:
git clone https://github.com/Rndwiga/mpesa-sdk.git
cd mpesa-sdk
composer update
Laravel Integration
This package supports Laravel integration out of the box. After installing the package, the service provider will be automatically registered thanks to Laravel's package auto-discovery feature.
If you're using Laravel < 5.5 or have disabled auto-discovery, add the service provider and facade to your config/app.php
file:
'providers' => [ // ... Rndwiga\Mpesa\MpesaServiceProvider::class, ], 'aliases' => [ // ... 'Mpesa' => Rndwiga\Mpesa\Facades\Mpesa::class, ],
Then publish the configuration file:
php artisan vendor:publish --tag=mpesa-config
Requirements
- PHP 8.1 or higher
- phpseclib/phpseclib 2.0 or higher
- psr/log 3.0 or higher (for logging)
- OpenSSL and JSON PHP extensions
Configuration
The SDK requires several environment variables to be set for proper operation:
# M-Pesa API Credentials
MPESA_CONSUMER_KEY=your_consumer_key
MPESA_CONSUMER_SECRET=your_consumer_secret
MPESA_INITIATOR_NAME=your_initiator_name
MPESA_INITIATOR_PASSWORD=your_initiator_password
# Business Shortcode
MPESA_SHORTCODE=your_shortcode
MPESA_BUSINESS_SHORTCODE=your_business_shortcode
# Express (STK Push) Settings
MPESA_PASSKEY=your_passkey
# Environment
MPESA_PRODUCTION=false
# Callback URLs
MPESA_CALLBACK_URL=https://your-domain.com/api/callback
MPESA_TIMEOUT_URL=https://your-domain.com/api/timeout
MPESA_RESULT_URL=https://your-domain.com/api/result
Laravel Configuration
When using the package with Laravel, you can publish the configuration file to customize the settings:
php artisan vendor:publish --tag=mpesa-config
This will create a config/mpesa.php
file in your Laravel application where you can configure all the M-Pesa settings. The configuration values will be automatically loaded from your .env
file using the prefixed environment variables (e.g., MPESA_CONSUMER_KEY
).
Example Laravel .env
configuration:
MPESA_CONSUMER_KEY=your_consumer_key
MPESA_CONSUMER_SECRET=your_consumer_secret
MPESA_PRODUCTION=false
MPESA_INITIATOR_NAME=your_initiator_name
MPESA_INITIATOR_PASSWORD=your_initiator_password
MPESA_SHORTCODE=your_shortcode
MPESA_BUSINESS_SHORTCODE=your_business_shortcode
MPESA_PASSKEY=your_passkey
MPESA_CALLBACK_URL=https://your-domain.com/api/callback
MPESA_TIMEOUT_URL=https://your-domain.com/api/timeout
MPESA_RESULT_URL=https://your-domain.com/api/result
Features
The SDK provides the following features:
API Services
- B2C (Business to Customer) - Send money from a business to customers
- B2B (Business to Business) - Send money from one business to another
- C2B (Customer to Business) - Receive money from customers
- Express (STK Push) - Prompt customers to enter their M-Pesa PIN on their phones
- Account Balance - Check account balance
- Transaction Status - Check the status of a transaction
- Transaction Reversal - Reverse a transaction
Additional Features
- PSR-3 Logging - Comprehensive logging of API requests and responses
- Token Caching - Efficient caching of access tokens to reduce API calls
- Enhanced Webhook Handling - Simplified processing of M-Pesa callbacks
- Custom Exceptions - Detailed exception handling for better error management
- Utilities - Logging, Storage, JSON Management, and Helper Functions
Usage
Initialization
Initialize the MpesaAPI with your credentials:
use Rndwiga\Mpesa\MpesaAPI; use Monolog\Logger; use Monolog\Handler\StreamHandler; use Rndwiga\Mpesa\Utils\FileCache; // Create a logger (optional) $logger = new Logger('mpesa'); $logger->pushHandler(new StreamHandler('path/to/mpesa.log', Logger::DEBUG)); // Create a cache (optional) $cache = new FileCache('path/to/cache'); // Initialize the Mpesa API $mpesa = new MpesaAPI( 'your_consumer_key', 'your_consumer_secret', false, // false for sandbox, true for production $logger, // optional $cache // optional );
Laravel Usage
When using the package with Laravel, you can access the Mpesa API through the facade:
use Rndwiga\Mpesa\Facades\Mpesa; // The Mpesa facade automatically uses the configuration from config/mpesa.php // No need to manually initialize the API // Example: Send money using B2C $response = Mpesa::b2c() ->setInitiatorName(config('mpesa.initiator_name')) ->setSecurityCredential(config('mpesa.initiator_password')) ->setCommandId('BusinessPayment') ->setAmount(100) ->setPartyA(config('mpesa.shortcode')) ->setPartyB('254712345678') ->setRemarks('Payment for services') ->setQueueTimeoutUrl(config('mpesa.timeout_url')) ->setResultUrl(config('mpesa.result_url')) ->setOccasion('Service payment') ->makePayment(); // Example: STK Push $response = Mpesa::express() ->setBusinessShortCode(config('mpesa.business_shortcode')) ->setPasskey(config('mpesa.passkey')) ->setTransactionType('CustomerPayBillOnline') ->setAmount(100) ->setPartyA('254712345678') ->setPartyB(config('mpesa.business_shortcode')) ->setPhoneNumber('254712345678') ->setCallbackUrl(config('mpesa.callback_url')) ->setAccountReference('Payment for order #123') ->setTransactionDesc('Payment for product/service') ->push();
You can also use dependency injection in your controllers:
use Rndwiga\Mpesa\MpesaAPI; class PaymentController extends Controller { protected $mpesa; public function __construct(MpesaAPI $mpesa) { $this->mpesa = $mpesa; } public function processPayment() { $response = $this->mpesa->express() ->setBusinessShortCode(config('mpesa.business_shortcode')) // ... other settings ->push(); // Process the response return response()->json($response); } }
B2C (Business to Customer)
Send money from a business to a customer:
$response = $mpesa->b2c() ->setInitiatorName('your_initiator_name') ->setSecurityCredential('your_initiator_password') ->setCommandId('BusinessPayment') // Options: SalaryPayment, BusinessPayment, PromotionPayment ->setAmount(100) // Amount to send ->setPartyA('your_shortcode') // Your shortcode ->setPartyB('254712345678') // Customer phone number ->setRemarks('Payment for services') // Transaction remarks ->setQueueTimeoutUrl('https://example.com/timeout') // Timeout URL ->setResultUrl('https://example.com/result') // Result URL ->setOccasion('Service payment') // Optional: Transaction occasion ->makePayment();
B2B (Business to Business)
Send money from one business to another:
$response = $mpesa->b2b() ->setInitiatorName('your_initiator_name') ->setSecurityCredential('your_initiator_password') ->setCommandId('BusinessPayBill') // Options: BusinessPayBill, MerchantToMerchantTransfer, etc. ->setAmount(100) // Amount to send ->setSenderIdentifierType(4) // 4 for organization shortcode ->setReceiverIdentifierType(4) // 4 for organization shortcode ->setPartyA('your_shortcode') // Your shortcode ->setPartyB('600000') // Receiver shortcode ->setAccountReference('Account reference') ->setRemarks('Payment for services') // Transaction remarks ->setQueueTimeoutUrl('https://example.com/timeout') // Timeout URL ->setResultUrl('https://example.com/result') // Result URL ->makePayment();
C2B (Customer to Business)
Register URLs for C2B transactions:
$response = $mpesa->c2b() ->setShortCode('your_shortcode') // Your shortcode ->setResponseType('Completed') // Options: Completed, Cancelled ->setConfirmationURL('https://example.com/confirmation') ->setValidationURL('https://example.com/validation') ->registerURLs();
Simulate a C2B transaction (for testing in sandbox):
$response = $mpesa->c2b() ->setShortCode('your_shortcode') // Your shortcode ->setCommandID('CustomerPayBillOnline') // Options: CustomerPayBillOnline, CustomerBuyGoodsOnline ->setAmount(100) // Amount to send ->setMsisdn('254712345678') // Customer phone number ->setBillRefNumber('REF123') // Reference number ->simulate();
Express (STK Push)
Prompt a customer to enter their M-Pesa PIN on their phone:
$response = $mpesa->express() ->setBusinessShortCode('your_business_shortcode') ->setPasskey('your_passkey') ->setTransactionType('CustomerPayBillOnline') ->setAmount(100) // Amount to charge ->setPartyA('254712345678') // Customer phone number ->setPartyB('your_business_shortcode') // Your shortcode ->setPhoneNumber('254712345678') // Customer phone number ->setCallbackUrl('https://example.com/callback') // Callback URL ->setAccountReference('Payment for order #123') // Reference ->setTransactionDesc('Payment for product/service') // Description ->push();
Query the status of an STK push:
$response = $mpesa->express() ->setBusinessShortCode('your_business_shortcode') ->setPasskey('your_passkey') ->query('ws_CO_DMZ_123456789_123456789');
Account Balance
Check your account balance:
$response = $mpesa->account() ->setInitiatorName('your_initiator_name') ->setSecurityCredential('your_initiator_password') ->setCommandId('AccountBalance') ->setPartyA('your_shortcode') // Your shortcode ->setIdentifierType(4) // 4 for organization shortcode ->setRemarks('Account balance query') ->setQueueTimeoutUrl('https://example.com/timeout') // Timeout URL ->setResultUrl('https://example.com/result') // Result URL ->queryBalance();
Transaction Status
Check the status of a transaction:
$response = $mpesa->account() ->setInitiatorName('your_initiator_name') ->setSecurityCredential('your_initiator_password') ->setCommandId('TransactionStatusQuery') ->setPartyA('your_shortcode') // Your shortcode ->setIdentifierType(4) // 4 for organization shortcode ->setTransactionID('LKXXXX1234') // The M-Pesa Transaction ID ->setRemarks('Transaction status query') ->setQueueTimeoutUrl('https://example.com/timeout') // Timeout URL ->setResultUrl('https://example.com/result') // Result URL ->queryTransactionStatus();
Transaction Reversal
Reverse a transaction:
$response = $mpesa->account() ->setInitiatorName('your_initiator_name') ->setSecurityCredential('your_initiator_password') ->setTransactionId('LKXXXX1234') ->setAmount(100) // Amount to reverse ->setReceiverParty('receiver_shortcode') ->setReceiverIdentifierType(Rndwiga\Mpesa\Api\Account::IDENTIFIER_TYPE_SHORTCODE) ->setRemarks('Test reversal') ->setQueueTimeoutUrl('https://example.com/timeout') ->setResultUrl('https://example.com/result') ->setOccasion('Test occasion') ->reverseTransaction();
Handling Callbacks
Using the WebhookHandler (Recommended)
Process callbacks from M-Pesa:
// Get the callback data $callbackData = file_get_contents('php://input'); // Create a webhook handler $webhook = $mpesa->webhook($callbackData); // Check the callback type if ($webhook->isCallbackType('express')) { // Handle Express (STK Push) callback $resultCode = $webhook->getValue('Body.stkCallback.ResultCode'); $resultDesc = $webhook->getValue('Body.stkCallback.ResultDesc'); if ($resultCode == 0) { // Transaction was successful $amount = $webhook->getValueFromItems('Amount'); $receiptNumber = $webhook->getValueFromItems('MpesaReceiptNumber'); $transactionDate = $webhook->getValueFromItems('TransactionDate'); $phoneNumber = $webhook->getValueFromItems('PhoneNumber'); // Process the payment // ... } // Send a success response back to Mpesa echo $webhook->generateSuccessResponse('Callback processed successfully'); } elseif ($webhook->isCallbackType('b2c')) { // Handle B2C callback // ... } elseif ($webhook->isCallbackType('c2b')) { // Handle C2B callback // ... }
Alternative Method
// Create a webhook handler and capture the callback data $webhook = $mpesa->webhook(); $webhook->captureCallback()->parseCallback(); // Check the type of callback if ($webhook->isCallbackType('c2b')) { // Process C2B callback $transactionId = $webhook->getValue('TransID'); $amount = $webhook->getValue('TransAmount'); $phoneNumber = $webhook->getValue('MSISDN'); // Do something with the data... } elseif ($webhook->isCallbackType('express')) { // Process STK Push callback $resultCode = $webhook->getValue('Body.stkCallback.ResultCode'); $resultDesc = $webhook->getValue('Body.stkCallback.ResultDesc'); // Get nested values using dot notation $amount = $webhook->getValue('Body.stkCallback.CallbackMetadata.Item.0.Value'); $mpesaReceiptNumber = $webhook->getValue('Body.stkCallback.CallbackMetadata.Item.1.Value'); // Do something with the data... } // Send a success response back to Mpesa echo $webhook->generateSuccessResponse('Transaction processed successfully'); // Or send an error response if needed // echo $webhook->generateErrorResponse('Transaction failed', '1');
Legacy Method
// For STK Push callbacks $callbackData = json_decode(file_get_contents('php://input'), true); $processedCallback = $mpesa->express()->processCallback($callbackData); // For other callbacks (B2C, B2B, Account Balance, etc.) $callbackData = json_decode(file_get_contents('php://input'), true); $processedCallback = $mpesa->account()->processCallback($callbackData); // Send a success response back to Mpesa echo $mpesa->finishTransaction();
Utilities
Logging
The SDK includes multiple logging options:
Using PSR-3 Compatible Logger
use Monolog\Logger; use Monolog\Handler\StreamHandler; $logger = new Logger('mpesa'); $logger->pushHandler(new StreamHandler('path/to/mpesa.log', Logger::DEBUG)); // Use the logger with the MpesaAPI $mpesa = new MpesaAPI( 'your_consumer_key', 'your_consumer_secret', false, $logger ); // The logger will automatically log API requests and responses
Using the Built-in Logger
use Rndwiga\Mpesa\Utils\Logger; // Create a logger with folder and file name $logger = new Logger('mpesa_logs', 'transactions'); // Log data at different levels $logger->logDebugData(['transaction_id' => '123456', 'status' => 'pending']); $logger->logInfoData(['message' => 'Payment received', 'amount' => 1000]); $logger->logWarningData(['message' => 'Retry attempt', 'attempt' => 3]); $logger->logErrorData(['message' => 'Transaction failed', 'reason' => 'Timeout']); // Or use PSR-3 methods $logger->debug('Debug message', ['context' => 'value']); $logger->info('Info message', ['context' => 'value']); $logger->warning('Warning message', ['context' => 'value']); $logger->error('Error message', ['context' => 'value']); // Get the log file path $logFilePath = $logger->getLogFile('log'); // Returns path to transactions.log
Storage
The SDK includes multiple storage options:
Using File Cache
use Rndwiga\Mpesa\Utils\FileCache; $cache = new FileCache('path/to/cache'); // Use the cache with the MpesaAPI $mpesa = new MpesaAPI( 'your_consumer_key', 'your_consumer_secret', false, null, $cache ); // The cache will automatically store access tokens
Using the Storage Class
use Rndwiga\Mpesa\Utils\Storage; // Create a storage instance $storage = new Storage('mpesa_data'); $storage->setLogFolder('transactions'); // Create a storage path $path = $storage->createStorage(); // Get the full storage path $fullPath = $storage->storagePath($path); // Compress a file $compressedFile = Storage::gzCompressFile('path/to/file.txt', 9, true); // Generate a random ID $randomId = Storage::generateRandomId();
JSON Management
The SDK includes utilities for working with JSON data:
Using WebhookHandler
use Rndwiga\Mpesa\Utils\WebhookHandler; // Parse JSON data $webhook = new WebhookHandler(); $webhook->captureCallback($jsonData); $webhook->parseCallback(); // Extract values using dot notation $value = $webhook->getValue('path.to.value'); // Generate JSON responses $response = $webhook->generateSuccessResponse('Success message');
Using JsonManager
use Rndwiga\Mpesa\Utils\JsonManager; // Save data to a JSON file $result = JsonManager::saveToFile('data.json', 'transactions', ['id' => 123, 'amount' => 1000]); // Save data to a specific file path $result = JsonManager::saveToFilePath('/path/to/file.json', ['id' => 123, 'amount' => 1000]); // Read a JSON file $data = JsonManager::readJsonFile('/path/to/file.json', true); // Add data to an existing JSON file $result = JsonManager::addDataToJsonFile('/path/to/file.json', ['id' => 456, 'amount' => 2000]); // Validate JSON data $validation = JsonManager::validateJsonData('{"id": 123, "amount": 1000}'); if ($validation['status'] === 'success') { // JSON is valid $data = $validation['response']; }
Helper Functions
The SDK includes helper functions for common tasks:
use function Rndwiga\Mpesa\Utils\mpesa_env; use function Rndwiga\Mpesa\Utils\mpesa_str_slug; // Get an environment variable $apiKey = mpesa_env('MPESA_API_KEY'); // Convert a string to a URL-friendly slug $slug = mpesa_str_slug('Hello World'); // Returns 'hello-world'
When using the package with Laravel, these helper functions are designed to work seamlessly with Laravel's built-in functions. The mpesa_env()
function will use Laravel's env()
function if available, and mpesa_str_slug()
will use Laravel's Str::slug()
if available.
Error Handling
The SDK provides detailed error handling for API responses:
try { $response = $mpesa->express()->push(); if (isset($response['success']) && $response['success']) { // Request was successful $checkoutRequestId = $response['data']->CheckoutRequestID; // ... } else { // Request failed $errorMessage = $response['errorMessage'] ?? 'Unknown error'; // Handle the error } } catch (\Exception $e) { // Handle exceptions $errorMessage = $e->getMessage(); // ... }
The package also provides custom exception classes for better error handling:
use Rndwiga\Mpesa\Exceptions\MpesaException; use Rndwiga\Mpesa\Exceptions\AuthenticationException; use Rndwiga\Mpesa\Exceptions\ValidationException; use Rndwiga\Mpesa\Exceptions\ApiException; try { // Make an API request $response = $mpesa->b2c() ->setInitiatorName('your_initiator_name') ->setSecurityCredential('your_initiator_password') ->setCommandId(Rndwiga\Mpesa\Api\B2C::COMMAND_ID_BUSINESS_PAYMENT) ->setAmount(100) ->setPartyA('your_shortcode') ->setPartyB('254722000000') ->setRemarks('Test payment') ->setQueueTimeoutUrl('https://example.com/timeout') ->setResultUrl('https://example.com/result') ->setOccasion('Test occasion') ->makePayment(); // Check if the request was successful if (!$response['success']) { // Handle API error echo "API Error: " . $response['errorMessage']; } } catch (AuthenticationException $e) { // Handle authentication errors (e.g., invalid credentials) echo "Authentication Error: " . $e->getMessage(); } catch (ValidationException $e) { // Handle validation errors (e.g., invalid phone number) echo "Validation Error: " . $e->getMessage(); } catch (ApiException $e) { // Handle API-specific errors echo "API Error: " . $e->getMessage(); echo "API Error Code: " . $e->getApiErrorCode(); echo "Request ID: " . $e->getRequestId(); // Get additional error data if available $errorData = $e->getErrorData(); if ($errorData) { // Process error data } } catch (MpesaException $e) { // Handle other Mpesa-related errors echo "Mpesa Error: " . $e->getMessage(); } catch (\Exception $e) { // Handle other unexpected errors echo "Error: " . $e->getMessage(); }
Custom Implementations
Custom Cache Implementation
You can create your own cache implementation by implementing the CacheInterface
:
use Rndwiga\Mpesa\Utils\CacheInterface; class RedisCache implements CacheInterface { private $redis; public function __construct($redisClient) { $this->redis = $redisClient; } public function get(string $key, $default = null) { $value = $this->redis->get($key); return $value !== false ? json_decode($value, true) : $default; } public function set(string $key, $value, ?int $ttl = null): bool { return $this->redis->setex($key, $ttl ?? 3600, json_encode($value)); } public function has(string $key): bool { return $this->redis->exists($key); } public function delete(string $key): bool { return $this->redis->del($key) > 0; } } // Use your custom cache implementation $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $cache = new RedisCache($redis); $mpesa = new MpesaAPI( 'your_consumer_key', 'your_consumer_secret', false, $logger, $cache );
Examples
Check the examples
directory for more detailed examples:
b2c_example.php
: Example of B2C paymentb2b_example.php
: Example of B2B paymentc2b_example.php
: Example of C2B registration and simulationexpress_example.php
: Example of STK Push and callback processingaccount_balance_example.php
: Example of checking account balancetransaction_status_example.php
: Example of checking transaction statustransaction_reversal_example.php
: Example of reversing a transactionwebhook_example.php
: Example of using the WebhookHandler to process callbacks
All examples demonstrate the use of logging, caching, and webhook handling features.
Testing
The package includes comprehensive tests for all components using PestPHP, a delightful testing framework for PHP:
composer test
You can also run the tests directly:
./vendor/bin/pest
Or run specific test files:
./vendor/bin/pest tests/Client/MpesaClientTest.php ./vendor/bin/pest tests/Utils/WebhookHandlerTest.php
PestPHP provides a more expressive and elegant syntax for writing tests. For more information, visit pestphp.com.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
This project is licensed under the MIT License - see the LICENSE file for details.