├── .gitignore ├── README.md ├── manage.py ├── mpesa_api ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── mpesa_credentials.py ├── tests.py ├── urls.py └── views.py └── mysite ├── __init__.py ├── settings.py ├── urls.py └── wsgi.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .idea/ 3 | db.sqlite3 4 | media/ 5 | academy/local_settings.py -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Step by Step Mpesa Integration Using Django 2.2 and Python 3.7 2 | Hey and welcome to this Python/Django tutorial series, my name is Henry Mbugua and I will be taking you through the various aspect and new answer of M-pesa Integration using Django web framework and Python 3.7. Django is a powerful Python web framework that can help you develop web applications quickly, from a simple prototype to large scale projects. Django will help you adopt a clean and pragmatic design using a comprehensive set of tools to build scalable web applications. 3 | 4 | ## Getting Started 5 | The following are the lessons covered in this tutorial series: 6 | * [Lesson 1](https://blog.hlab.tech/lesson-1-a-step-by-step-tutorial-on-how-to-integrate-to-mpesa-api-using-django-2-2-and-python-3-7/) - A Step by Step Tutorial on How to Integrate to Mpesa API Using Django 2.2 and Python 3.7 7 | * [Lesson 2](https://blog.hlab.tech/lesson-2-a-step-by-step-tutorial-on-how-to-generate-mpesa-access-token-using-django-2-2-and-python-3-7/) - A Step by Step Tutorial on How to Generate Mpesa Access Token Using Django 2.2 and Python 3.7 8 | * [Lesson 3](https://blog.hlab.tech/lesson-3-a-step-by-step-tutorial-on-how-to-do-stk-push-integration-to-m-pesa-on-daraja-using-django-2-2-and-python-3-7/) - A Step by Step Tutorial on How to do STK Push Integration To M-Pesa On Daraja Using Django 2.2 and Python 3.7 9 | * [Lesson 4](https://blog.hlab.tech/lesson-4-a-step-by-step-tutorial-on-how-to-develop-c2b-integration-to-m-pesa-on-daraja-using-django-2-2-and-python-3-7/) - A Step by Step Tutorial on How to develop C2B Integration To M-Pesa On Daraja Using Django 2.2 and Python 3.7 10 | * [Lesson 5](https://blog.hlab.tech/lesson-5-a-step-by-step-tutorial-on-how-to-register-confirmation-and-validation-urls-to-m-pesa-c2b-integration-on-daraja-using-django-2-2-and-python-3-7/) - A Step by Step Tutorial on How to Register Confirmation and Validation URL’s to M-Pesa C2B Integration on Daraja Using Django 2.2 and Python 3.7 11 | * [Lesson 6](https://blog.hlab.tech/lesson-6-a-step-by-step-tutorial-on-how-to-fix-bug-on-registering-confirmation-and-validation-urls-to-m-pesa-c2b-integration-on-daraja-using-django-2-2-and-python-3-7/) - Lesson 6: A Step by Step Tutorial on How to Fix Bug on Registering Confirmation and Validation URL’s to M-Pesa C2B Integration on Daraja Using Django 2.2 and Python 3.7 12 | 13 | ## Authors 14 | 15 | * **Henry Mbugua** - *Blog* - [Hlab Blog](https://blog.hlab.tech/) 16 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /mpesa_api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HenryLab/Python-Django-Mpesa-API-Integration/7b1b99e74756f3a48c15eb334dbe0218b2bd987f/mpesa_api/__init__.py -------------------------------------------------------------------------------- /mpesa_api/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import MpesaPayment 3 | 4 | 5 | admin.site.register(MpesaPayment) 6 | -------------------------------------------------------------------------------- /mpesa_api/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MpesaApiConfig(AppConfig): 5 | name = 'mpesa_api' 6 | -------------------------------------------------------------------------------- /mpesa_api/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.1 on 2019-09-08 13:14 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='MpesaCallBacks', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('created_at', models.DateTimeField(auto_now_add=True)), 19 | ('updated_at', models.DateTimeField(auto_now=True)), 20 | ('ip_address', models.TextField()), 21 | ('caller', models.TextField()), 22 | ('conversation_id', models.TextField()), 23 | ('content', models.TextField()), 24 | ], 25 | options={ 26 | 'verbose_name': 'Mpesa Call Back', 27 | 'verbose_name_plural': 'Mpesa Call Backs', 28 | }, 29 | ), 30 | migrations.CreateModel( 31 | name='MpesaCalls', 32 | fields=[ 33 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 34 | ('created_at', models.DateTimeField(auto_now_add=True)), 35 | ('updated_at', models.DateTimeField(auto_now=True)), 36 | ('ip_address', models.TextField()), 37 | ('caller', models.TextField()), 38 | ('conversation_id', models.TextField()), 39 | ('content', models.TextField()), 40 | ], 41 | options={ 42 | 'verbose_name': 'Mpesa Call', 43 | 'verbose_name_plural': 'Mpesa Calls', 44 | }, 45 | ), 46 | migrations.CreateModel( 47 | name='MpesaPayment', 48 | fields=[ 49 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 50 | ('created_at', models.DateTimeField(auto_now_add=True)), 51 | ('updated_at', models.DateTimeField(auto_now=True)), 52 | ('amount', models.DecimalField(decimal_places=2, max_digits=10)), 53 | ('description', models.TextField()), 54 | ('type', models.TextField()), 55 | ('reference', models.TextField()), 56 | ('first_name', models.CharField(max_length=100)), 57 | ('middle_name', models.CharField(max_length=100)), 58 | ('last_name', models.CharField(max_length=100)), 59 | ('phone_number', models.TextField()), 60 | ('organization_balance', models.DecimalField(decimal_places=2, max_digits=10)), 61 | ], 62 | options={ 63 | 'verbose_name': 'Mpesa Payment', 64 | 'verbose_name_plural': 'Mpesa Payments', 65 | }, 66 | ), 67 | ] 68 | -------------------------------------------------------------------------------- /mpesa_api/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HenryLab/Python-Django-Mpesa-API-Integration/7b1b99e74756f3a48c15eb334dbe0218b2bd987f/mpesa_api/migrations/__init__.py -------------------------------------------------------------------------------- /mpesa_api/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class BaseModel(models.Model): 5 | created_at = models.DateTimeField(auto_now_add=True) 6 | updated_at = models.DateTimeField(auto_now=True) 7 | 8 | class Meta: 9 | abstract = True 10 | 11 | 12 | # M-pesa Payment models 13 | 14 | class MpesaCalls(BaseModel): 15 | ip_address = models.TextField() 16 | caller = models.TextField() 17 | conversation_id = models.TextField() 18 | content = models.TextField() 19 | 20 | class Meta: 21 | verbose_name = 'Mpesa Call' 22 | verbose_name_plural = 'Mpesa Calls' 23 | 24 | 25 | class MpesaCallBacks(BaseModel): 26 | ip_address = models.TextField() 27 | caller = models.TextField() 28 | conversation_id = models.TextField() 29 | content = models.TextField() 30 | 31 | class Meta: 32 | verbose_name = 'Mpesa Call Back' 33 | verbose_name_plural = 'Mpesa Call Backs' 34 | 35 | 36 | class MpesaPayment(BaseModel): 37 | amount = models.DecimalField(max_digits=10, decimal_places=2) 38 | description = models.TextField() 39 | type = models.TextField() 40 | reference = models.TextField() 41 | first_name = models.CharField(max_length=100) 42 | middle_name = models.CharField(max_length=100) 43 | last_name = models.CharField(max_length=100) 44 | phone_number = models.TextField() 45 | organization_balance = models.DecimalField(max_digits=10, decimal_places=2) 46 | 47 | class Meta: 48 | verbose_name = 'Mpesa Payment' 49 | verbose_name_plural = 'Mpesa Payments' 50 | 51 | def __str__(self): 52 | return self.first_name 53 | 54 | -------------------------------------------------------------------------------- /mpesa_api/mpesa_credentials.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | from requests.auth import HTTPBasicAuth 4 | from datetime import datetime 5 | import base64 6 | 7 | 8 | class MpesaC2bCredential: 9 | consumer_key = 'cHnkwYIgBbrxlgBoneczmIJFXVm0oHky' 10 | consumer_secret = '2nHEyWSD4VjpNh2g' 11 | api_URL = 'https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials' 12 | 13 | 14 | class MpesaAccessToken: 15 | r = requests.get(MpesaC2bCredential.api_URL, 16 | auth=HTTPBasicAuth(MpesaC2bCredential.consumer_key, MpesaC2bCredential.consumer_secret)) 17 | mpesa_access_token = json.loads(r.text) 18 | validated_mpesa_access_token = mpesa_access_token['access_token'] 19 | 20 | 21 | class LipanaMpesaPpassword: 22 | lipa_time = datetime.now().strftime('%Y%m%d%H%M%S') 23 | Business_short_code = "174379" 24 | Test_c2b_shortcode = "600344" 25 | passkey = 'bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919' 26 | 27 | data_to_encode = Business_short_code + passkey + lipa_time 28 | 29 | online_password = base64.b64encode(data_to_encode.encode()) 30 | decode_password = online_password.decode('utf-8') 31 | 32 | -------------------------------------------------------------------------------- /mpesa_api/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /mpesa_api/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | 6 | urlpatterns = [ 7 | path('access/token', views.getAccessToken, name='get_mpesa_access_token'), 8 | path('online/lipa', views.lipa_na_mpesa_online, name='lipa_na_mpesa'), 9 | 10 | # register, confirmation, validation and callback urls 11 | path('c2b/register', views.register_urls, name="register_mpesa_validation"), 12 | path('c2b/confirmation', views.confirmation, name="confirmation"), 13 | path('c2b/validation', views.validation, name="validation"), 14 | path('c2b/callback', views.call_back, name="call_back"), 15 | 16 | ] 17 | -------------------------------------------------------------------------------- /mpesa_api/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse, JsonResponse 2 | import requests 3 | from requests.auth import HTTPBasicAuth 4 | import json 5 | from . mpesa_credentials import MpesaAccessToken, LipanaMpesaPpassword 6 | from django.views.decorators.csrf import csrf_exempt 7 | from .models import MpesaPayment 8 | 9 | 10 | def getAccessToken(request): 11 | consumer_key = 'cHnkwYIgBbrxlgBoneczmIJFXVm0oHky' 12 | consumer_secret = '2nHEyWSD4VjpNh2g' 13 | api_URL = 'https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials' 14 | 15 | r = requests.get(api_URL, auth=HTTPBasicAuth(consumer_key, consumer_secret)) 16 | mpesa_access_token = json.loads(r.text) 17 | validated_mpesa_access_token = mpesa_access_token['access_token'] 18 | 19 | return HttpResponse(validated_mpesa_access_token) 20 | 21 | 22 | def lipa_na_mpesa_online(request): 23 | access_token = MpesaAccessToken.validated_mpesa_access_token 24 | api_url = "https://sandbox.safaricom.co.ke/mpesa/stkpush/v1/processrequest" 25 | headers = {"Authorization": "Bearer %s" % access_token} 26 | request = { 27 | "BusinessShortCode": LipanaMpesaPpassword.Business_short_code, 28 | "Password": LipanaMpesaPpassword.decode_password, 29 | "Timestamp": LipanaMpesaPpassword.lipa_time, 30 | "TransactionType": "CustomerPayBillOnline", 31 | "Amount": 1, 32 | "PartyA": 254728851119, # replace with your phone number to get stk push 33 | "PartyB": LipanaMpesaPpassword.Business_short_code, 34 | "PhoneNumber": 254728851119, # replace with your phone number to get stk push 35 | "CallBackURL": "https://sandbox.safaricom.co.ke/mpesa/", 36 | "AccountReference": "Henry", 37 | "TransactionDesc": "Testing stk push" 38 | } 39 | 40 | response = requests.post(api_url, json=request, headers=headers) 41 | return HttpResponse('success') 42 | 43 | 44 | @csrf_exempt 45 | def register_urls(request): 46 | access_token = MpesaAccessToken.validated_mpesa_access_token 47 | api_url = "https://sandbox.safaricom.co.ke/mpesa/c2b/v1/registerurl" 48 | headers = {"Authorization": "Bearer %s" % access_token} 49 | options = {"ShortCode": LipanaMpesaPpassword.Test_c2b_shortcode, 50 | "ResponseType": "Completed", 51 | "ConfirmationURL": "https://79372821.ngrok.io/api/v1/c2b/confirmation", 52 | "ValidationURL": "https://79372821.ngrok.io/api/v1/c2b/validation"} 53 | response = requests.post(api_url, json=options, headers=headers) 54 | 55 | return HttpResponse(response.text) 56 | 57 | 58 | @csrf_exempt 59 | def call_back(request): 60 | pass 61 | 62 | 63 | @csrf_exempt 64 | def validation(request): 65 | 66 | context = { 67 | "ResultCode": 0, 68 | "ResultDesc": "Accepted" 69 | } 70 | return JsonResponse(dict(context)) 71 | 72 | 73 | @csrf_exempt 74 | def confirmation(request): 75 | mpesa_body =request.body.decode('utf-8') 76 | mpesa_payment = json.loads(mpesa_body) 77 | 78 | payment = MpesaPayment( 79 | first_name=mpesa_payment['FirstName'], 80 | last_name=mpesa_payment['LastName'], 81 | middle_name=mpesa_payment['MiddleName'], 82 | description=mpesa_payment['TransID'], 83 | phone_number=mpesa_payment['MSISDN'], 84 | amount=mpesa_payment['TransAmount'], 85 | reference=mpesa_payment['BillRefNumber'], 86 | organization_balance=mpesa_payment['OrgAccountBalance'], 87 | type=mpesa_payment['TransactionType'], 88 | 89 | ) 90 | payment.save() 91 | 92 | context = { 93 | "ResultCode": 0, 94 | "ResultDesc": "Accepted" 95 | } 96 | 97 | return JsonResponse(dict(context)) 98 | 99 | -------------------------------------------------------------------------------- /mysite/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HenryLab/Python-Django-Mpesa-API-Integration/7b1b99e74756f3a48c15eb334dbe0218b2bd987f/mysite/__init__.py -------------------------------------------------------------------------------- /mysite/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for mysite project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.2.4. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.2/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'nl9&eqhh!092-vf*5#vrp8p8n=5^35=$0+8b&wh-sh&lino' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = ['79372821.ngrok.io', '127.0.0.1', 'localhost'] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'mpesa_api.apps.MpesaApiConfig', 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | ] 42 | 43 | MIDDLEWARE = [ 44 | 'django.middleware.security.SecurityMiddleware', 45 | 'django.contrib.sessions.middleware.SessionMiddleware', 46 | 'django.middleware.common.CommonMiddleware', 47 | 'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.messages.middleware.MessageMiddleware', 50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 51 | ] 52 | 53 | ROOT_URLCONF = 'mysite.urls' 54 | 55 | TEMPLATES = [ 56 | { 57 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 58 | 'DIRS': [], 59 | 'APP_DIRS': True, 60 | 'OPTIONS': { 61 | 'context_processors': [ 62 | 'django.template.context_processors.debug', 63 | 'django.template.context_processors.request', 64 | 'django.contrib.auth.context_processors.auth', 65 | 'django.contrib.messages.context_processors.messages', 66 | ], 67 | }, 68 | }, 69 | ] 70 | 71 | WSGI_APPLICATION = 'mysite.wsgi.application' 72 | 73 | 74 | # Database 75 | # https://docs.djangoproject.com/en/2.2/ref/settings/#databases 76 | 77 | DATABASES = { 78 | 'default': { 79 | 'ENGINE': 'django.db.backends.sqlite3', 80 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 81 | } 82 | } 83 | 84 | 85 | # Password validation 86 | # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators 87 | 88 | AUTH_PASSWORD_VALIDATORS = [ 89 | { 90 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 91 | }, 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 94 | }, 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 100 | }, 101 | ] 102 | 103 | 104 | # Internationalization 105 | # https://docs.djangoproject.com/en/2.2/topics/i18n/ 106 | 107 | LANGUAGE_CODE = 'en-us' 108 | 109 | TIME_ZONE = 'UTC' 110 | 111 | USE_I18N = True 112 | 113 | USE_L10N = True 114 | 115 | USE_TZ = True 116 | 117 | 118 | # Static files (CSS, JavaScript, Images) 119 | # https://docs.djangoproject.com/en/2.2/howto/static-files/ 120 | 121 | STATIC_URL = '/static/' 122 | 123 | 124 | import sentry_sdk 125 | from sentry_sdk.integrations.django import DjangoIntegration 126 | 127 | sentry_sdk.init( 128 | dsn='https://6c4aee19c2f24687885882e015c9de13@sentry.io/1777995', 129 | integrations=[DjangoIntegration()] 130 | ) 131 | -------------------------------------------------------------------------------- /mysite/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path, include 3 | 4 | 5 | def trigger_error(request): 6 | division_by_zero = 300 / 0 7 | 8 | 9 | urlpatterns = [ 10 | path('api/v1/', include('mpesa_api.urls')), 11 | path('admin/', admin.site.urls), 12 | path('sentry-debug', trigger_error), 13 | ] 14 | -------------------------------------------------------------------------------- /mysite/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for mysite project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') 15 | 16 | application = get_wsgi_application() 17 | --------------------------------------------------------------------------------