├── .github ├── FUNDING.yml └── dependabot.yml ├── .styleci.yml ├── examples ├── config.php.example └── c2b_transaction.php ├── src ├── Response │ ├── TransactionResponse.php │ ├── CustomerNameResponse.php │ ├── TransactionStatusResponse.php │ ├── ReversalResponse.php │ └── BaseResponse.php ├── config │ └── mpesa.php ├── Exceptions │ ├── AuthenticationException.php │ ├── MpesaException.php │ ├── ValidationException.php │ └── ApiException.php ├── Laravel │ ├── ServiceProvider.php │ └── Mpesa.php ├── Auth │ └── TokenManager.php ├── Constants │ ├── ResponseCodes.php │ └── TransactionStatus.php ├── Validation │ └── ParameterValidator.php └── Mpesa.php ├── phpunit.xml ├── LICENSE.md ├── composer.json ├── CONTRIBUTING.md ├── .phpunit.result.cache ├── CHANGELOG.md ├── API.md └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: karson 4 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel 2 | 3 | disabled: 4 | - single_class_element_per_statement 5 | -------------------------------------------------------------------------------- /examples/config.php.example: -------------------------------------------------------------------------------- 1 | env('MPESA_API_KEY'), 7 | 8 | 'public_key' => env('MPESA_PUBLIC_KEY'), 9 | 10 | 'is_test' => env('MPESA_TEST', true), 11 | 'service_provider_code' => env('MPESA_SERVICE_PROVIDER_CODE', '171717'), 12 | 'is_async' => env('MPESA_IS_ASYNC', false), 13 | 14 | ]; 15 | -------------------------------------------------------------------------------- /src/Exceptions/AuthenticationException.php: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | tests/Unit 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Exceptions/MpesaException.php: -------------------------------------------------------------------------------- 1 | context = $context; 15 | } 16 | 17 | /** 18 | * Get additional context information 19 | */ 20 | public function getContext(): array 21 | { 22 | return $this->context; 23 | } 24 | 25 | /** 26 | * Set additional context information 27 | */ 28 | public function setContext(array $context): self 29 | { 30 | $this->context = $context; 31 | return $this; 32 | } 33 | 34 | /** 35 | * Add context information 36 | */ 37 | public function addContext(string $key, mixed $value): self 38 | { 39 | $this->context[$key] = $value; 40 | return $this; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Karson Adam 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/Exceptions/ValidationException.php: -------------------------------------------------------------------------------- 1 | errors = $errors; 12 | 13 | if (empty($message) && !empty($errors)) { 14 | $message = "Validation failed: " . implode(', ', $errors); 15 | } 16 | 17 | parent::__construct($message, $code, null, ['errors' => $errors]); 18 | } 19 | 20 | /** 21 | * Get validation errors 22 | */ 23 | public function getErrors(): array 24 | { 25 | return $this->errors; 26 | } 27 | 28 | /** 29 | * Get first validation error 30 | */ 31 | public function getFirstError(): ?string 32 | { 33 | return $this->errors[0] ?? null; 34 | } 35 | 36 | /** 37 | * Check if has specific error 38 | */ 39 | public function hasError(string $error): bool 40 | { 41 | return in_array($error, $this->errors); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Laravel/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 21 | __DIR__.'/../config/mpesa.php'=> config_path('mpesa.php'), 22 | ]); 23 | } 24 | 25 | /** 26 | * Register the application services. 27 | * 28 | * @return void 29 | */ 30 | public function register() 31 | { 32 | $this->mergeConfigFrom( 33 | __DIR__.'/../config/mpesa.php', 34 | 'mpesa' 35 | ); 36 | 37 | $this->app->bind(Mpesa::class, function ($app) { 38 | $mpesa = new Mpesa( 39 | $app['config']['mpesa.public_key'], 40 | $app['config']['mpesa.api_key'], 41 | $app['config']['mpesa.is_test'], 42 | $app['config']['mpesa.service_provider_code'] 43 | ); 44 | return $mpesa; 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Auth/TokenManager.php: -------------------------------------------------------------------------------- 1 | publicKey = $publicKey; 14 | $this->apiKey = $apiKey; 15 | } 16 | 17 | /** 18 | * Get current token, generating a new one if needed 19 | */ 20 | public function getToken(): string 21 | { 22 | if ($this->token === null) { 23 | $this->generateToken(); 24 | } 25 | 26 | return $this->token; 27 | } 28 | 29 | /** 30 | * Generate a new authentication token 31 | */ 32 | private function generateToken(): void 33 | { 34 | $key = "-----BEGIN PUBLIC KEY-----\n"; 35 | $key .= wordwrap($this->publicKey, 60, "\n", true); 36 | $key .= "\n-----END PUBLIC KEY-----"; 37 | 38 | $encrypted = ''; 39 | openssl_public_encrypt($this->apiKey, $encrypted, $key, OPENSSL_PKCS1_PADDING); 40 | 41 | $this->token = base64_encode($encrypted); 42 | } 43 | 44 | /** 45 | * Clear stored token 46 | */ 47 | public function clearToken(): void 48 | { 49 | $this->token = null; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Exceptions/ApiException.php: -------------------------------------------------------------------------------- 1 | responseCode = $responseCode; 18 | $this->responseDescription = $responseDescription; 19 | 20 | $context = []; 21 | if ($responseCode) { 22 | $context['response_code'] = $responseCode; 23 | } 24 | if ($responseDescription) { 25 | $context['response_description'] = $responseDescription; 26 | } 27 | 28 | parent::__construct($message, $code, $previous, $context); 29 | } 30 | 31 | /** 32 | * Get API response code 33 | */ 34 | public function getResponseCode(): ?string 35 | { 36 | return $this->responseCode; 37 | } 38 | 39 | /** 40 | * Get API response description 41 | */ 42 | public function getResponseDescription(): ?string 43 | { 44 | return $this->responseDescription; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/c2b_transaction.php: -------------------------------------------------------------------------------- 1 | c2b( 21 | transactionReference: 'C2BTXN001' . time(), 22 | customerMSISDN: '258841234567', 23 | amount: 100, 24 | thirdPartReference: 'REF001' 25 | ); 26 | 27 | if ($syncResponse->isTransactionSuccessful()) { 28 | echo "✓ Transaction successful!\n"; 29 | echo "Transaction ID: " . $syncResponse->getTransactionId() . "\n"; 30 | echo "Conversation ID: " . $syncResponse->getConversationId() . "\n"; 31 | echo "Response Code: " . $syncResponse->getResponseCode() . "\n"; 32 | } else { 33 | echo "✗ Transaction failed\n"; 34 | if (!$syncResponse->isSuccessful()) { 35 | echo "HTTP Error: " . $syncResponse->getStatusCode() . "\n"; 36 | echo "code: " . $syncResponse->getResponseCode() . "\n"; 37 | echo "description: " . $syncResponse->getResponseDescription() . "\n"; 38 | } 39 | if (!$syncResponse->isApiSuccess()) { 40 | echo "API Error: " . $syncResponse->getApiResponseDescription() . "\n"; 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "karson/mpesa-php-sdk", 3 | "description": "", 4 | "keywords": [ 5 | "karson", 6 | "mpesa-sdk", 7 | "mpesa-api", 8 | "mpesa", 9 | "payment-gateway", 10 | "vodacom-mozambique", 11 | "payment-gateway" 12 | ], 13 | "homepage": "https://github.com/karson/mpesa-php-sdk", 14 | "license": "MIT", 15 | "type": "library", 16 | "minimum-stability": "dev", 17 | "prefer-stable": true, 18 | "authors": [ 19 | { 20 | "name": "Karson Adam", 21 | "email": "karson@turbohost.co.mz", 22 | "role": "Developer" 23 | } 24 | ], 25 | "require": { 26 | "php": ">=8.1", 27 | "ext-curl": "*", 28 | "ext-json": "*", 29 | "guzzlehttp/guzzle": "^7.0", 30 | "illuminate/support": "*" 31 | }, 32 | "require-dev": { 33 | "phpunit/phpunit": "^9.3.3" 34 | }, 35 | "autoload": { 36 | "psr-4": { 37 | "Karson\\MpesaPhpSdk\\": "src" 38 | } 39 | }, 40 | "autoload-dev": { 41 | "psr-4": { 42 | "Karson\\MpesaPhpSdk\\Tests\\": "tests" 43 | } 44 | }, 45 | "scripts": { 46 | "test": "vendor/bin/phpunit", 47 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage" 48 | 49 | }, 50 | "config": { 51 | "sort-packages": true 52 | }, 53 | "extra": { 54 | "laravel": { 55 | "providers": [ 56 | "Karson\\MpesaPhpSdk\\Laravel\\ServiceProvider" 57 | ], 58 | "aliases": { 59 | "Mpesa": "Karson\\MpesaPhpSdk\\Laravel\\Mpesa" 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Laravel/Mpesa.php: -------------------------------------------------------------------------------- 1 | 'Request processed successfully', 46 | self::INTERNAL_ERROR => 'Internal Error', 47 | self::INSUFFICIENT_BALANCE => 'Not enough balance', 48 | self::TRANSACTION_FAILED => 'Transaction failed', 49 | self::TRANSACTION_EXPIRED => 'Transaction expired', 50 | self::TRANSACTION_NOT_PERMITTED => 'Transaction not permitted to sender', 51 | self::REQUEST_TIMEOUT => 'Request timeout', 52 | self::DUPLICATE_TRANSACTION => 'Duplicate transaction', 53 | default => 'Unknown response code' 54 | }; 55 | } 56 | 57 | /** 58 | * Check if HTTP status code indicates success 59 | */ 60 | public static function isHttpSuccess(int $statusCode): bool 61 | { 62 | return $statusCode >= 200 && $statusCode < 300; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Response/CustomerNameResponse.php: -------------------------------------------------------------------------------- 1 | parseCustomerNameResponse(); 19 | } 20 | 21 | private function parseCustomerNameResponse(): void 22 | { 23 | $data = is_object($this->response) ? $this->response : json_decode($this->response); 24 | 25 | if ($data) { 26 | $this->output_CustomerMSISDN = $data->output_CustomerMSISDN ?? null; 27 | $this->output_FirstName = $data->output_FirstName ?? null; 28 | $this->output_SecondName = $data->output_SecondName ?? null; 29 | $this->output_ResponseCode = $data->output_ResponseCode ?? null; 30 | $this->output_ResponseDesc = $data->output_ResponseDesc ?? null; 31 | } 32 | } 33 | 34 | public function getCustomerMSISDN(): ?string 35 | { 36 | return $this->output_CustomerMSISDN; 37 | } 38 | 39 | public function getFirstName(): ?string 40 | { 41 | return $this->output_FirstName; 42 | } 43 | 44 | public function getSecondName(): ?string 45 | { 46 | return $this->output_SecondName; 47 | } 48 | 49 | public function getLastName(): ?string 50 | { 51 | return $this->output_SecondName; 52 | } 53 | 54 | public function getCustomerName(): ?string 55 | { 56 | $firstName = $this->output_FirstName ?? ''; 57 | $secondName = $this->output_SecondName ?? ''; 58 | 59 | return trim($firstName . ' ' . $secondName) ?: null; 60 | } 61 | 62 | public function getResponseCode(): ?string 63 | { 64 | return $this->output_ResponseCode; 65 | } 66 | 67 | public function getResponseDescription(): ?string 68 | { 69 | return $this->output_ResponseDesc; 70 | } 71 | 72 | public function isCustomerFound(): bool 73 | { 74 | return $this->isSuccessful() && !empty($this->output_FirstName); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Constants/TransactionStatus.php: -------------------------------------------------------------------------------- 1 | parseStatusResponse(); 20 | } 21 | 22 | private function parseStatusResponse(): void 23 | { 24 | $data = is_object($this->response) ? $this->response : json_decode($this->response); 25 | 26 | if ($data) { 27 | $this->output_ConversationID = $data->output_ConversationID ?? null; 28 | $this->output_ResponseTransactionStatus = $data->output_ResponseTransactionStatus ?? null; 29 | $this->output_ResponseCode = $data->output_ResponseCode ?? null; 30 | $this->output_ResponseDesc = $data->output_ResponseDesc ?? null; 31 | $this->output_ThirdPartyReference = $data->output_ThirdPartyReference ?? null; 32 | } 33 | } 34 | 35 | public function getConversationId(): ?string 36 | { 37 | return $this->output_ConversationID; 38 | } 39 | 40 | public function getTransactionStatus(): ?string 41 | { 42 | return $this->output_ResponseTransactionStatus; 43 | } 44 | 45 | public function getResponseCode(): ?string 46 | { 47 | return $this->output_ResponseCode; 48 | } 49 | 50 | public function getResponseDescription(): ?string 51 | { 52 | return $this->output_ResponseDesc; 53 | } 54 | 55 | public function getThirdPartyReference(): ?string 56 | { 57 | return $this->output_ThirdPartyReference; 58 | } 59 | 60 | public function isTransactionCompleted(): bool 61 | { 62 | return TransactionStatus::isCompleted($this->output_ResponseTransactionStatus ?? ''); 63 | } 64 | 65 | public function isTransactionPending(): bool 66 | { 67 | return TransactionStatus::isPending($this->output_ResponseTransactionStatus ?? ''); 68 | } 69 | 70 | public function isTransactionFailed(): bool 71 | { 72 | return TransactionStatus::isFailed($this->output_ResponseTransactionStatus ?? ''); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Response/ReversalResponse.php: -------------------------------------------------------------------------------- 1 | parseReversalResponse(); 21 | } 22 | 23 | private function parseReversalResponse(): void 24 | { 25 | $data = is_object($this->response) ? $this->response : json_decode($this->response); 26 | 27 | if ($data) { 28 | $this->output_TransactionID = $data->output_TransactionID ?? null; 29 | $this->output_ConversationID = $data->output_ConversationID ?? null; 30 | $this->output_OriginatorConversationID = $data->output_OriginatorConversationID ?? null; 31 | $this->output_ResponseCode = $data->output_ResponseCode ?? null; 32 | $this->output_ResponseDesc = $data->output_ResponseDesc ?? null; 33 | $this->output_ReversalTransactionID = $data->output_ReversalTransactionID ?? null; 34 | $this->output_ReversalAmount = isset($data->output_ReversalAmount) ? (float)$data->output_ReversalAmount : null; 35 | } 36 | } 37 | 38 | public function getTransactionId(): ?string 39 | { 40 | return $this->output_TransactionID; 41 | } 42 | 43 | public function getConversationId(): ?string 44 | { 45 | return $this->output_ConversationID; 46 | } 47 | 48 | public function getOriginatorConversationId(): ?string 49 | { 50 | return $this->output_OriginatorConversationID; 51 | } 52 | 53 | public function getResponseCode(): ?string 54 | { 55 | return $this->output_ResponseCode; 56 | } 57 | 58 | public function getResponseDescription(): ?string 59 | { 60 | return $this->output_ResponseDesc; 61 | } 62 | 63 | public function getReversalTransactionId(): ?string 64 | { 65 | return $this->output_ReversalTransactionID; 66 | } 67 | 68 | public function getReversalAmount(): ?float 69 | { 70 | return $this->output_ReversalAmount; 71 | } 72 | 73 | public function isReversalSuccessful(): bool 74 | { 75 | return $this->output_ResponseCode === 'INS-0' && !empty($this->output_ReversalTransactionID); 76 | } 77 | 78 | public function isPartialReversal(): bool 79 | { 80 | return $this->output_ReversalAmount !== null; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | Please read and understand the contribution guide before creating an issue or pull request. 6 | 7 | ## Etiquette 8 | 9 | This project is open source, and as such, the maintainers give their free time to build and maintain the source code 10 | held within. They make the code freely available in the hope that it will be of use to other developers. It would be 11 | extremely unfair for them to suffer abuse or anger for their hard work. 12 | 13 | Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the 14 | world that developers are civilized and selfless people. 15 | 16 | It's the duty of the maintainer to ensure that all submissions to the project are of sufficient 17 | quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. 18 | 19 | ## Viability 20 | 21 | When requesting or submitting new features, first consider whether it might be useful to others. Open 22 | source projects are used by many developers, who may have entirely different needs to your own. Think about 23 | whether or not your feature is likely to be used by other users of the project. 24 | 25 | ## Procedure 26 | 27 | Before filing an issue: 28 | 29 | - Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. 30 | - Check to make sure your feature suggestion isn't already present within the project. 31 | - Check the pull requests tab to ensure that the bug doesn't have a fix in progress. 32 | - Check the pull requests tab to ensure that the feature isn't already in progress. 33 | 34 | Before submitting a pull request: 35 | 36 | - Check the codebase to ensure that your feature doesn't already exist. 37 | - Check the pull requests to ensure that another person hasn't already submitted the feature or fix. 38 | 39 | ## Requirements 40 | 41 | If the project maintainer has any additional requirements, you will find them listed here. 42 | 43 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer). 44 | 45 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 46 | 47 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 48 | 49 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. 50 | 51 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 52 | 53 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 54 | 55 | **Happy coding**! 56 | -------------------------------------------------------------------------------- /.phpunit.result.cache: -------------------------------------------------------------------------------- 1 | {"version":1,"defects":{"Tests\\Unit\\Auth\\TokenManagerTest::testGetTokenGeneratesNewToken":4,"Tests\\Unit\\Auth\\TokenManagerTest::testClearTokenResetsState":4,"Tests\\Unit\\Auth\\TokenManagerTest::testRefreshTokenGeneratesNewToken":4,"Tests\\Unit\\Auth\\TokenManagerTest::testTokenIsReusedWhenAlreadyGenerated":4,"Tests\\Unit\\Validation\\ParameterValidatorTest::testValidateMSISDNReturnsTrueForValidNumbers":3,"Tests\\Unit\\Validation\\ParameterValidatorTest::testValidateMSISDNReturnsFalseForInvalidNumbers":3,"Tests\\Unit\\Validation\\ParameterValidatorTest::testValidateC2BParametersReturnsEmptyArrayForValidParams":3},"times":{"Tests\\Unit\\Auth\\TokenManagerTest::testGetTokenGeneratesNewToken":0.035,"Tests\\Unit\\Auth\\TokenManagerTest::testClearTokenResetsState":0.039,"Tests\\Unit\\Auth\\TokenManagerTest::testRefreshTokenGeneratesNewToken":1.063,"Tests\\Unit\\Auth\\TokenManagerTest::testTokenIsReusedWhenAlreadyGenerated":0.058,"Tests\\Unit\\Constants\\ResponseCodesTest::testIsSuccessReturnsTrueForSuccessCode":0,"Tests\\Unit\\Constants\\ResponseCodesTest::testIsSuccessReturnsFalseForErrorCodes":0,"Tests\\Unit\\Constants\\ResponseCodesTest::testGetDescriptionReturnsCorrectDescriptions":0,"Tests\\Unit\\Constants\\ResponseCodesTest::testIsHttpSuccessReturnsTrueForSuccessStatusCodes":0,"Tests\\Unit\\Constants\\ResponseCodesTest::testIsHttpSuccessReturnsFalseForErrorStatusCodes":0,"Tests\\Unit\\Constants\\ResponseCodesTest::testConstantsHaveCorrectValues":0,"Tests\\Unit\\Constants\\TransactionStatusTest::testIsCompletedReturnsTrueForCompletedStatuses":0.003,"Tests\\Unit\\Constants\\TransactionStatusTest::testIsCompletedReturnsFalseForNonCompletedStatuses":0,"Tests\\Unit\\Constants\\TransactionStatusTest::testIsPendingReturnsTrueForPendingStatuses":0,"Tests\\Unit\\Constants\\TransactionStatusTest::testIsPendingReturnsFalseForNonPendingStatuses":0,"Tests\\Unit\\Constants\\TransactionStatusTest::testIsFailedReturnsTrueForFailedStatuses":0,"Tests\\Unit\\Constants\\TransactionStatusTest::testIsFailedReturnsFalseForNonFailedStatuses":0,"Tests\\Unit\\Constants\\TransactionStatusTest::testGetAllStatusesReturnsAllStatuses":0,"Tests\\Unit\\Constants\\TransactionStatusTest::testConstantsHaveCorrectValues":0,"Tests\\Unit\\Validation\\ParameterValidatorTest::testValidateMSISDNReturnsTrueForValidNumbers":0,"Tests\\Unit\\Validation\\ParameterValidatorTest::testValidateMSISDNReturnsFalseForInvalidNumbers":0,"Tests\\Unit\\Validation\\ParameterValidatorTest::testValidateTransactionReferenceReturnsTrueForValidReferences":0,"Tests\\Unit\\Validation\\ParameterValidatorTest::testValidateTransactionReferenceReturnsFalseForInvalidReferences":0,"Tests\\Unit\\Validation\\ParameterValidatorTest::testValidateServiceProviderCodeReturnsTrueForValidCodes":0,"Tests\\Unit\\Validation\\ParameterValidatorTest::testValidateServiceProviderCodeReturnsFalseForInvalidCodes":0,"Tests\\Unit\\Validation\\ParameterValidatorTest::testValidateAmountReturnsTrueForValidAmounts":0,"Tests\\Unit\\Validation\\ParameterValidatorTest::testValidateAmountReturnsFalseForInvalidAmounts":0,"Tests\\Unit\\Validation\\ParameterValidatorTest::testValidateC2BParametersReturnsEmptyArrayForValidParams":0,"Tests\\Unit\\Validation\\ParameterValidatorTest::testValidateC2BParametersReturnsErrorsForInvalidParams":0,"Tests\\Unit\\Validation\\ParameterValidatorTest::testValidateB2BParametersReturnsEmptyArrayForValidParams":0,"Tests\\Unit\\Validation\\ParameterValidatorTest::testValidateReversalParametersReturnsEmptyArrayForValidParams":0}} -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `mpesa-php-sdk` will be documented in this file 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [2.2.2] - 2025-12-05 11 | ### Added 12 | - **Fix**: Bug fix in directory structure 13 | 14 | ## [2.2.1] - 2025-12-05 15 | ### Added 16 | - **Laravel Integration**: Added Laravel integration with service provider and facade 17 | 18 | ## [2.0.0] - 2025-01-17 19 | ### Added 20 | - **Token Management System**: Intelligent token management with caching 21 | - `TokenManager` class with automatic token generation and caching 22 | - Smart token reuse to improve performance 23 | - Manual token clearing capabilities 24 | - **Constants System**: Comprehensive constants for API responses and transaction status 25 | - `ResponseCodes` class with all M-Pesa API response codes 26 | - `TransactionStatus` class with transaction status constants 27 | - Utility methods for status checking (`isCompleted()`, `isPending()`, `isFailed()`) 28 | - **Parameter Validation**: Robust input validation system 29 | - `ParameterValidator` class with comprehensive validation rules 30 | - MSISDN validation for Mozambique phone numbers 31 | - Transaction reference and service provider code validation 32 | - Amount validation with proper type checking 33 | - **Exception System**: Custom exception hierarchy for better error handling 34 | - `MpesaException` base exception with context support 35 | - `ValidationException` for parameter validation errors 36 | - `AuthenticationException` for token/auth failures 37 | - `ApiException` for API-specific errors with response codes 38 | - **B2B Transactions**: Complete Business-to-Business transaction support 39 | - `b2b()` method for B2B payments 40 | - Synchronous and asynchronous B2B response classes 41 | - B2B response factory for automatic response creation 42 | - **Enhanced Response Classes**: Improved response handling across all transaction types 43 | - Better error detection with `isApiSuccess()` method 44 | - Consistent response parsing and data access 45 | - Integration with new constants system 46 | - **Comprehensive Testing**: Full test suite for reliability 47 | - Unit tests for all constants and validation logic 48 | - Token manager testing with expiration scenarios 49 | - Parameter validation testing for all transaction types 50 | - PHPUnit configuration with coverage reporting 51 | 52 | ### Enhanced 53 | - **Main SDK Class**: Updated `Mpesa` class with validation and improved error handling 54 | - Automatic parameter validation on all transaction methods 55 | - Integration with new token management system 56 | - Better exception handling with specific error types 57 | - **Documentation**: Completely updated README.md with comprehensive examples 58 | - Token management usage examples 59 | - Error handling patterns 60 | - Parameter validation examples 61 | - Best practices for token clearing and regeneration 62 | - **Project Structure**: Organized codebase with clear separation of concerns 63 | - `Auth/` directory for authentication components 64 | - `Constants/` directory for API constants 65 | - `Exceptions/` directory for custom exceptions 66 | - `Validation/` directory for input validation 67 | - Improved response class organization 68 | 69 | ### Changed 70 | - **Breaking Changes**: Updated method signatures and response handling 71 | - Token generation now uses `TokenManager` instead of direct generation 72 | - Removed `getData()` method from response classes - use specific getters instead 73 | - Response classes now include additional validation methods 74 | - Exception handling requires catching specific exception types 75 | - **Response Architecture**: Refactored response classes for better maintainability 76 | - Created `AsyncResponse` and `SyncResponse` base classes 77 | - Eliminated ~90% of code duplication between similar response classes 78 | - Improved consistency across all response types 79 | - **Performance**: Improved performance through token caching and reuse 80 | - **Reliability**: Enhanced error handling and input validation 81 | - **Maintainability**: Better code organization and comprehensive testing 82 | 83 | ### Examples 84 | - **Complete Example**: Added comprehensive usage example (`examples/complete_example.php`) 85 | - **Token Management Example**: Dedicated token management demonstration 86 | - **Best Practices**: Documented patterns for production usage 87 | ## [1.4.0] - 2020-11-06 88 | ### Added 89 | - laravel compatibility 90 | - ServiceProviderCode Parameter initialization 91 | ### Changed 92 | - ServiceProvider Optional on transactions methods 93 | ## [1.3.0] - 2020-11-06 94 | - Update dependencies 95 | ## [1.2.2] - 2020-05-22 96 | ### Changed 97 | - Disable SSL Verification behavior of request 98 | 99 | ## [1.0] - 2020-01-14 100 | ### Added 101 | - Readme 102 | -------------------------------------------------------------------------------- /src/Response/BaseResponse.php: -------------------------------------------------------------------------------- 1 | statusCode = $response->getStatusCode(); 26 | $bodyContents = $response->getBody()->getContents(); 27 | $this->response = json_decode($bodyContents); 28 | 29 | if ($this->response === null) { 30 | $this->response = $bodyContents; 31 | } 32 | 33 | $this->headers = $response->getHeaders(); 34 | $this->parseResponse(); 35 | } 36 | 37 | public function getStatusCode(): int 38 | { 39 | return $this->statusCode; 40 | } 41 | 42 | public function getRawResponse(): mixed 43 | { 44 | return $this->response; 45 | } 46 | 47 | public function getHeaders(): array 48 | { 49 | return $this->headers; 50 | } 51 | 52 | public function isSuccessful(): bool 53 | { 54 | return ResponseCodes::isHttpSuccess($this->statusCode); 55 | } 56 | 57 | /** 58 | * Check if the API response code indicates success 59 | */ 60 | public function isApiSuccess(): bool 61 | { 62 | $data = is_object($this->response) ? $this->response : json_decode($this->response); 63 | 64 | if ($data && isset($data->output_ResponseCode)) { 65 | return ResponseCodes::isSuccess($data->output_ResponseCode); 66 | } 67 | 68 | return false; 69 | } 70 | 71 | /** 72 | * Get the API response code description 73 | */ 74 | public function getApiResponseDescription(): string 75 | { 76 | $data = is_object($this->response) ? $this->response : json_decode($this->response); 77 | 78 | if ($data && isset($data->output_ResponseCode)) { 79 | return ResponseCodes::getDescription($data->output_ResponseCode); 80 | } 81 | 82 | return 'Unknown response'; 83 | } 84 | 85 | /** 86 | * Parse the response data and extract sync response properties 87 | */ 88 | protected function parseResponse(): void 89 | { 90 | $data = is_object($this->response) ? $this->response : json_decode($this->response); 91 | 92 | if ($data) { 93 | $this->output_TransactionID = $data->output_TransactionID ?? null; 94 | $this->output_ConversationID = $data->output_ConversationID ?? null; 95 | $this->output_ResponseCode = $data->output_ResponseCode ?? null; 96 | $this->output_ResponseDescription = $data->output_ResponseDescription ?? $data->output_ResponseDesc ?? null; 97 | $this->output_ThirdPartyReference = $data->output_ThirdPartyReference ?? null; 98 | } 99 | } 100 | 101 | /** 102 | * Get the transaction ID 103 | */ 104 | public function getTransactionId(): ?string 105 | { 106 | return $this->output_TransactionID; 107 | } 108 | 109 | /** 110 | * Get the conversation ID 111 | */ 112 | public function getConversationId(): ?string 113 | { 114 | return $this->output_ConversationID; 115 | } 116 | 117 | /** 118 | * Get the response code 119 | */ 120 | public function getResponseCode(): ?string 121 | { 122 | return $this->output_ResponseCode; 123 | } 124 | 125 | /** 126 | * Get the response description 127 | */ 128 | public function getResponseDescription(): ?string 129 | { 130 | return $this->output_ResponseDescription; 131 | } 132 | 133 | /** 134 | * Checks if the transaction was successful based on HTTP and API response codes 135 | */ 136 | public function isTransactionSuccessful(): bool 137 | { 138 | return $this->isSuccessful() && $this->isApiSuccess(); 139 | } 140 | 141 | /** 142 | * For asynchronous responses, the transaction status should be checked 143 | * later using the ConversationID through the status() method 144 | */ 145 | public function isTransactionInitiated(): bool 146 | { 147 | return $this->isSuccessful() && !empty($this->output_ConversationID); 148 | } 149 | 150 | /** 151 | * Get the third party reference 152 | */ 153 | public function getThirdPartyReference(): ?string 154 | { 155 | return $this->output_ThirdPartyReference; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/Validation/ParameterValidator.php: -------------------------------------------------------------------------------- 1 | 0; 54 | } 55 | 56 | /** 57 | * Validate security credential format 58 | */ 59 | public static function validateSecurityCredential(string $credential): bool 60 | { 61 | // Should be non-empty string, max 255 characters 62 | return !empty($credential) && strlen($credential) <= 255; 63 | } 64 | 65 | /** 66 | * Validate initiator identifier format 67 | */ 68 | public static function validateInitiatorIdentifier(string $identifier): bool 69 | { 70 | // Should be non-empty string, max 100 characters 71 | return !empty($identifier) && strlen($identifier) <= 100; 72 | } 73 | 74 | /** 75 | * Validate all required parameters for C2B transaction 76 | */ 77 | public static function validateC2BParameters(array $params): array 78 | { 79 | $errors = []; 80 | 81 | if (!isset($params['transactionReference']) || !self::validateTransactionReference($params['transactionReference'])) { 82 | $errors[] = 'Invalid transaction reference format'; 83 | } 84 | 85 | if (!isset($params['customerMSISDN']) || !self::validateMSISDN($params['customerMSISDN'])) { 86 | $errors[] = 'Invalid customer MSISDN format (should be 258XXXXXXXX)'; 87 | } 88 | 89 | if (!isset($params['amount']) || !self::validateAmount($params['amount'])) { 90 | $errors[] = 'Invalid amount (should be positive number)'; 91 | } 92 | 93 | if (!isset($params['thirdPartyReference']) || !self::validateThirdPartyReference($params['thirdPartyReference'])) { 94 | $errors[] = 'Invalid third party reference format'; 95 | } 96 | 97 | if (!isset($params['serviceProviderCode']) || !self::validateServiceProviderCode($params['serviceProviderCode'])) { 98 | $errors[] = 'Invalid service provider code format (should be 6 digits)'; 99 | } 100 | 101 | return $errors; 102 | } 103 | 104 | /** 105 | * Validate all required parameters for B2C transaction 106 | */ 107 | public static function validateB2CParameters(array $params): array 108 | { 109 | return self::validateC2BParameters($params); // Same validation rules 110 | } 111 | 112 | /** 113 | * Validate all required parameters for B2B transaction 114 | */ 115 | public static function validateB2BParameters(array $params): array 116 | { 117 | $errors = []; 118 | 119 | if (!isset($params['transactionReference']) || !self::validateTransactionReference($params['transactionReference'])) { 120 | $errors[] = 'Invalid transaction reference format'; 121 | } 122 | 123 | if (!isset($params['amount']) || !self::validateAmount($params['amount'])) { 124 | $errors[] = 'Invalid amount (should be positive number)'; 125 | } 126 | 127 | if (!isset($params['thirdPartyReference']) || !self::validateThirdPartyReference($params['thirdPartyReference'])) { 128 | $errors[] = 'Invalid third party reference format'; 129 | } 130 | 131 | if (!isset($params['primaryPartyCode']) || !self::validateServiceProviderCode($params['primaryPartyCode'])) { 132 | $errors[] = 'Invalid primary party code format (should be 6 digits)'; 133 | } 134 | 135 | if (!isset($params['receiverPartyCode']) || !self::validateServiceProviderCode($params['receiverPartyCode'])) { 136 | $errors[] = 'Invalid receiver party code format (should be 6 digits)'; 137 | } 138 | 139 | return $errors; 140 | } 141 | 142 | /** 143 | * Validate all required parameters for reversal transaction 144 | */ 145 | public static function validateReversalParameters(array $params): array 146 | { 147 | $errors = []; 148 | 149 | if (!isset($params['transactionID']) || empty($params['transactionID'])) { 150 | $errors[] = 'Transaction ID is required'; 151 | } 152 | 153 | if (!isset($params['securityCredential']) || !self::validateSecurityCredential($params['securityCredential'])) { 154 | $errors[] = 'Invalid security credential'; 155 | } 156 | 157 | if (!isset($params['initiatorIdentifier']) || !self::validateInitiatorIdentifier($params['initiatorIdentifier'])) { 158 | $errors[] = 'Invalid initiator identifier'; 159 | } 160 | 161 | if (!isset($params['thirdPartyReference']) || !self::validateThirdPartyReference($params['thirdPartyReference'])) { 162 | $errors[] = 'Invalid third party reference format'; 163 | } 164 | 165 | if (isset($params['reversalAmount']) && !self::validateAmount($params['reversalAmount'])) { 166 | $errors[] = 'Invalid reversal amount (should be positive number)'; 167 | } 168 | 169 | return $errors; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/Mpesa.php: -------------------------------------------------------------------------------- 1 | tokenManager = new TokenManager($publicKey, $apiKey); 32 | 33 | if (!$isTest) { 34 | $this->base_uri = 'https://api.vm.co.mz'; 35 | } 36 | } 37 | 38 | 39 | /** 40 | * Standard customer-to-business transaction 41 | * 42 | * @param string $transactionReference 43 | * @param string $customerMSISDN 44 | * @param int $amount 45 | * @param string $thirdPartReference 46 | * @return TransactionResponse 47 | * @throws ValidationException 48 | */ 49 | public function c2b(string $transactionReference, string $customerMSISDN, float $amount, string $thirdPartReference, ?string $serviceProviderCode = null): TransactionResponse 50 | { 51 | // Validate parameters 52 | $params = [ 53 | 'transactionReference' => $transactionReference, 54 | 'customerMSISDN' => $customerMSISDN, 55 | 'amount' => $amount, 56 | 'thirdPartyReference' => $thirdPartReference, 57 | 'serviceProviderCode' => $serviceProviderCode ?? $this->serviceProviderCode 58 | ]; 59 | 60 | $errors = ParameterValidator::validateC2BParameters($params); 61 | if (!empty($errors)) { 62 | throw new ValidationException($errors); 63 | } 64 | 65 | $fields = [ 66 | "input_TransactionReference" => $transactionReference, 67 | "input_CustomerMSISDN" => $customerMSISDN, 68 | "input_Amount" => $amount, 69 | "input_ThirdPartyReference" => $thirdPartReference, 70 | "input_ServiceProviderCode" => $serviceProviderCode ?? $this->serviceProviderCode 71 | ]; 72 | 73 | $response = $this->makeRequest('/ipg/v1x/c2bPayment/singleStage/', 18352, 'POST', $fields); 74 | 75 | return new TransactionResponse($response); 76 | } 77 | 78 | 79 | /** 80 | * Business to customer transaction sync 81 | * 82 | * @param string $customerMSISDN 83 | * @param int $amount 84 | * @param string $transactionReference 85 | * @param string $thirdPartReference 86 | * @return TransactionResponse 87 | */ 88 | public function b2c(string $customerMSISDN, int $amount, string $transactionReference, string $thirdPartReference, ?string $serviceProviderCode = "171717"): TransactionResponse 89 | { 90 | $fields = [ 91 | "input_TransactionReference" => $transactionReference, 92 | "input_CustomerMSISDN" => $customerMSISDN, 93 | "input_Amount" => $amount, 94 | "input_ThirdPartyReference" => $thirdPartReference, 95 | "input_ServiceProviderCode" => $serviceProviderCode ?? $this->serviceProviderCode 96 | ]; 97 | 98 | $response = $this->makeRequest('/ipg/v1x/b2cPayment/', 18345, 'POST', $fields); 99 | 100 | return new TransactionResponse($response); 101 | } 102 | 103 | 104 | 105 | /** 106 | * Business to business transaction sync 107 | * 108 | * @param string $transactionReference 109 | * @param int $amount 110 | * @param string $thirdPartReference 111 | * @param string $primaryPartyCode Business shortcode for funds debit 112 | * @param string $receiverPartyCode Business shortcode for funds credit 113 | * @return TransactionResponse 114 | */ 115 | public function b2b(string $transactionReference, int $amount, string $thirdPartReference, string $primaryPartyCode, string $receiverPartyCode): TransactionResponse 116 | { 117 | $fields = [ 118 | "input_TransactionReference" => $transactionReference, 119 | "input_Amount" => $amount, 120 | "input_ThirdPartyReference" => $thirdPartReference, 121 | "input_PrimaryPartyCode" => $primaryPartyCode, 122 | "input_ReceiverPartyCode" => $receiverPartyCode 123 | ]; 124 | 125 | $response = $this->makeRequest('/ipg/v1x/b2bPayment/', 18349, 'POST', $fields); 126 | 127 | return new TransactionResponse($response); 128 | } 129 | 130 | 131 | 132 | /** 133 | * Process transaction refund/reversal 134 | * 135 | * @param string $transactionID 136 | * @param string $securityCredential 137 | * @param string $initiatorIdentifier 138 | * @param string $thirdPartyReference 139 | * @param string $serviceProviderCode 140 | * @param string $reversalAmount Optional: for partial refunds 141 | * @return ReversalResponse 142 | */ 143 | public function reversal(string $transactionID, string $securityCredential, string $initiatorIdentifier, string $thirdPartyReference, ?string $serviceProviderCode = null, ?string $reversalAmount = null): ReversalResponse 144 | { 145 | $serviceProviderCode = $serviceProviderCode ?: $this->serviceProviderCode; 146 | $fields = [ 147 | "input_TransactionID" => $transactionID, 148 | "input_SecurityCredential" => $securityCredential, 149 | "input_InitiatorIdentifier" => $initiatorIdentifier, 150 | "input_ThirdPartyReference" => $thirdPartyReference, 151 | "input_ServiceProviderCode" => $serviceProviderCode, 152 | ]; 153 | if (isset($reversalAmount)) { 154 | $fields['input_ReversalAmount'] = $reversalAmount; 155 | } 156 | 157 | $response = $this->makeRequest('/ipg/v1x/reversal/', 18354, 'PUT', $fields); 158 | 159 | return new ReversalResponse($response); 160 | } 161 | 162 | /** 163 | * Query transaction status 164 | * 165 | * @param string $thirdPartyReference 166 | * @param string $queryReference 167 | * @param string $serviceProviderCode 168 | * @return TransactionStatusResponse 169 | */ 170 | public function queryTransactionStatus(string $thirdPartyReference, string $queryReference, ?string $serviceProviderCode = null): TransactionStatusResponse 171 | { 172 | $serviceProviderCode = $serviceProviderCode ?: $this->serviceProviderCode; 173 | $fields = [ 174 | 'input_ThirdPartyReference' => $thirdPartyReference, 175 | 'input_QueryReference' => $queryReference, 176 | 'input_ServiceProviderCode' => $serviceProviderCode 177 | ]; 178 | 179 | $response = $this->makeRequest('/ipg/v1x/queryTransactionStatus/', 18353, 'GET', $fields); 180 | 181 | return new TransactionStatusResponse($response); 182 | } 183 | 184 | /** 185 | * Query customer name for confirmation purposes 186 | * The API provides the First and Second name from the customer but obfuscated. 187 | * 188 | * @param string $customerMSISDN 189 | * @param string $thirdPartyReference 190 | * @param string $serviceProviderCode 191 | * @return CustomerNameResponse 192 | */ 193 | public function queryCustomerName(string $customerMSISDN, string $thirdPartyReference, ?string $serviceProviderCode = null): CustomerNameResponse 194 | { 195 | $serviceProviderCode = $serviceProviderCode ?: $this->serviceProviderCode; 196 | $fields = [ 197 | "input_CustomerMSISDN" => $customerMSISDN, 198 | "input_ThirdPartyReference" => $thirdPartyReference, 199 | "input_ServiceProviderCode" => $serviceProviderCode 200 | ]; 201 | 202 | $response = $this->makeRequest('/ipg/v1x/queryCustomerName/', 19323, 'GET', $fields); 203 | 204 | return new CustomerNameResponse($response); 205 | } 206 | 207 | /** 208 | * Generates a base64 encoded token 209 | * @throws AuthenticationException 210 | */ 211 | public function getToken(): string 212 | { 213 | if (empty($this->publicKey) || empty($this->apiKey)) { 214 | throw new AuthenticationException('Public key and API key are required'); 215 | } 216 | 217 | try { 218 | return $this->tokenManager->getToken(); 219 | } catch (\Exception $e) { 220 | throw new AuthenticationException('Failed to generate token: ' . $e->getMessage(), 0, $e); 221 | } 222 | } 223 | 224 | /** 225 | * Get token manager instance 226 | */ 227 | public function getTokenManager(): TokenManager 228 | { 229 | return $this->tokenManager; 230 | } 231 | 232 | /** 233 | * @param string $url 234 | * @param int $port 235 | * @param string $method 236 | * @param array $fields 237 | * @return \Psr\Http\Message\ResponseInterface 238 | */ 239 | private function makeRequest(string $url, int $port, string $method, array $fields = []) 240 | { 241 | $client = new Client([ 242 | 'base_uri' => $this->base_uri . ':' . $port, 243 | 'timeout' => 90, 244 | ]); 245 | 246 | $options = [ 247 | 'http_errors' => false, 248 | 'headers' => $this->getHeaders(), 249 | 'verify' => false 250 | ]; 251 | 252 | if ($method == 'POST' || $method == 'PUT') { 253 | $options += ['json' => $fields]; 254 | } else { 255 | $options += ['query' => $fields]; 256 | } 257 | return $client->request($method, $url, $options); 258 | } 259 | 260 | /** 261 | * @return array 262 | */ 263 | private function getHeaders() 264 | { 265 | $headers = [ 266 | 'Content-Type' => 'application/json', 267 | 'Authorization' => 'Bearer ' . $this->getToken(), 268 | 'origin' => 'developer.mpesa.vm.co.mz', 269 | 'Connection' => 'keep-alive', 270 | 'User-Agent' => 'Hypertech/MpesaPHP-SDK' 271 | ]; 272 | return $headers; 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # M-Pesa API Documentation 2 | 3 | This document provides comprehensive information about the M-Pesa API endpoints and their usage. 4 | 5 | ## Table of Contents 6 | 7 | 1. [C2B Payment Single Stage](#c2b-payment-single-stage) 8 | 2. [B2C Payment](#b2c-payment) 9 | 3. [B2B Payment](#b2b-payment) 10 | 4. [Query Transaction Status](#query-transaction-status) 11 | 5. [Reversal API](#reversal-api) 12 | 13 | --- 14 | 15 | ## C2B Payment Single Stage 16 | 17 | **Customer-to-Business Transaction** 18 | 19 | The C2B API Call is used as a standard customer-to-business transaction. Funds from the customer's mobile money wallet will be deducted and transferred to the mobile money wallet of the business. To authenticate and authorize this transaction, M-Pesa Payments Gateway will initiate a USSD Push message to the customer to gather and verify the mobile money PIN number. 20 | 21 | ### Endpoint Information 22 | 23 | | Property | Value | 24 | |----------|-------| 25 | | **Address** | `api.sandbox.vm.co.mz` | 26 | | **Port** | `18352` | 27 | | **Path** | `/ipg/v1x/c2bPayment/singleStage/` | 28 | | **SSL** | ✔ Required | 29 | | **Method** | `POST` | 30 | 31 | ### Headers 32 | 33 | | Header | Value | Required | 34 | |--------|-------|----------| 35 | | `Content-Type` | `application/json` | ✔ | 36 | | `Authorization` | `Bearer {access_token}` | ✔ | 37 | | `Origin` | `developer.mpesa.vm.co.mz` | ✔ | 38 | 39 | ### Request Parameters 40 | 41 | | Parameter | Description | Type | Required | Example | 42 | |-----------|-------------|------|----------|---------| 43 | | `input_TransactionReference` | Transaction reference for customer/business | String | ✔ | `T12344C` | 44 | | `input_CustomerMSISDN` | Customer's mobile number | String | ✔ | `258843330333` | 45 | | `input_Amount` | Transaction amount | String | ✔ | `10` | 46 | | `input_ThirdPartyReference` | Unique third party system reference | String | ✔ | `11114` | 47 | | `input_ServiceProviderCode` | Business shortcode for funds credit | String | ✔ | `171717` | 48 | 49 | ### Response Parameters 50 | 51 | #### Synchronous Response 52 | 53 | | Field | Description | Type | Example | 54 | |-------|-------------|------|---------| 55 | | `output_ConversationID` | Mobile Money platform generated ID | String | `AG_20180206_00005e7dccc6da08efa8` | 56 | | `output_TransactionID` | Mobile Money platform transaction ID | String | `4XDF12345` | 57 | | `output_ResponseDesc` | iPG platform status description | String | See response codes | 58 | | `output_ResponseCode` | iPG platform status code | String | See response codes | 59 | | `output_ThirdPartyReference` | Third party system reference | String | `11114` | 60 | 61 | #### Asynchronous Response 62 | 63 | Immediate response provided before session closure. Use Conversation ID for status queries. 64 | 65 | | Field | Description | Type | Example | 66 | |-------|-------------|------|---------| 67 | | `output_ThirdPartyReference` | Third party system reference | String | `11114` | 68 | | `output_ConversationID` | Mobile Money platform generated ID | String | `18ffa4ccf93343379421d2eac15a3e8c` | 69 | | `output_ResponseCode` | iPG platform status code | String | See response codes | 70 | | `output_ResponseDesc` | iPG platform status description | String | See response codes | 71 | 72 | #### Async Result Callback (Webhook) 73 | 74 | Sent to your configured Response URL when transaction completes. 75 | 76 | **Request to Your Endpoint:** 77 | 78 | | Field | Description | Type | Example | 79 | |-------|-------------|------|---------| 80 | | `input_OriginalConversationID` | iPG platform conversation ID | String | `241fasif1f1n92fn129nfasnf91nf1` | 81 | | `input_ThirdPartyReference` | Third party system reference | String | `11114` | 82 | | `input_TransactionID` | Mobile Money transaction ID | String | `4XDF12345` | 83 | | `input_ResultCode` | Transaction result code | String | `0` | 84 | | `input_ResultDesc` | Transaction result description | String | `Request Processed Successfully` | 85 | 86 | **Required Response from Your Endpoint:** 87 | 88 | | Field | Description | Type | Example | 89 | |-------|-------------|------|---------| 90 | | `output_OriginalConversationID` | Same as received conversation ID | String | `241fasif1f1n92fn129nfasnf91nf1` | 91 | | `output_ResponseDesc` | Receipt status description | String | `Successfully Accepted Result` | 92 | | `output_ResponseCode` | Receipt status code | String | `0` | 93 | | `output_ThirdPartyConversationID` | Your system reference | String | `11114` | 94 | 95 | --- 96 | 97 | ## B2C Payment 98 | 99 | **Business-to-Customer Transaction** 100 | 101 | The B2C API Call is used as a standard business-to-customer transaction. Funds from the business' mobile money wallet will be deducted and transferred to the mobile money wallet of the third party customer. 102 | 103 | ### Endpoint Information 104 | 105 | | Property | Value | 106 | |----------|-------| 107 | | **Address** | `api.sandbox.vm.co.mz` | 108 | | **Port** | `18345` | 109 | | **Path** | `/ipg/v1x/b2cPayment/` | 110 | | **SSL** | ✔ Required | 111 | | **Method** | `POST` | 112 | 113 | ### Headers 114 | 115 | | Header | Value | Required | 116 | |--------|-------|----------| 117 | | `Content-Type` | `application/json` | ✔ | 118 | | `Authorization` | `Bearer {access_token}` | ✔ | 119 | | `Origin` | `developer.mpesa.vm.co.mz` | ✔ | 120 | 121 | ### Request Parameters 122 | 123 | | Parameter | Description | Type | Required | Example | 124 | |-----------|-------------|------|----------|---------| 125 | | `input_TransactionReference` | Transaction reference for customer/business | String | ✔ | `T12344C` | 126 | | `input_CustomerMSISDN` | Customer's mobile number | String | ✔ | `258843330333` | 127 | | `input_Amount` | Transaction amount | String | ✔ | `10` | 128 | | `input_ThirdPartyReference` | Unique third party system reference | String | ✔ | `11114` | 129 | | `input_ServiceProviderCode` | Business shortcode for funds debit | String | ✔ | `171717` | 130 | 131 | ### Response Parameters 132 | 133 | #### Synchronous Response 134 | 135 | | Field | Description | Type | Example | 136 | |-------|-------------|------|---------| 137 | | `output_ConversationID` | Mobile Money platform generated ID | String | `AG_20180206_00005e7dccc6da08efa8` | 138 | | `output_TransactionID` | Mobile Money platform transaction ID | String | `4XDF12345` | 139 | | `output_ResponseDesc` | iPG platform status description | String | See response codes | 140 | | `output_ResponseCode` | iPG platform status code | String | See response codes | 141 | | `output_ThirdPartyReference` | Third party system reference | String | `11114` | 142 | 143 | #### Asynchronous Response 144 | 145 | Immediate response provided before session closure. Use Conversation ID for status queries. 146 | 147 | | Field | Description | Type | Example | 148 | |-------|-------------|------|---------| 149 | | `output_ThirdPartyReference` | Third party system reference | String | `11114` | 150 | | `output_ConversationID` | Mobile Money platform generated ID | String | `18ffa4ccf93343379421d2eac15a3e8c` | 151 | | `output_ResponseCode` | iPG platform status code | String | See response codes | 152 | | `output_ResponseDesc` | iPG platform status description | String | See response codes | 153 | 154 | #### Async Result Callback (Webhook) 155 | 156 | **Request to Your Endpoint:** 157 | 158 | | Field | Description | Type | Example | 159 | |-------|-------------|------|---------| 160 | | `input_OriginalConversationID` | iPG platform conversation ID | String | `241fasif1f1n92fn129nfasnf91nf1` | 161 | | `input_ThirdPartyReference` | Third party system reference | String | `11114` | 162 | | `input_TransactionID` | Mobile Money transaction ID | String | `4XDF12345` | 163 | | `input_ResultCode` | Transaction result code | String | `0` | 164 | | `input_ResultDesc` | Transaction result description | String | `Request Processed Successfully` | 165 | 166 | **Required Response from Your Endpoint:** 167 | 168 | | Field | Description | Type | Example | 169 | |-------|-------------|------|---------| 170 | | `output_OriginalConversationID` | Same as received conversation ID | String | `241fasif1f1n92fn129nfasnf91nf1` | 171 | | `output_ResponseDesc` | Receipt status description | String | `Successfully Accepted Result` | 172 | | `output_ResponseCode` | Receipt status code | String | `0` | 173 | | `output_ThirdPartyConversationID` | Your system reference | String | `11114` | 174 | 175 | --- 176 | 177 | ## B2B Payment 178 | 179 | **Business-to-Business Transaction** 180 | 181 | The B2B API Call is used as a standard business-to-business transaction. Funds from the business' mobile money wallet will be deducted and transferred to the mobile money wallet of the third party business. 182 | 183 | ### Endpoint Information 184 | 185 | | Property | Value | 186 | |----------|-------| 187 | | **Address** | `api.sandbox.vm.co.mz` | 188 | | **Port** | `18349` | 189 | | **Path** | `/ipg/v1x/b2bPayment/` | 190 | | **SSL** | ✔ Required | 191 | | **Method** | `POST` | 192 | 193 | ### Headers 194 | 195 | | Header | Value | Required | 196 | |--------|-------|----------| 197 | | `Content-Type` | `application/json` | ✔ | 198 | | `Authorization` | `Bearer {access_token}` | ✔ | 199 | | `Origin` | `developer.mpesa.vm.co.mz` | ✔ | 200 | 201 | ### Request Parameters 202 | 203 | | Parameter | Description | Type | Required | Example | 204 | |-----------|-------------|------|----------|---------| 205 | | `input_TransactionReference` | Transaction reference for business | String | ✔ | `T12344C` | 206 | | `input_Amount` | Transaction amount | String | ✔ | `10` | 207 | | `input_ThirdPartyReference` | Unique third party system reference | String | ✔ | `11114` | 208 | | `input_PrimaryPartyCode` | Business shortcode for funds debit | String | ✔ | `171717` | 209 | | `input_ReceiverPartyCode` | Business shortcode for funds credit | String | ✔ | `979797` | 210 | 211 | ### Response Parameters 212 | 213 | #### Synchronous Response 214 | 215 | | Field | Description | Type | Example | 216 | |-------|-------------|------|---------| 217 | | `output_ConversationID` | Mobile Money platform generated ID | String | `AG_20180206_00005e7dccc6da08efa8` | 218 | | `output_TransactionID` | Mobile Money platform transaction ID | String | `4XDF12345` | 219 | | `output_ResponseDesc` | iPG platform status description | String | See response codes | 220 | | `output_ResponseCode` | iPG platform status code | String | See response codes | 221 | | `output_ThirdPartyReference` | Third party system reference | String | `11114` | 222 | 223 | #### Asynchronous Response 224 | 225 | | Field | Description | Type | Example | 226 | |-------|-------------|------|---------| 227 | | `output_ThirdPartyReference` | Third party system reference | String | `11114` | 228 | | `output_ConversationID` | Mobile Money platform generated ID | String | `18ffa4ccf93343379421d2eac15a3e8c` | 229 | | `output_ResponseCode` | iPG platform status code | String | See response codes | 230 | | `output_ResponseDesc` | iPG platform status description | String | See response codes | 231 | 232 | --- 233 | 234 | ## Query Transaction Status 235 | 236 | **Transaction Status Inquiry** 237 | 238 | The Query Transaction Status API is used to determine the current status of a particular transaction. Using either the Transaction ID or the Conversation ID from the Mobile Money Platform, the M-Pesa Payments Gateway will return information about the transaction's status. 239 | 240 | ### Endpoint Information 241 | 242 | | Property | Value | 243 | |----------|-------| 244 | | **Address** | `api.sandbox.vm.co.mz` | 245 | | **Port** | `18353` | 246 | | **Path** | `/ipg/v1x/queryTransactionStatus/` | 247 | | **SSL** | ✔ Required | 248 | | **Method** | `GET` | 249 | 250 | ### Headers 251 | 252 | | Header | Value | Required | 253 | |--------|-------|----------| 254 | | `Content-Type` | `application/json` | ✔ | 255 | | `Authorization` | `Bearer {access_token}` | ✔ | 256 | | `Origin` | `developer.mpesa.vm.co.mz` | ✔ | 257 | 258 | ### Request Parameters 259 | 260 | | Parameter | Description | Type | Required | Example | 261 | |-----------|-------------|------|----------|---------| 262 | | `input_ThirdPartyReference` | Unique third party system reference | String | ✔ | `11114` | 263 | | `input_QueryReference` | Transaction ID, ThirdPartyReference, or ConversationID | String | ✔ | `5C1400CVRO` or `AG_20180206_00005e7dccc6da08efa8` | 264 | | `input_ServiceProviderCode` | Business shortcode | String | ✔ | `171717` | 265 | 266 | ### Response Parameters 267 | 268 | #### Synchronous Response 269 | 270 | | Field | Description | Type | Example | 271 | |-------|-------------|------|---------| 272 | | `output_ConversationID` | Mobile Money platform generated ID | String | `AG_20180206_00005e7dccc6da08efa8` | 273 | | `output_ResponseDesc` | iPG platform status description | String | See response codes | 274 | | `output_ResponseCode` | iPG platform status code | String | See response codes | 275 | | `output_ThirdPartyReference` | Third party system reference | String | `11114` | 276 | | `output_ResponseTransactionStatus` | Transaction processing status | String | `Cancelled`, `Completed`, `Expired`, `N/A` | 277 | 278 | --- 279 | 280 | ## Reversal API 281 | 282 | **Transaction Reversal** 283 | 284 | The Reversal API is used to reverse a successful transaction. Using the Transaction ID of a previously successful transaction, M-Pesa Payments Gateway will withdraw the funds from the recipient party's mobile money wallet and revert the funds to the mobile money wallet of the initiating party of the original transaction. 285 | 286 | ### Endpoint Information 287 | 288 | | Property | Value | 289 | |----------|-------| 290 | | **Address** | `api.sandbox.vm.co.mz` | 291 | | **Port** | `18354` | 292 | | **Path** | `/ipg/v1x/reversal/` | 293 | | **SSL** | ✔ Required | 294 | | **Method** | `PUT` | 295 | 296 | ### Headers 297 | 298 | | Header | Value | Required | 299 | |--------|-------|----------| 300 | | `Content-Type` | `application/json` | ✔ | 301 | | `Authorization` | `Bearer {access_token}` | ✔ | 302 | | `Origin` | `developer.mpesa.vm.co.mz` | ✔ | 303 | 304 | ### Request Parameters 305 | 306 | | Parameter | Description | Type | Required | Example | 307 | |-----------|-------------|------|----------|---------| 308 | | `input_TransactionID` | Mobile Money Platform TransactionID for successful transaction | String | ✔ | `49XCDF6` | 309 | | `input_SecurityCredential` | Vodacom generated security credential | String | ✔ | `Mpesa2019` | 310 | | `input_InitiatorIdentifier` | Vodacom generated initiator identifier | String | ✔ | `MPesa2018` | 311 | | `input_ThirdPartyReference` | Unique third party system reference | String | ✔ | `11114` | 312 | | `input_ServiceProviderCode` | Business shortcode | String | ✔ | `171717` | 313 | | `input_ReversalAmount` | Amount to reverse (optional for partial reversal) | String | ✘ | `10` | 314 | 315 | ### Response Parameters 316 | 317 | #### Synchronous Response 318 | 319 | | Field | Description | Type | Example | 320 | |-------|-------------|------|---------| 321 | | `output_ConversationID` | Mobile Money platform generated ID | String | `AG_20180206_00005e7dccc6da08efa8` | 322 | | `output_TransactionID` | Mobile Money platform transaction ID | String | `4XDF12345` | 323 | | `output_ResponseDesc` | iPG platform status description | String | See response codes | 324 | | `output_ResponseCode` | iPG platform status code | String | See response codes | 325 | | `output_ThirdPartyReference` | Third party system reference | String | `11114` | 326 | 327 | #### Asynchronous Response 328 | 329 | | Field | Description | Type | Example | 330 | |-------|-------------|------|---------| 331 | | `output_ThirdPartyReference` | Third party system reference | String | `11114` | 332 | | `output_ConversationID` | Mobile Money platform generated ID | String | `18ffa4ccf93343379421d2eac15a3e8c` | 333 | | `output_ResponseCode` | iPG platform status code | String | See response codes | 334 | | `output_ResponseDesc` | iPG platform status description | String | See response codes | 335 | 336 | #### Async Result Callback (Webhook) 337 | 338 | **Request to Your Endpoint:** 339 | 340 | | Field | Description | Type | Example | 341 | |-------|-------------|------|---------| 342 | | `input_OriginalConversationID` | iPG platform conversation ID | String | `241fasif1f1n92fn129nfasnf91nf1` | 343 | | `input_ThirdPartyReference` | Third party system reference | String | `11114` | 344 | | `input_TransactionID` | Mobile Money transaction ID | String | `4XDF12345` | 345 | | `input_ResultCode` | Transaction result code | String | `0` | 346 | | `input_ResultDesc` | Transaction result description | String | `Request Processed Successfully` | 347 | 348 | **Required Response from Your Endpoint:** 349 | 350 | | Field | Description | Type | Example | 351 | |-------|-------------|------|---------| 352 | | `output_OriginalConversationID` | Same as received conversation ID | String | `241fasif1f1n92fn129nfasnf91nf1` | 353 | | `output_ResponseDesc` | Receipt status description | String | `Successfully Accepted Result` | 354 | | `output_ResponseCode` | Receipt status code | String | `0` | 355 | | `output_ThirdPartyConversationID` | Your system reference | String | `11114` | 356 | 357 | --- 358 | 359 | ## Response Codes 360 | 361 | ### Common Response Codes 362 | 363 | | Code | Description | Status | 364 | |------|-------------|--------| 365 | | `INS-0` | Request processed successfully | Success | 366 | | `INS-1` | Internal Error | Error | 367 | | `INS-2` | Not enough balance | Error | 368 | | `INS-4` | Transaction failed | Error | 369 | | `INS-5` | Transaction expired | Error | 370 | | `INS-6` | Transaction not permitted to sender | Error | 371 | | `INS-9` | Request timeout | Error | 372 | | `INS-10` | Duplicate transaction | Error | 373 | 374 | ### Transaction Status Values 375 | 376 | | Status | Description | 377 | |--------|-------------| 378 | | `Completed` | Transaction completed successfully | 379 | | `Pending` | Transaction is being processed | 380 | | `Cancelled` | Transaction was cancelled | 381 | | `Expired` | Transaction expired | 382 | | `Failed` | Transaction failed | 383 | | `N/A` | Status not available | 384 | 385 | --- 386 | 387 | ## Authentication 388 | 389 | All API calls require authentication using a Bearer token. The token should be obtained from the M-Pesa authentication endpoint and included in the `Authorization` header of all requests. 390 | 391 | ### Example Authentication Header 392 | 393 | ``` 394 | Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9... 395 | ``` 396 | 397 | ### Token Expiration 398 | 399 | Tokens have a limited lifespan and should be refreshed when they expire. Monitor the response codes for authentication failures and implement token refresh logic accordingly. 400 | 401 | 402 | 403 | ### Response Codes & Descriptions 404 | 405 | | Http Status Code | Code | Description | 406 | |-------------------|-------|-------------| 407 | | 200 / 201 | INS-0 | Request processed successfully | 408 | | 500 | INS-1 | Internal Error | 409 | | 401 | INS-2 | Invalid API Key | 410 | | 401 | INS-4 | User is not active | 411 | | 401 | INS-5 | Transaction cancelled by customer | 412 | | 401 | INS-6 | Transaction Failed | 413 | | 408 | INS-9 | Request timeout | 414 | | 409 | INS-10 | Duplicate Transaction | 415 | | 400 | INS-13 | Invalid Shortcode Used | 416 | | 400 | INS-14 | Invalid Reference Used | 417 | | 400 | INS-15 | Invalid Amount Used | 418 | | 503 | INS-16 | Unable to handle the request due to a temporary overloading | 419 | | 400 | INS-17 | Invalid Transaction Reference. Length Should Be Between 1 and 20. | 420 | | 400 | INS-18 | Invalid TransactionID Used | 421 | | 400 | INS-19 | Invalid ThirdPartyReference Used | 422 | | 400 | INS-20 | Not All Parameters Provided. Please try again. | 423 | | 400 | INS-21 | Parameter validations failed. Please try again. | 424 | | 400 | INS-22 | Invalid Operation Type | 425 | | 400 | INS-23 | Unknown Status. Contact M-Pesa Support | 426 | | 400 | INS-24 | Invalid InitiatorIdentifier Used | 427 | | 400 | INS-25 | Invalid SecurityCredential Used | 428 | | 400 | INS-26 | Not authorized | 429 | | 400 | INS-993 | Direct Debit Missing | 430 | | 400 | INS-994 | Direct Debit Already Exists | 431 | | 400 | INS-995 | Customer's Profile Has Problems | 432 | | 400 | INS-996 | Customer Account Status Not Active | 433 | | 400 | INS-997 | Linking Transaction Not Found | 434 | | 400 | INS-998 | Invalid Market | 435 | | 400 | INS-2001 | Initiator authentication error. | 436 | | 400 | INS-2002 | Receiver invalid. | 437 | | 422 | INS-2006 | Insufficient balance | 438 | | 400 | INS-2051 | MSISDN invalid. | 439 | | 400 | INS-2057 | Language code invalid. | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mpesa Mozambique PHP SDK 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/karson/mpesa-php-sdk.svg?style=flat-square)](https://packagist.org/packages/karson/mpesa-php-sdk) 4 | [![Build Status](https://img.shields.io/travis/karson/mpesa-php-sdk/master.svg?style=flat-square)](https://travis-ci.org/karson/mpesa-php-sdk) 5 | [![Quality Score](https://img.shields.io/scrutinizer/g/karson/mpesa-php-sdk.svg?style=flat-square)](https://scrutinizer-ci.com/g/karson/mpesa-php-sdk) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/karson/mpesa-php-sdk.svg?style=flat-square)](https://packagist.org/packages/karson/mpesa-php-sdk) 7 | 8 | A comprehensive PHP SDK for integrating with M-Pesa Mozambique APIs. This package provides a clean, modern interface for all M-Pesa operations with robust error handling, callback processing, and extensive validation. 9 | 10 | ## Features 11 | 12 | - **🚀 Modern PHP 8.1+ Support**: Built with modern PHP features including typed properties, named arguments, and enums 13 | - **📦 Unified Response Architecture**: Streamlined response classes with eliminated code duplication (~90% reduction) 14 | - **🔄 Callback Handler System**: Complete callback processing with event-driven architecture (TODO) 15 | - **✅ Type Safety**: Strongly typed responses with specific methods for each operation 16 | - **🏗️ Clean Architecture**: Organized structure with dedicated response classes and clear inheritance 17 | - **🔐 Security First**: Built-in signature validation, parameter sanitization, and secure token management 18 | - **📊 Transaction Operations**: Full support for C2B, B2C, B2B transactions with sync/async modes 19 | - **🔍 Query Operations**: Transaction status queries and customer name lookups 20 | - **💰 Refund/Reversal**: Complete transaction reversal support with partial refund capabilities 21 | - **🎯 Smart Validation**: Comprehensive parameter validation with detailed error messages 22 | - **📱 Laravel Integration**: Native Laravel service provider with configuration publishing 23 | - **🔧 Developer Experience**: Extensive examples, comprehensive documentation, and debugging tools 24 | 25 | ## Installation 26 | 27 | You can install the package via composer: 28 | 29 | ```bash 30 | composer require karson/mpesa-php-sdk 31 | ``` 32 | 33 | ## Usage 34 | 35 | ### Basic Setup 36 | 37 | ```php 38 | use Karson\MpesaPhpSdk\Mpesa; 39 | 40 | // Initialize with your credentials from M-Pesa Developer Console (https://developer.mpesa.vm.co.mz/) 41 | $mpesa = new Mpesa( 42 | publicKey: 'your_public_key', 43 | apiKey: 'your_api_key', 44 | isTest: true, // false for production 45 | serviceProviderCode: '171717' // Your service provider code 46 | ); 47 | ``` 48 | 49 | ### Customer to Business (C2B) Transactions 50 | 51 | ```php 52 | // C2B Transaction (Unified API) 53 | $response = $mpesa->c2b( 54 | transactionReference: 'TXN001', 55 | from: '258841234567', 56 | amount: 100, 57 | thirdPartReference: 'REF001' 58 | ); 59 | 60 | if ($response->isTransactionSuccessful()) { 61 | echo "Transaction ID: " . $response->getTransactionId(); 62 | echo "Conversation ID: " . $response->getConversationId(); 63 | echo "Third Party Reference: " . $response->getThirdPartyReference(); 64 | } else { 65 | echo "Error: " . $response->getResponseDescription(); 66 | echo "Error Code: " . $response->getResponseCode(); 67 | } 68 | 69 | // Check transaction status 70 | if ($response->isTransactionInitiated()) { 71 | echo "Transaction initiated. Use Conversation ID for status tracking."; 72 | } 73 | ``` 74 | 75 | ### Business to Customer (B2C) Transactions 76 | 77 | ```php 78 | // B2C Transaction (Unified API) 79 | $response = $mpesa->b2c( 80 | customerMSISDN: '258841234567', 81 | amount: 100, 82 | transactionReference: 'TXN002', 83 | thirdPartReference: 'REF002' 84 | ); 85 | 86 | if ($response->isTransactionSuccessful()) { 87 | echo "Transaction ID: " . $response->getTransactionId(); 88 | echo "Conversation ID: " . $response->getConversationId(); 89 | echo "Third Party Reference: " . $response->getThirdPartyReference(); 90 | } else { 91 | echo "Error: " . $response->getResponseDescription(); 92 | } 93 | ``` 94 | 95 | ### Transaction Status Query 96 | 97 | ```php 98 | $response = $mpesa->status( 99 | thirdPartyReference: 'REF001', 100 | queryReference: 'QUERY001' 101 | ); 102 | 103 | // Access status information 104 | echo "Transaction Status: " . $response->getTransactionStatus(); 105 | echo "Amount: " . $response->getAmount(); 106 | echo "Currency: " . $response->getCurrency(); 107 | ``` 108 | 109 | ### Customer Name Query 110 | 111 | ```php 112 | $response = $mpesa->queryCustomerName( 113 | customerMSISDN: '258841234567', 114 | thirdPartyReference: 'REF003' 115 | ); 116 | 117 | if ($response->isSuccessful()) { 118 | echo "Customer Name: " . $response->getCustomerName(); 119 | echo "First Name: " . $response->getFirstName(); 120 | echo "Last Name: " . $response->getLastName(); 121 | } 122 | ``` 123 | 124 | ### Business to Business (B2B) Transactions 125 | 126 | ```php 127 | // B2B Transaction (Unified API) 128 | $response = $mpesa->b2b( 129 | transactionReference: 'TXN003', 130 | amount: 100, 131 | thirdPartReference: 'REF003', 132 | primaryPartyCode: '171717', // Sender business code 133 | receiverPartyCode: '979797' // Receiver business code 134 | ); 135 | 136 | if ($response->isTransactionSuccessful()) { 137 | echo "B2B Transaction ID: " . $response->getTransactionId(); 138 | echo "Conversation ID: " . $response->getConversationId(); 139 | echo "Third Party Reference: " . $response->getThirdPartyReference(); 140 | } else { 141 | echo "Error: " . $response->getResponseDescription(); 142 | } 143 | ``` 144 | 145 | ### Transaction Refund/Reversal 146 | 147 | ```php 148 | $response = $mpesa->reversal( 149 | transactionID: 'TXN123456', 150 | securityCredential: 'your_security_credential', 151 | initiatorIdentifier: 'your_initiator', 152 | thirdPartyReference: 'REF004', 153 | reversalAmount: '50' // Optional: partial refund 154 | ); 155 | 156 | if ($response->isReversalSuccessful()) { 157 | echo "Refund Transaction ID: " . $response->getReversalTransactionId(); 158 | echo "Refund Amount: " . $response->getReversalAmount(); 159 | 160 | if ($response->isPartialReversal()) { 161 | echo "This was a partial refund"; 162 | } 163 | } else { 164 | echo "Refund failed: " . $response->getResponseDescription(); 165 | } 166 | ``` 167 | 168 | 169 | ### Response Objects 170 | 171 | All methods return strongly typed response objects based on a unified architecture: 172 | 173 | #### BaseResponse (Unified Response Class) 174 | 175 | All transaction responses now inherit from `BaseResponse` with common methods: 176 | 177 | ```php 178 | // Common methods available on all responses 179 | $response->getTransactionId(); // Transaction ID 180 | $response->getConversationId(); // Conversation ID for tracking 181 | $response->getResponseCode(); // M-Pesa response code 182 | $response->getResponseDescription(); // Response description 183 | $response->getThirdPartyReference(); // Third party reference 184 | $response->isTransactionSuccessful(); // Check if transaction succeeded 185 | $response->isTransactionInitiated(); // Check if transaction was initiated 186 | $response->getStatusCode(); // HTTP status code 187 | $response->getRawResponse(); // Raw API response 188 | $response->isSuccessful(); // HTTP success check 189 | $response->isApiSuccess(); // M-Pesa API success check 190 | ``` 191 | 192 | #### Transaction Response Classes 193 | 194 | - **TransactionResponse**: Unified response for C2B, B2C, B2B transactions 195 | - **TransactionStatusResponse**: For transaction status queries 196 | - **CustomerNameResponse**: For customer name lookups 197 | - **ReversalResponse**: For refund/reversal operations 198 | 199 | #### Transaction Status Response Methods 200 | - `getTransactionId()`: Get the transaction ID 201 | - `getConversationId()`: Get the conversation ID 202 | - `getTransactionStatus()`: Get the current transaction status 203 | - `getAmount()`: Get the transaction amount 204 | - `getCurrency()`: Get the transaction currency 205 | - `getReceiverParty()`: Get the receiver party information 206 | - `getTransactionCompletedDateTime()`: Get completion timestamp 207 | - `isTransactionCompleted()`: Check if transaction is completed 208 | - `isTransactionPending()`: Check if transaction is pending 209 | - `isTransactionFailed()`: Check if transaction failed 210 | 211 | #### Customer Name Response Methods 212 | - `getCustomerMSISDN()`: Get the customer phone number 213 | - `getFirstName()`: Get the customer's first name (obfuscated) 214 | - `getSecondName()`: Get the customer's second name (obfuscated) 215 | - `getCustomerName()`: Get the full customer name 216 | - `isCustomerFound()`: Check if customer was found 217 | 218 | #### Refund/Reversal Response Methods 219 | - `getTransactionId()`: Get the original transaction ID 220 | - `getReversalTransactionId()`: Get the reversal transaction ID 221 | - `getReversalAmount()`: Get the reversed amount (for partial reversals) 222 | - `getConversationId()`: Get the conversation ID 223 | - `isReversalSuccessful()`: Check if reversal was successful 224 | - `isPartialReversal()`: Check if it was a partial reversal 225 | 226 | #### Response Class Architecture 227 | 228 | The SDK v2.0 features a completely refactored response architecture that eliminates ~90% of code duplication: 229 | 230 | ``` 231 | BaseResponse (unified base class) 232 | ├── TransactionResponse (unified for C2B, B2C, B2B) 233 | ├── TransactionStatusResponse (transaction status queries) 234 | ├── CustomerNameResponse (customer name lookups) 235 | └── ReversalResponse (refund/reversal operations) 236 | ``` 237 | 238 | **Key Improvements:** 239 | - **Unified API**: All transaction types (C2B, B2C, B2B) now return the same `TransactionResponse` class 240 | - **Eliminated Duplication**: Removed redundant sync/async response classes 241 | - **Better Type Safety**: Specific methods for each response type with proper return types 242 | - **Consistent Interface**: All responses share common methods from `BaseResponse` 243 | - **Simplified Usage**: No need to remember different method names for different transaction types 244 | 245 | ## Project Structure 246 | 247 | The SDK v2.0 features a streamlined, organized structure: 248 | 249 | ``` 250 | src/ 251 | ├── Mpesa.php # Main SDK class with unified API 252 | ├── Auth/ # Authentication management 253 | │ └── TokenManager.php # Token generation and caching 254 | ├── Constants/ # API constants and enums 255 | │ ├── ResponseCodes.php # Response code constants 256 | │ └── TransactionStatus.php # Transaction status constants 257 | ├── Validation/ # Parameter validation 258 | │ └── ParameterValidator.php # Input validation utilities 259 | ├── Exceptions/ # Custom exceptions 260 | │ ├── ValidationException.php 261 | │ ├── AuthenticationException.php 262 | │ ├── ApiException.php 263 | │ └── CallbackException.php 264 | ├── Providers/ 265 | │ └── ServiceProvider.php # Laravel service provider 266 | ├── config/ 267 | │ └── mpesa.php # Configuration file 268 | ├── Callback/ # Callback handling system (NEW) 269 | │ ├── CallbackHandler.php # Main callback processor 270 | │ └── Events/ # Callback event classes 271 | │ ├── CallbackEvent.php # Base event class 272 | │ ├── TransactionCompletedEvent.php 273 | │ └── TransactionFailedEvent.php 274 | └── response/ # Unified response classes 275 | ├── BaseResponse.php # Unified base response 276 | ├── TransactionResponse.php # For C2B, B2C, B2B transactions 277 | ├── TransactionStatusResponse.php 278 | ├── CustomerNameResponse.php 279 | └── ReversalResponse.php 280 | ``` 281 | 282 | **Key Architectural Improvements:** 283 | - **Unified Response System**: Single `TransactionResponse` class for all transaction types 284 | - **Callback Handler System**: Complete event-driven callback processing 285 | - **Streamlined Structure**: Eliminated redundant directories and classes 286 | - **Better Organization**: Clear separation of concerns with dedicated folders 287 | - **Enhanced Security**: Comprehensive exception handling and validation 288 | 289 | ## API Reference 290 | 291 | For detailed API documentation including all endpoints, request/response parameters, and examples, see the [API Documentation](API.md). 292 | 293 | ## Laravel Integration 294 | 295 | ### Installation in Laravel 296 | 297 | Add the following environment variables to your `.env` file: 298 | 299 | ```env 300 | MPESA_API_KEY="Your API Key" 301 | MPESA_PUBLIC_KEY="Your Public Key" 302 | MPESA_ENV=test # 'live' for production environment 303 | MPESA_SERVICE_PROVIDER_CODE=171717 304 | ``` 305 | 306 | ### Service Provider Registration 307 | 308 | The package includes a Laravel service provider that automatically registers the M-Pesa service. You can inject it into your controllers: 309 | 310 | ```php 311 | use Karson\MpesaPhpSdk\Mpesa; 312 | use Karson\MpesaPhpSdk\Callback\CallbackHandler; 313 | 314 | class PaymentController extends Controller 315 | { 316 | public function __construct( 317 | private Mpesa $mpesa, 318 | private CallbackHandler $callbackHandler 319 | ) { 320 | } 321 | 322 | public function processPayment(Request $request) 323 | { 324 | $response = $this->mpesa->c2b( 325 | transactionReference: $request->transaction_ref, 326 | from: $request->phone_number, 327 | amount: $request->amount, 328 | thirdPartReference: $request->reference 329 | ); 330 | 331 | if ($response->isTransactionSuccessful()) { 332 | // Handle successful payment 333 | return response()->json([ 334 | 'success' => true, 335 | 'transaction_id' => $response->getTransactionId(), 336 | 'conversation_id' => $response->getConversationId(), 337 | 'third_party_reference' => $response->getThirdPartyReference() 338 | ]); 339 | } 340 | 341 | return response()->json([ 342 | 'success' => false, 343 | 'message' => $response->getResponseDescription(), 344 | 'error_code' => $response->getResponseCode() 345 | ], 400); 346 | } 347 | 348 | public function handleCallback(Request $request) 349 | { 350 | 351 | $response = $request->getContent(), 352 | 353 | ... 354 | 355 | } 356 | } 357 | ``` 358 | 359 | ## Error Handling 360 | 361 | All response objects provide comprehensive error information with the unified API: 362 | 363 | ```php 364 | $response = $mpesa->c2b('TXN001', '258841234567', 100, 'REF001'); 365 | 366 | // Check HTTP status 367 | if (!$response->isSuccessful()) { 368 | echo "HTTP Error: " . $response->getStatusCode(); 369 | } 370 | 371 | // Check transaction status 372 | if (!$response->isTransactionSuccessful()) { 373 | echo "Transaction Error: " . $response->getResponseDescription(); 374 | echo "Error Code: " . $response->getResponseCode(); 375 | } 376 | 377 | // Check if transaction was initiated (for async processing) 378 | if ($response->isTransactionInitiated()) { 379 | echo "Transaction initiated. Conversation ID: " . $response->getConversationId(); 380 | } 381 | 382 | // Get raw response for debugging 383 | var_dump($response->getRawResponse()); 384 | ``` 385 | 386 | ## Response Structure 387 | 388 | ### Unified Transaction Responses 389 | 390 | All transaction methods (C2B, B2C, B2B) now return a unified `TransactionResponse` object: 391 | 392 | ```php 393 | // All transaction types use the same response structure 394 | $c2bResponse = $mpesa->c2b('TXN001', '258841234567', 100, 'REF001'); 395 | $b2cResponse = $mpesa->b2c('258841234567', 100, 'TXN002', 'REF002'); 396 | $b2bResponse = $mpesa->b2b('TXN003', 100, 'REF003', '171717', '979797'); 397 | 398 | // All responses have the same methods available 399 | foreach ([$c2bResponse, $b2cResponse, $b2bResponse] as $response) { 400 | if ($response->isTransactionSuccessful()) { 401 | echo "Transaction ID: " . $response->getTransactionId(); 402 | echo "Conversation ID: " . $response->getConversationId(); 403 | echo "Third Party Reference: " . $response->getThirdPartyReference(); 404 | } 405 | } 406 | ``` 407 | 408 | ### Transaction Status Tracking 409 | 410 | Use the conversation ID to track transaction status: 411 | 412 | ```php 413 | // Initiate transaction 414 | $response = $mpesa->c2b('TXN001', '258841234567', 100, 'REF001'); 415 | 416 | if ($response->isTransactionInitiated()) { 417 | $conversationId = $response->getConversationId(); 418 | 419 | // Later, check the status 420 | $statusResponse = $mpesa->queryTransactionStatus('REF001', 'QUERY001'); 421 | 422 | if ($statusResponse->isTransactionCompleted()) { 423 | echo "Transaction completed successfully"; 424 | echo "Amount: " . $statusResponse->getAmount(); 425 | echo "Currency: " . $statusResponse->getCurrency(); 426 | } elseif ($statusResponse->isTransactionPending()) { 427 | echo "Transaction is still pending"; 428 | } elseif ($statusResponse->isTransactionFailed()) { 429 | echo "Transaction failed"; 430 | } 431 | } 432 | ``` 433 | 434 | ## Exception Handling 435 | 436 | The SDK provides comprehensive error handling with custom exceptions: 437 | 438 | ```php 439 | use Karson\MpesaPhpSdk\Exceptions\ValidationException; 440 | use Karson\MpesaPhpSdk\Exceptions\AuthenticationException; 441 | use Karson\MpesaPhpSdk\Exceptions\ApiException; 442 | use Karson\MpesaPhpSdk\Exceptions\CallbackException; 443 | 444 | try { 445 | $response = $mpesa->c2b('TXN001', '258841234567', 100, 'REF001'); 446 | 447 | } catch (ValidationException $e) { 448 | echo "Validation Error: " . $e->getMessage(); 449 | foreach ($e->getErrors() as $error) { 450 | echo "- " . $error . "\n"; 451 | } 452 | 453 | } catch (AuthenticationException $e) { 454 | echo "Authentication failed: " . $e->getMessage(); 455 | 456 | } catch (ApiException $e) { 457 | echo "API Error: " . $e->getMessage(); 458 | echo "Response Code: " . $e->getResponseCode(); 459 | echo "Response Description: " . $e->getResponseDescription(); 460 | 461 | } catch (CallbackException $e) { 462 | echo "Callback Error: " . $e->getMessage(); 463 | if ($e->getCallbackData()) { 464 | echo "Callback Data: " . json_encode($e->getCallbackData()); 465 | } 466 | 467 | } catch (Exception $e) { 468 | echo "General Error: " . $e->getMessage(); 469 | } 470 | ``` 471 | 472 | ### Exception Types 473 | 474 | - **ValidationException**: Thrown when input parameters fail validation 475 | - **AuthenticationException**: Thrown when API authentication fails 476 | - **ApiException**: Thrown when M-Pesa API returns an error 477 | 478 | ## Token Management 479 | 480 | The SDK includes intelligent token management with caching and optimization: 481 | 482 | ```php 483 | // Get token manager 484 | $tokenManager = $mpesa->getTokenManager(); 485 | 486 | // Get token (generated automatically if needed) 487 | $token = $mpesa->getToken(); 488 | echo "Token: " . substr($token, 0, 20) . "..."; 489 | 490 | // Clear stored token 491 | $tokenManager->clearToken(); 492 | ``` 493 | 494 | ### Token Management Features 495 | 496 | - **Automatic Generation**: Tokens are generated automatically when needed 497 | - **Smart Reuse**: Existing tokens are reused to improve performance 498 | - **Manual Control**: Clear tokens when needed 499 | - **Thread Safe**: Safe for use in concurrent environments 500 | 501 | ### Best Practices 502 | 503 | ```php 504 | // Get token (automatically generated if needed) 505 | $token = $mpesa->getToken(); 506 | 507 | // For long-running processes, clear and regenerate periodically 508 | while ($process->isRunning()) { 509 | // Clear token periodically (e.g., every hour) to force regeneration 510 | if ($shouldRefreshToken) { 511 | $tokenManager->clearToken(); 512 | } 513 | 514 | // Your API calls here (token will be generated automatically if needed) 515 | $response = $mpesa->receive(...); 516 | 517 | sleep(60); 518 | } 519 | ``` 520 | 521 | ## Parameter Validation 522 | 523 | The SDK includes built-in parameter validation to ensure data integrity: 524 | 525 | ```php 526 | use Karson\MpesaPhpSdk\Validation\ParameterValidator; 527 | 528 | // Validate MSISDN format 529 | if (!ParameterValidator::validateMSISDN('258841234567')) { 530 | echo "Invalid phone number format"; 531 | } 532 | 533 | // Validate transaction parameters 534 | $params = [ 535 | 'transactionReference' => 'TXN001', 536 | 'customerMSISDN' => '258841234567', 537 | 'amount' => 100, 538 | 'thirdPartyReference' => 'REF001', 539 | 'serviceProviderCode' => '171717' 540 | ]; 541 | 542 | $errors = ParameterValidator::validateC2BParameters($params); 543 | if (!empty($errors)) { 544 | foreach ($errors as $error) { 545 | echo "Validation Error: " . $error . "\n"; 546 | } 547 | } 548 | ``` 549 | 550 | ## Response Code Constants 551 | 552 | Use predefined constants for consistent response handling: 553 | 554 | ```php 555 | use Karson\MpesaPhpSdk\Constants\ResponseCodes; 556 | use Karson\MpesaPhpSdk\Constants\TransactionStatus; 557 | 558 | $response = $mpesa->receive('TXN001', '258841234567', 100, 'REF001'); 559 | 560 | // Check using constants 561 | if ($response->getResponseCode() === ResponseCodes::SUCCESS) { 562 | echo "Transaction successful!"; 563 | } 564 | 565 | // Check transaction status using constants 566 | $statusResponse = $mpesa->status('REF001', 'QUERY001'); 567 | if (TransactionStatus::isCompleted($statusResponse->getTransactionStatus())) { 568 | echo "Transaction completed successfully"; 569 | } elseif (TransactionStatus::isPending($statusResponse->getTransactionStatus())) { 570 | echo "Transaction is still pending"; 571 | } elseif (TransactionStatus::isFailed($statusResponse->getTransactionStatus())) { 572 | echo "Transaction failed"; 573 | } 574 | ``` 575 | 576 | ## Testing 577 | 578 | The SDK includes comprehensive unit tests to ensure reliability: 579 | 580 | ```bash 581 | # Run all tests 582 | composer test 583 | 584 | # Run tests with coverage 585 | ./vendor/bin/phpunit --coverage-html coverage 586 | 587 | # Run specific test suite 588 | ./vendor/bin/phpunit tests/Unit/Constants/ 589 | ./vendor/bin/phpunit tests/Unit/Validation/ 590 | ``` 591 | 592 | ### Test Coverage 593 | 594 | The test suite covers: 595 | - **Constants**: Response codes and transaction status validation 596 | - **Validation**: Parameter validation for all transaction types 597 | - **Authentication**: Token management and expiration 598 | - **Response Classes**: All response parsing and methods 599 | - **Exceptions**: Custom exception handling 600 | 601 | ### Running Tests 602 | 603 | ```bash 604 | # Install dependencies 605 | composer install 606 | 607 | # Run tests 608 | ./vendor/bin/phpunit 609 | 610 | # Run with verbose output 611 | ./vendor/bin/phpunit --verbose 612 | 613 | # Run specific test 614 | ./vendor/bin/phpunit tests/Unit/Constants/ResponseCodesTest.php 615 | ``` 616 | 617 | ## What's New in v2.0 618 | 619 | ### 🚀 Major Improvements 620 | 621 | - **Unified API**: All transaction methods (C2B, B2C, B2B) now return the same `TransactionResponse` class 622 | - **Eliminated Code Duplication**: ~90% reduction in response class code through unified architecture 623 | - **Enhanced Type Safety**: Better IDE support with specific typed methods 624 | - **Improved Performance**: Optimized response parsing and memory usage 625 | - **Better Security**: Enhanced validation, signature verification, and error handling 626 | 627 | ### 🔄 Breaking Changes 628 | 629 | #### Response Classes Unified 630 | ```php 631 | // Before v2.0 (multiple response classes) 632 | $c2bResponse = $mpesa->c2b(...); // Returns C2BSyncResponse 633 | $b2cResponse = $mpesa->b2c(...); // Returns B2CSyncResponse 634 | $b2bResponse = $mpesa->b2b(...); // Returns B2BSyncResponse 635 | 636 | // v2.0+ (unified response) 637 | $c2bResponse = $mpesa->c2b(...); // Returns TransactionResponse 638 | $b2cResponse = $mpesa->b2c(...); // Returns TransactionResponse 639 | $b2bResponse = $mpesa->b2b(...); // Returns TransactionResponse 640 | ``` 641 | 642 | #### Removed getData() Method 643 | ```php 644 | // Before v2.0 645 | $transactionId = $response->getData()['output_TransactionID']; 646 | 647 | // v2.0+ (better type safety) 648 | $transactionId = $response->getTransactionId(); 649 | ``` 650 | 651 | ### 📦 New Features 652 | 653 | 654 | #### Enhanced Response Methods 655 | ```php 656 | // New methods available on all responses 657 | $response->getThirdPartyReference(); // Available on all transaction responses 658 | $response->isTransactionInitiated(); // Check if async transaction started 659 | $response->isApiSuccess(); // Check M-Pesa API success 660 | ``` 661 | 662 | 663 | ### Changelog 664 | 665 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 666 | 667 | ## Contributing 668 | 669 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 670 | 671 | ### Security 672 | 673 | If you discover any security related issues, please email karson@turbohost.co instead of using the issue tracker. 674 | 675 | ## Credits 676 | 677 | - [Karson Adam](https://github.com/karson) 678 | - [All Contributors](../../contributors) 679 | 680 | ## License 681 | 682 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 683 | 684 | ## PHP Package Boilerplate 685 | 686 | This package was generated using the [PHP Package Boilerplate](https://laravelpackageboilerplate.com). 687 | --------------------------------------------------------------------------------