├── .gitignore ├── Procfile ├── README.md ├── app.py ├── requirements.txt ├── runtime.txt └── uwsgi.ini /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: uwsgi uwsgi.ini -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # STK push with Daraja API using Python 2 | This is a simple tutorial on how to use the Lipa na M-Pesa Online Payment API to make an STK push and prompt users to make payments through M-pesa (C2B payments) using **python** **Flask** and **Flask-RESTful**. It makes the payment process easy for customers by reducing the burden of having to remember the Paybill Number, account number and amount of the transaction in question. This is done by prompting users only to enter their M-pesa PIN. 3 | Lipa na M-Pesa Online Payment API is used to initiate a M-Pesa transaction on behalf of a customer using STK Push 4 | 5 | ### Steps of involved 6 | Thanks to [Peter Njeru](https://peternjeru.co.ke) for this explanation. 7 | 8 | 1. The Business sets the data in the request and sends it 9 | 2. The API receives the request and validates it internally first, then sends you an acknowledgement response. 10 | 3. The API then sends an STK Push request to the target customer's mobile phone. The customer's phone has to be online and unlocked to receive the request. 11 | 4. The customer confirms the payment amount via the message displayed on-screen, then either enters the PIN or cancels the request accordingly. 12 | 5. The API receives the customer's response. If the response is a negative, it cancels the transaction and sends a corresponding callback to the initiating 3rd party via the predefined callback URL in the initial request, with the info on why the transaction was cancelled. The possible negative responses could be due to the following scenarios: 13 | * An invalid PIN entered by the customer 14 | * Timeout due to customer not entering the PIN within a given time period (usually 1 min 30 secs) 15 | * The customer's SIM card not having the STK applet on it 16 | * A literal request cancellation by the user on their phone 17 | * Another STK transaction is already underway on the customer's phone (no more than one request can be processed at the same time on the same phone) 18 | 19 | 6. If the PIN is correct, it means the customer accepted the request. The API forwards the transaction to M-Pesa. 20 | 7. M-Pesa automatically processes the request, then sends the response back to the API system which then forwards it to you via the callback URL specified in your initial request. Here, the callback can also either be a success or failure, just like a normal C2B transaction. 21 | 8. There are no repeat calls for failed callbacks, thus if the API is unable to send the callback to you, you have the Transaction Status Query API to confirm the status of the request, or also confirm via the M-Pesa Org. portal. 22 | 23 | ## Getting a Daraja App 24 | The fisrt step is to go to [Safaricom Developer's Website](https://developer.safaricom.co.ke) and get an account. 25 | 26 | #### Creating a sandbox app 27 | Got to [Creating sandbox apps](https://developer.safaricom.co.ke/user/me/apps). 28 | 29 | Creating an app involves setting an app name and choosing the API products for which you will want to use. API products are a business package of the available APIs and the rules for access built around them. 30 | For this case make sure to use Lipa na Mpesa online. 31 | 32 | #### Consumer key and consumer secret 33 | 34 | Go your dashboard and select your new app. 35 | The Keys tab shows the app keys (**CONSUMER_KEY** & **CONSUMER_SECRET**), the date they were issued and their expiry period. Sandbox keys do not expire. 36 | 37 | 38 | ## Making the request 39 | 40 | #### Authentication 41 | Before you make any request you have to get an *access_token* to authenticate your app. You do this by making an OAuth API request to generate an *access_token* for your app. 42 | 43 | You will need a Basic Auth over HTTPS authorization token. 44 | ```python 45 | 46 | consumer_key = "YOUR_APP_CONSUMER_KEY" 47 | consumer_secret = "YOUR_APP_CONSUMER_SECRET" 48 | api_URL = "https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials" 49 | 50 | # make a get request using python requests liblary 51 | r = requests.get(api_URL, auth=HTTPBasicAuth(consumer_key, consumer_secret)) 52 | 53 | ``` 54 | Make the request and get your *access_token* from the response body 55 | ```pyhton 56 | access_token = r.json()['access_token'] 57 | 58 | ``` 59 | 60 | #### Request parameters 61 | 62 | | **Parameter** | **Decription** | **Example** | 63 | |------------------- |----------------------------------------------------------------------------------------------------------------------------------------------- |--------------------------------------------------------------------------------- | 64 | | BusinessShortCode | The organization shortcode used to receive the transaction. | 174379 | 65 | | Pasword | The password for encrypting the request. This is generated by base64 encoding BusinessShortcode, Passkey and Timestamp. | base64.b64encode( " " ) | 66 | | Timestamp | The timestamp of the transaction in the format yyyymmddhhiiss. | 20190617113403 | 67 | | TransactionType | The transaction type to be used for this request. Only CustomerPayBillOnline is supported. | "CustomerPayBillOnline" | 68 | | Amount | The amount to be transacted. | Integer | 69 | | PartyB | Same as businessShortCode | 174379 | 70 | | Party A | The phone number sending the funds. | 254721345678 | 71 | | PhoneNumber | The phone number sending the funds. | Same as Party A | 72 | | CallBackURL | The url to where responses from M-Pesa will be sent to. Expose an end point in your API to confirm transaction status [Completed or Failed] | POST https://myapi.com/payments/confirmation | 73 | | AccountReference | Used with M-Pesa PayBills. | Payvill Account Number | 74 | | TransactionDesc | A description of the transaction | Any String | 75 | 76 | #### Response Parameters 77 | 78 | | **Parameter** | **Description** | 79 | |--------------------- |------------------------------------------------------------------------------------------ | 80 | | MerchantRequestID | Merchant Request ID | 81 | | CheckoutRequestID | Check out Request ID | 82 | | ResponseCode | [Response Code](https://developer.safaricom.co.ke/docs#m-pesa-result-and-response-codes) | 83 | | ResultDesc | Result description | 84 | | ResponseDescription | Response Description message | 85 | | ResultCode | Result Code | 86 | ##### example of a response after a successfull STK push request 87 | ```json 88 | { 89 | "MerchantRequestID": "25353-1377561-4", 90 | "CheckoutRequestID": "ws_CO_26032018185226297", 91 | "ResponseCode": "0", 92 | "ResponseDescription": "Success. Request accepted for processing", 93 | "CustomerMessage": "Success. Request accepted for processing" 94 | } 95 | ``` 96 | 97 | ## API endpoint for CallBackURL 98 | All M-Pesa APIs work asynchronously. When you send a request for a transaction, it is added to a queue and after its processed, M-Pesa sends the response to the registered *CallBackURL*. This is where you check if the payment was successfull or not. 99 | 100 | **CheckoutRequestID** is used to identify each transaction. For example if you are dealing with orders you can mark the order as pending when initializing the STK push and upon recieving a response(*in the CallBackURL*) showing the transaction was successfull, you the mark the order as fully paid (Each order will have a unique *CheckoutRequestID*) 101 | 102 | To receive responses, either M-Pesa results or queue timeouts, an HTTP listener will be needed. 103 | 104 | For example, if you have an endpoint on your API which you resgistered as your *CallBackURL* https://myapi.com/payments/confirmation 105 | 106 | After a successfull transaction, M-Pesa will send a request to your *CallBackURL* with the following body. 107 | 108 | ```json 109 | { 110 | "Body": 111 | { 112 | "stkCallback": 113 | { 114 | "MerchantRequestID": "21605-295434-4", 115 | "CheckoutRequestID": "ws_CO_04112017184930742", 116 | "ResultCode": 0, 117 | "ResultDesc": "The service request is processed successfully.", 118 | "CallbackMetadata": 119 | { 120 | "Item": 121 | [ 122 | { 123 | "Name": "Amount", 124 | "Value": 1 125 | }, 126 | { 127 | "Name": "MpesaReceiptNumber", 128 | "Value": "LK451H35OP" 129 | }, 130 | { 131 | "Name": "Balance" 132 | }, 133 | { 134 | "Name": "TransactionDate", 135 | "Value": 20171104184944 136 | }, 137 | { 138 | "Name": "PhoneNumber", 139 | "Value": 254727894083 140 | } 141 | ] 142 | } 143 | } 144 | } 145 | } 146 | 147 | ``` 148 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_restful import Api, Resource, reqparse 3 | import datetime 4 | import requests 5 | from requests.auth import HTTPBasicAuth 6 | import base64 7 | import json 8 | 9 | # get Oauth token from M-pesa [function] 10 | def get_mpesa_token(): 11 | 12 | consumer_key = "YOUR_APP_CONSUMER_KEY" 13 | consumer_secret = "YOUR_APP_CONSUMER_SECRET" 14 | api_URL = "https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials" 15 | 16 | # make a get request using python requests liblary 17 | r = requests.get(api_URL, auth=HTTPBasicAuth(consumer_key, consumer_secret)) 18 | 19 | # return access_token from response 20 | return r.json()['access_token'] 21 | 22 | 23 | # initialize a flask app 24 | app = Flask(__name__) 25 | 26 | # intialize a flask-restful api 27 | api = Api(app) 28 | 29 | class MakeSTKPush(Resource): 30 | 31 | # get 'phone' and 'amount' from request body 32 | parser = reqparse.RequestParser() 33 | parser.add_argument('phone', 34 | type=str, 35 | required=True, 36 | help="This fied is required") 37 | 38 | parser.add_argument('amount', 39 | type=str, 40 | required=True, 41 | help="this fied is required") 42 | 43 | # make stkPush method 44 | def post(self): 45 | 46 | """ make and stk push to daraja API""" 47 | 48 | encode_data = b"" 49 | 50 | # encode business_shortcode, online_passkey and current_time (yyyyMMhhmmss) to base64 51 | passkey = base64.b64encode(encode_data) 52 | 53 | # make stk push 54 | try: 55 | 56 | # get access_token 57 | access_token = get_mpesa_token() 58 | 59 | # stk_push request url 60 | api_url = "https://sandbox.safaricom.co.ke/mpesa/stkpush/v1/processrequest" 61 | 62 | # put access_token in request headers 63 | headers = { "Authorization": f"Bearer {access_token}" ,"Content-Type": "application/json" } 64 | 65 | # get phone and amount 66 | data = MakeSTKPush.parser.parse_args() 67 | 68 | # define request body 69 | request = { 70 | "BusinessShortCode": "", 71 | "Password": str(passkey)[2:-1], 72 | "Timestamp": "", # timestamp format: 20190317202903 yyyyMMhhmmss 73 | "TransactionType": "CustomerPayBillOnline", 74 | "Amount": data['amount'], 75 | "PartyA": data['phone'], 76 | "PartyB": "", 77 | "PhoneNumber": data['phone'], 78 | "CallBackURL": "", 79 | "AccountReference": "UNIQUE_REFERENCE", 80 | "TransactionDesc": "" 81 | } 82 | 83 | # make request and catch response 84 | response = requests.post(api_url,json=request,headers=headers) 85 | 86 | # check response code for errors and return response 87 | if response.status_code > 299: 88 | return{ 89 | "success": False, 90 | "message":"Sorry, something went wrong please try again later." 91 | },400 92 | 93 | # CheckoutRequestID = response.text['CheckoutRequestID'] 94 | 95 | # Do something in your database e.g store the transaction or as an order 96 | # make sure to store the CheckoutRequestID to identify the tranaction in 97 | # your CallBackURL endpoint. 98 | 99 | # return a respone to your user 100 | return { 101 | "data": json.loads(response.text) 102 | },200 103 | 104 | except: 105 | # catch error and return respones 106 | 107 | return { 108 | "success":False, 109 | "message":"Sorry something went wrong please try again." 110 | },400 111 | 112 | 113 | # stk push path [POST request to {baseURL}/stkpush] 114 | api.add_resource(MakeSTKPush,"/stkpush") 115 | 116 | if __name__ == "__main__": 117 | 118 | app.run(port=5000,debug=True) 119 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | Flask-RESTful 3 | requests 4 | uwsgi -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.6.7 -------------------------------------------------------------------------------- /uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | http-socket = :$(PORT) 3 | master = true 4 | die-on-term = true 5 | module = app:app 6 | memory-report = true --------------------------------------------------------------------------------