├── .env.example ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── index.js ├── package.json └── test └── test.js /.env.example: -------------------------------------------------------------------------------- 1 | MPESA_PUBLIC_KEY= 2 | MPESA_API_HOST= 3 | MPESA_API_KEY= 4 | MPESA_ORIGIN= 5 | MPESA_SERVICE_PROVIDER_CODE= 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | .env 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Version 1.1.0 4 | 5 | FEATURE: allow initializing the API with custom configuration. 6 | 7 | ## Version 1.0.1 8 | 9 | Initial Release 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | We love contributions! But we'd like you to follow these guidelines to make the contribution process 4 | easy for everyone involved: 5 | 6 | ## Issue Tracker 7 | 8 | The issue tracker is our main channel to report a bug in the source code or a mistake in the 9 | documentation, and to request a new feature. 10 | 11 | Before submitting your issue, please search the archive. Maybe your question was already answered, 12 | your bug has already been reported or your feature has already been requested. 13 | 14 | Providing the following information will increase the chances of your issue being dealt with 15 | quickly: 16 | 17 | * **Bug reports** - if an error is being thrown, please include a stack trace and the steps to 18 | reproduce the error. If there is no error, please explain why do you consider it a bug. 19 | 20 | * **Feature Requests** - please make it clear whether you're willing to write the code for it or you 21 | need someone else to do it. 22 | 23 | * **Related Issues** - if you found a similar issue that has been reported before, be sure to 24 | mention it. 25 | 26 | ## Pull Requests 27 | 28 | Before making any changes, consider following these steps: 29 | 30 | 1. Search for an open or closed Pull Request related to your changes. 31 | 32 | 2. Search the issue tracker for issues related to your changes. 33 | 34 | 3. Open a [new issue](github.com/rosariopfernandes/mpesa-node-api/issues/new) to discuss your changes 35 | with the project owners. If they approve it, send the Pull Request. 36 | 37 | ### Sending Pull Requests 38 | If your change has been approved, follow this process: 39 | 40 | 1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork and configure the 41 | remotes: 42 | 43 | ```bash 44 | # Clone your fork into the current directory 45 | git clone https://github.com// 46 | # Navigate to the newly cloned directory 47 | cd 48 | # Assign the original repo to a remote called "upstream" 49 | git remote add upstream https://github.com/rosariopfernandes/mpesa-node-api 50 | ``` 51 | 52 | 2. Make your changes in a new branch: 53 | 54 | ```bash 55 | git checkout -b my-fix-branch master 56 | ``` 57 | 58 | 3. Commit the changes using a descriptive commit message 59 | 60 | ```bash 61 | git commit -a 62 | ``` 63 | 64 | Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files. 65 | 66 | 4. Push your branch to GitHub: 67 | 68 | ```bash 69 | git push origin my-fix-branch 70 | ``` 71 | 72 | 5. In GitHub, [send a Pull Request](https://help.github.com/articles/using-pull-requests/) with a 73 | clear title and description. 74 | 75 | * If we suggest changes then: 76 | * Make the required changes; 77 | * Rebase your branch and force push to your GitHub repository (this updates your Pull Request): 78 | ```bash 79 | # Rebase the branch 80 | git rebase master -i 81 | # Update the Pull Request 82 | git push origin my-fix-branch -f 83 | ``` 84 | That's it! Thank you for you contribution! 85 | 86 | ### After your Pull Request is merged 87 | 88 | You can delete your branch and pull changes from the original 89 | (upstream) repository: 90 | 91 | 1. Delete the remote branch on GitHub either through the GitHub UI or your local shell as follows: 92 | 93 | ```bash 94 | git push origin --delete my-fix-branch 95 | ``` 96 | 97 | 2. Check out the master branch: 98 | 99 | ```bash 100 | git checkout master -f 101 | ``` 102 | 103 | 3. Delete the local branch: 104 | 105 | ```bash 106 | git branch -D my-fix-branch 107 | ``` 108 | 109 | 4. Update your master with the latest upstream version: 110 | 111 | ```bash 112 | git pull --ff upstream master 113 | ``` 114 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Rosário Pereira Fernandes 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # M-Pesa Node API 2 | 3 | Node.js wrapper for the M-Pesa Mozambique API. 4 | 5 | ## Using the API 6 | 7 | 1. Install it using npm: 8 | ```shell 9 | npm install mpesa-node-api 10 | ``` 11 | 12 | 1. Create the configuration `.env` file on your root directory based on [`.env.example`](.env.example). 13 | 14 | 1. Use your favorite text editor to edit the `.env` file and fill in the blank lines with configuration 15 | you got from the [M-Pesa Developer Portal](https://developer.mpesa.vm.co.mz/). See an example: 16 | ```shell 17 | MPESA_PUBLIC_KEY=example_public_key 18 | MPESA_API_HOST=api.sandbox.vm.co.mz 19 | MPESA_API_KEY=example_api_key 20 | MPESA_ORIGIN=developer.mpesa.vm.co.mz 21 | MPESA_SERVICE_PROVIDER_CODE=171717 22 | ``` 23 | 24 | 1. In your JavaScript file, import the package using `require()`: 25 | ```js 26 | const mpesa = require('mpesa-node-api'); 27 | ``` 28 | 29 | ### Supported Transactions 30 | 31 | All transactions return a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). 32 | So you can either use `then/catch` or `async/await` to get the transaction response. 33 | 34 | Responses match the ones specified on the [M-Pesa API Documentation](https://developer.mpesa.vm.co.mz/apis/). 35 | 36 | Example C2B response: 37 | 38 | ```json 39 | { 40 | "output_ConversationID": "f02f957c19f4499faf6a6f19c0307e69", 41 | "output_ResponseCode": "INS-0", 42 | "output_ResponseDesc": "Request processed successfully", 43 | "output_ThirdPartyReference": "ZXVM9H", 44 | "output_TransactionID": "f449abol7j38" 45 | } 46 | ``` 47 | 48 | #### Customer to Business (C2B) 49 | ```js 50 | const mpesa = require('mpesa-node-api'); 51 | 52 | mpesa.initiate_c2b(/* amount */ 10, /* msisdn */ 258843330333, /* transaction ref */ 'T12344C', /*3rd party ref*/ 'ref1') 53 | .then(function(response) { 54 | // logging the response 55 | console.log(response); 56 | }) 57 | .catch(function(error) { 58 | // TODO: handle errors 59 | }); 60 | ``` 61 | 62 | #### Business to Customer (B2C) 63 | ```js 64 | const mpesa = require('mpesa-node-api'); 65 | 66 | mpesa.initiate_b2c(/* amount */ 10, /* msisdn */ 258843330333, /* transaction ref */ 'T12344C', /*3rd party ref*/ 'ref1') 67 | .then(function(response) { 68 | // logging the response 69 | console.log(response); 70 | }) 71 | .catch(function(error) { 72 | // TODO: handle errors 73 | }); 74 | ``` 75 | 76 | ### Planned Support: 77 | - [ ] B2B 78 | - [ ] Reversal 79 | - [ ] Query Transaction Status 80 | 81 | ### Using custom configuration 82 | 83 | Optionally, you can also use custom configuration to initialize the API: 84 | 85 | ```js 86 | const mpesa = require('mpesa-node-api'); 87 | 88 | mpesa.initializeApi({ 89 | baseUrl: "YOUR_MPESA_API_HOST", 90 | apiKey: "YOUR_MPESA_API_KEY", 91 | publicKey: "YOUR_MPESA_PUBLIC_KEY", 92 | origin: "YOUR_MPESA_ORIGIN", 93 | serviceProviderCode: "YOUR_MPESA_SERVICE_PROVIDER_CODE" 94 | }); 95 | mpesa.initiate_c2b( 10, 258843330333, 'T12344C', 'ref1'); 96 | ``` 97 | 98 | ## Getting a copy for development 99 | 100 | These instructions will get you a copy of the project up and running on 101 | your local machine for development and testing purposes. 102 | 103 | ### Prerequisites 104 | 105 | Make sure you have installed [Node.js](https://nodejs.org/en/), which comes with `npm`. 106 | 107 | ### Installing 108 | 109 | 1. Fork the GitHub repository. 110 | 1. Clone it to your local machine using `git clone https://github.com//mpesa-node-api.git` 111 | 1. Navigate into the project's directory using `cd mpesa-node-api`; 112 | 1. Install dependencies using `npm run install`. 113 | 114 | ## Contributing 115 | 116 | Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on the process 117 | for submitting pull requests to us. 118 | 119 | ## License 120 | 121 | This project is licensed under the [MIT License](LICENSE) - see the [LICENSE](LICENSE) file for 122 | details. 123 | 124 | ## Acknowledgments 125 | 126 | Inspired by the [mpesa-php-api](https://github.com/abdulmueid/mpesa-php-api) created by 127 | [Abdul Mueid](https://github.com/abdulmueid/). 128 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const axios = require('axios').default; 3 | const crypto = require('crypto'); 4 | const constants = require('constants'); 5 | 6 | let mpesaConfig; 7 | 8 | function _getBearerToken(mpesa_public_key, mpesa_api_key) { 9 | const publicKey = "-----BEGIN PUBLIC KEY-----\n"+mpesa_public_key+"\n"+"-----END PUBLIC KEY-----"; 10 | const buffer = Buffer.from(mpesa_api_key); 11 | const encrypted = crypto.publicEncrypt({ 12 | 'key': publicKey, 13 | 'padding': constants.RSA_PKCS1_PADDING, 14 | }, buffer); 15 | return encrypted.toString("base64"); 16 | } 17 | 18 | function initialize_api_from_dotenv() { 19 | if (!mpesaConfig) { 20 | mpesaConfig = { 21 | baseUrl: process.env.MPESA_API_HOST, 22 | apiKey: process.env.MPESA_API_KEY, 23 | publicKey: process.env.MPESA_PUBLIC_KEY, 24 | origin: process.env.MPESA_ORIGIN, 25 | serviceProviderCode: process.env.MPESA_SERVICE_PROVIDER_CODE 26 | }; 27 | validateConfig(mpesaConfig); 28 | console.log("Using M-Pesa environment configuration"); 29 | } else { 30 | console.log("Using custom M-Pesa configuration"); 31 | } 32 | } 33 | 34 | function required_config_arg(argName) { 35 | return "Please provide a valid " + argName + " in the configuration when calling initializeApi()"; 36 | } 37 | 38 | function validateConfig(configParams) { 39 | if (!configParams.baseUrl) { 40 | throw required_config_arg("baseUrl") 41 | } 42 | if (!configParams.apiKey) { 43 | throw required_config_arg("apiKey") 44 | } 45 | if (!configParams.publicKey) { 46 | throw required_config_arg("publicKey") 47 | } 48 | if (!configParams.origin) { 49 | throw required_config_arg("origin") 50 | } 51 | if (!configParams.serviceProviderCode) { 52 | throw required_config_arg("serviceProviderCode") 53 | } 54 | } 55 | 56 | module.exports.initializeApi = function (configParams) { 57 | validateConfig(configParams); 58 | mpesaConfig = configParams; 59 | }; 60 | 61 | module.exports.initiate_c2b = async function (amount, msisdn, transaction_ref, thirdparty_ref) { 62 | initialize_api_from_dotenv(); 63 | try { 64 | let response; 65 | response = await axios({ 66 | method: 'post', 67 | url: 'https://' + mpesaConfig.baseUrl + ':18352/ipg/v1x/c2bPayment/singleStage/', 68 | headers: { 69 | 'Content-Type': 'application/json', 70 | 'Authorization': 'Bearer ' + _getBearerToken(mpesaConfig.publicKey, mpesaConfig.apiKey), 71 | 'Origin': mpesaConfig.origin 72 | }, 73 | data: { 74 | "input_TransactionReference": transaction_ref, 75 | "input_CustomerMSISDN": msisdn + "", 76 | "input_Amount": amount + "", 77 | "input_ThirdPartyReference": thirdparty_ref, 78 | "input_ServiceProviderCode": mpesaConfig.serviceProviderCode + "" 79 | } 80 | }); 81 | return response.data; 82 | } catch (e) { 83 | if (e.response.data) { 84 | throw e.response.data; 85 | } else { 86 | throw e; 87 | } 88 | } 89 | }; 90 | 91 | module.exports.initiate_b2c = async function (amount, msisdn, transaction_ref, thirdparty_ref) { 92 | initialize_api_from_dotenv(); 93 | try { 94 | let response; 95 | response = await axios({ 96 | method: 'post', 97 | url: 'https://' + mpesaConfig.baseUrl + ':18345/ipg/v1x/b2cPayment/', 98 | headers: { 99 | 'Content-Type': 'application/json', 100 | 'Authorization': 'Bearer ' + _getBearerToken(mpesaConfig.publicKey, mpesaConfig.apiKey), 101 | 'Origin': mpesaConfig.origin 102 | }, 103 | data: { 104 | "input_TransactionReference": transaction_ref, 105 | "input_CustomerMSISDN": msisdn + "", 106 | "input_Amount": amount + "", 107 | "input_ThirdPartyReference": thirdparty_ref, 108 | "input_ServiceProviderCode": mpesaConfig.serviceProviderCode + "" 109 | } 110 | }); 111 | return response.data; 112 | } catch (e) { 113 | if (e.response.data) { 114 | throw e.response.data; 115 | } else { 116 | throw e; 117 | } 118 | } 119 | }; 120 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mpesa-node-api", 3 | "version": "1.1.0", 4 | "description": "Node.js library for M-Pesa API (Mozambique)", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/rosariopfernandes/mpesa-node-api.git" 12 | }, 13 | "keywords": [ 14 | "mpesa" 15 | ], 16 | "author": "rosariofernandes51@gmail.com", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/rosariopfernandes/mpesa-node-api/issues" 20 | }, 21 | "homepage": "https://github.com/rosariopfernandes/mpesa-node-api", 22 | "dependencies": { 23 | "axios": "^0.21.1", 24 | "constants": "0.0.2", 25 | "crypto": "^1.0.1", 26 | "dotenv": "^8.2.0" 27 | }, 28 | "devDependencies": { 29 | "chai": "^4.2.0", 30 | "mocha": "^8.0.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const mpesa = require('../index'); 2 | const expect = require('chai').expect; 3 | 4 | describe('config', function () { 5 | it('initializeApi throws an error when required config is missing', function () { 6 | const actualConfig = { 7 | baseUrl: "api.mpesa.co.mz" 8 | }; 9 | try { 10 | mpesa.initializeApi(actualConfig) 11 | } catch (e) { 12 | expect(e).to.equal('Please provide a valid apiKey in the configuration when calling initializeApi()') 13 | } 14 | }); 15 | 16 | it('initializeApi works with valid config', function () { 17 | const actualConfig = { 18 | baseUrl: "api.mpesa.co.mz", 19 | apiKey: "apiKey", 20 | publicKey: "key", 21 | origin: "developer.mpesa.co.mz", 22 | serviceProviderCode: 171717 23 | }; 24 | expect(function () { 25 | mpesa.initializeApi(actualConfig) 26 | }).to.not.throw(); 27 | }); 28 | }); 29 | --------------------------------------------------------------------------------