j-muiruri / daraja-php-sdk
Enterprise-grade PHP SDK for the Safaricom Daraja M-Pesa API. Full coverage: OAuth, STK Push, C2B, B2C, B2B, Transaction Status, Account Balance, Reversal, Dynamic QR, Tax Remittance, and Bill Manager. Includes a Laravel integration layer with auto-discovery, events, middleware, and webhook routing.
Requires
- php: ^8.2
- ext-json: *
- ext-openssl: *
- guzzlehttp/guzzle: ^7.8
Requires (Dev)
- laravel/framework: ^12.0
- mockery/mockery: ^1.6
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^11.0
- predis/predis: ^2.0
Suggests
- ext-redis: Required for RedisTokenCache — recommended for multi-process/multi-pod deployments
- laravel/framework: Required for the Laravel service provider, facade, events, middleware, and Artisan commands (^12.0)
- predis/predis: Alternative to ext-redis for RedisTokenCache
This package is auto-updated.
Last update: 2026-07-07 05:29:15 UTC
README
A modern, fully typed PHP 8.2+ SDK built for the Safaricom Daraja 3.0 M-Pesa API.
Index
- System Requirements
- Installation & Setup
- Integration Guides
- Initialization
- Feature Matrix
- API Reference
System Requirements
- PHP 8.2 or higher
- Composer dependency manager
- Extensions:
ext-openssl,ext-json - HTTP Client: Guzzle 7.x
Installation & Setup
Pull the package into your project via Composer:
composer require j-muiruri/daraja-php-sdk
Quick Start
1. Create the client
use Daraja\DarajaClient; use Daraja\Enums\Environment; $mpesa = DarajaClient::make( consumerKey: $_ENV['MPESA_CONSUMER_KEY'], consumerSecret: $_ENV['MPESA_CONSUMER_SECRET'], shortcode: $_ENV['MPESA_SHORTCODE'], passkey: $_ENV['MPESA_PASSKEY'], environment: Environment::Sandbox, callbackUrl: '[https://yourapp.co.ke/mpesa/callback](https://yourapp.co.ke/mpesa/callback)', resultUrl: '[https://yourapp.co.ke/mpesa/result](https://yourapp.co.ke/mpesa/result)', timeoutUrl: '[https://yourapp.co.ke/mpesa/timeout](https://yourapp.co.ke/mpesa/timeout)', );
Or read directly from environment variables:
$mpesa = DarajaClient::fromEnv();
Feature Matrix
| API Category | Interfacing Service | Exposed Method(s) |
|---|---|---|
| OAuth 2.0 | AccessTokenManager |
Auto-managed internally (transparent) |
| STK Push | stk() |
push(), pushBuyGoods(), query() |
| C2B | c2b() |
registerUrls(), simulate() |
| B2C | b2c() |
sendSalary(), sendBusinessPayment(), sendPromotion(), pay() |
| B2B | b2b() |
payBill(), buyGoods(), pay() |
| Transaction Status | transactionStatus() |
query() |
| Account Balance | accountBalance() |
query() |
| Reversal | reversal() |
reverse() |
| Dynamic QR | qr() |
generate(), extractImage(), saveImage() |
API Reference
STK Push (Lipa na M-Pesa Online)
Initiates a payment prompt on the customer's phone. The customer enters their M-Pesa PIN to confirm.
// Initiate an STK Push to a Paybill $response = $mpesa->stk()->push( phone: '0712345678', // or '+254712345678' or '254712345678' amount: 1500, // KES, minimum 1 accountReference: 'INV-0042', // Max 12 chars — shown to customer description: 'Order #42', // Max 13 chars callbackUrl: 'https://yourapp.co.ke/mpesa/callback', // override per request ); if ($response->isAccepted()) { $checkoutId = $response->checkoutRequestId(); // Store $checkoutId to poll status or match against the callback } // Buy Goods (till number) variant $response = $mpesa->stk()->pushBuyGoods( phone: '0712345678', amount: 250, till: '123456', callbackUrl: 'https://yourapp.co.ke/mpesa/callback', ); // Query status (when callback isn't received) $status = $mpesa->stk()->query($checkoutId); echo $status->resultDescription(); // "The service request is processed successfully."
STK Push Callback payload (POST to your callbackUrl):
{
"Body": {
"stkCallback": {
"MerchantRequestID": "29115-34620561-1",
"CheckoutRequestID": "ws_CO_191220191020363925",
"ResultCode": 0,
"ResultDesc": "The service request is processed successfully.",
"CallbackMetadata": {
"Item": [
{ "Name": "Amount", "Value": 1500 },
{ "Name": "MpesaReceiptNumber", "Value": "QHT3XXXXXXXXXXX" },
{ "Name": "PhoneNumber", "Value": 254712345678 }
]
}
}
}
}
C2B — Register URLs
Register validation and confirmation URLs before your customers start paying.
$mpesa->c2b()->registerUrls( confirmationUrl: 'https://yourapp.co.ke/mpesa/confirm', validationUrl: 'https://yourapp.co.ke/mpesa/validate', // optional responseType: 'Completed', // 'Completed' or 'Cancelled' ); // Sandbox only — simulate a payment $mpesa->c2b()->simulate( phone: '0712345678', amount: 500, billRefNumber: 'TEST001', commandId: 'CustomerPayBillOnline', );
⚠️ WARNING
B2C, B2B, Reversals, and Account Balance operations explicitly require you to supply an initiatorName and valid securityCredential string within your initialization configuration. Safely disburse outbound capital transfers from your operational utility wallets.
B2C — Disbursements
Send money to customers (salaries, promotions, refunds).
Requires
initiatorNameandsecurityCredentialin Config.
// Salary payment $mpesa->b2c()->sendSalary( phone: '0712345678', amount: 45000, remarks: 'April Salary', ); //Promotion/betting payout $mpesa->b2c()->sendPromotion( phone: '0733123456', amount: 500, remarks: 'Jackpot winnings', ); // General payment $mpesa->b2c()->sendBusinessPayment('0722123456', 1200, 'Refund - Order #112');
B2C Result callback payload (async, POST to resultUrl):
{
"Result": {
"ResultType": 0,
"ResultCode": 0,
"ResultDesc": "The service request is processed successfully.",
"OriginatorConversationID": "29112-34801843-1",
"ConversationID": "AG_20191219_00005797af5d7d75f652",
"TransactionID": "QHT3XXXXXXXXXXX"
}
}
B2B — Pay Suppliers
// Pay a supplier's paybill $mpesa->b2b()->payBill( receiverShortcode: '000001', amount: 75000, accountReference: 'SUPP-ACC-001', remarks: 'Invoice #INV-2025-03', ); // Pay a merchant till $mpesa->b2b()->buyGoods('987654', 12000, 'Office supplies');
Transaction Status
Reconcile transactions when callbacks were missed.
use Daraja\Enums\IdentifierType; $status = $mpesa->transactionStatus()->query( transactionId: 'QHT3XXXXXXXXXXX', // M-Pesa receipt number identifierType: IdentifierType::Shortcode, remarks: 'Reconciliation check', );
Account Balance
use Daraja\Enums\IdentifierType; $mpesa->accountBalance()->query( identifierType: IdentifierType::Shortcode, remarks: 'EOD balance check', ); // Result arrives asynchronously on your resultUrl
Transaction Reversal
$mpesa->reversal()->reverse( transactionId: 'QHT3XXXXXXXXXXX', amount: 1500, remarks: 'Customer cancellation', );
Dynamic QR Code
use Daraja\Enums\QRCodeType; $response = $mpesa->qr()->generate( merchantName: 'Asante Coffee', refNo: 'INV-001', amount: 350, type: QRCodeType::DynamicMerchant, size: 400, ); // Get the Base64 PNG to embed in an <img> tag $base64 = $mpesa->qr()->extractImage($response); echo '<img src="data:image/png;base64,' . $base64 . '">'; // Or save to disk $mpesa->qr()->saveImage($response, '/var/www/html/qr/payment.png');
Phone Number Formats
The PhoneNumber value object accepts any common Kenyan format:
use Daraja\ValueObjects\PhoneNumber; PhoneNumber::from('0712345678'); // → 254712345678 PhoneNumber::from('+254712345678'); // → 254712345678 PhoneNumber::from('254712345678'); // → 254712345678 PhoneNumber::from('712345678'); // → 254712345678
Invalid numbers throw Daraja\Exceptions\ValidationException.
Security Credentials (B2C, B2B, Reversal, Balance)
These APIs require the initiator password to be encrypted with Safaricom's public certificate.
Step 1 — Download the certificate from the Daraja portal:
- Sandbox:
https://developer.safaricom.co.ke/sites/default/files/cert/sandbox/cert.cer - Production:
https://developer.safaricom.co.ke/sites/default/files/cert/prod/cert.cer
Step 2 — Generate the credential once and store it:
use Daraja\Concerns\HasSecurityCredential; class CredentialGenerator { use HasSecurityCredential; public function generate(string $password, string $certPath): string { return $this->generateSecurityCredential($password, $certPath); } } $gen = new CredentialGenerator(); $credential = $gen->generate('MyInitiatorPassword', '/path/to/cert.cer'); // Store $credential in your .env as MPESA_SECURITY_CREDENTIAL
Error Handling
use Daraja\Exceptions\ApiException; use Daraja\Exceptions\AuthenticationException; use Daraja\Exceptions\ValidationException; try { $response = $mpesa->stk()->push(...); } catch (ValidationException $e) { // Bad parameters — check before hitting the API foreach ($e->errors() as $field => $message) { echo "{$field}: {$message}\n"; } } catch (AuthenticationException $e) { // OAuth token failure — check consumer key/secret logger()->error('M-Pesa auth failed', ['error' => $e->getMessage()]); } catch (ApiException $e) { // API returned an error response logger()->error('M-Pesa API error', [ 'status' => $e->statusCode(), 'code' => $e->errorCode(), 'msg' => $e->getMessage(), ]); }
Laravel Integration
// config/mpesa.php return [ 'consumer_key' => env('MPESA_CONSUMER_KEY'), 'consumer_secret' => env('MPESA_CONSUMER_SECRET'), 'shortcode' => env('MPESA_SHORTCODE'), 'passkey' => env('MPESA_PASSKEY'), 'environment' => env('MPESA_ENVIRONMENT', 'sandbox'), 'security_credential' => env('MPESA_SECURITY_CREDENTIAL'), 'initiator_name' => env('MPESA_INITIATOR_NAME'), 'callback_url' => env('MPESA_CALLBACK_URL'), 'result_url' => env('MPESA_RESULT_URL'), 'timeout_url' => env('MPESA_TIMEOUT_URL'), ]; // app/Providers/AppServiceProvider.php use Daraja\DarajaClient; use Daraja\Enums\Environment; $this->app->singleton(DarajaClient::class, function () { return DarajaClient::make( consumerKey: config('mpesa.consumer_key'), consumerSecret: config('mpesa.consumer_secret'), shortcode: config('mpesa.shortcode'), passkey: config('mpesa.passkey'), environment: Environment::from(config('mpesa.environment')), securityCredential: config('mpesa.security_credential'), initiatorName: config('mpesa.initiator_name'), callbackUrl: config('mpesa.callback_url'), resultUrl: config('mpesa.result_url'), timeoutUrl: config('mpesa.timeout_url'), ); });
Testing
composer test
Contributing
- Fork the repo and create a feature branch
- Write tests first — every new feature needs passing tests
- Run
composer testandcomposer analysebefore opening a PR - Follow PSR-12 coding style
License
MIT — see LICENSE.