M-Pesa Integration in Django: STK Push & C2B
NOTE: This is a production-ready integration
Integrating Safaricom's M-Pesa API into a Django application can feel intimidating. You have to handle access tokens, base64 encoding, asynchronous callbacks, and strict validation rules.
In this guide, we will build a robust, production-ready Mpesa integration that supports both STK Push (prompting the user's phone directly) and C2B (Customer-to-Business, where the user manually pays via a Paybill/Till number).
Let's break it down step-by-step.
Step 1: Centralizing Your Configurations
First, we need a clean way to handle credentials and generate passwords. Create a file named mpesa_config.py. We will use standard Django settings to keep our actual keys secure.
# mpesa_config.py
import base64
import json
import requests
from datetime import datetime
from requests.auth import HTTPBasicAuth
from django.conf import settings
class MpesaConfig:
consumer_key = settings.MPESA_CONSUMER_KEY
consumer_secret = settings.MPESA_CONSUMER_SECRET
business_short_code = settings.MPESA_SHORTCODE
passkey = settings.MPESA_PASSKEY
# Use sandbox URLs for testing, api.safaricom.co.ke for production
auth_url = 'https://api.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials'
stk_url = "https://api.safaricom.co.ke/mpesa/stkpush/v1/processrequest"
c2b_register_url = "https://api.safaricom.co.ke/mpesa/c2b/v2/registerurl"
domain = "https://www.yourdomain.com"
stk_callback_url = f"{domain}/payments/mpesa-callback/"
c2b_validation_url = f"{domain}/payments/c2b/validation/"
c2b_confirmation_url = f"{domain}/payments/c2b/confirmation/"
class MpesaAuth:
@staticmethod
def get_access_token():
try:
r = requests.get(
MpesaConfig.auth_url,
auth=HTTPBasicAuth(MpesaConfig.consumer_key, MpesaConfig.consumer_secret),
timeout=10
)
r.raise_for_status()
return r.json()["access_token"]
except requests.exceptions.RequestException as e:
print(f"Error fetching access token: {e}")
return None
class MpesaPassword:
@staticmethod
def generate():
timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
data_to_encode = MpesaConfig.business_short_code + MpesaConfig.passkey + timestamp
encoded_password = base64.b64encode(data_to_encode.encode()).decode('utf-8')
return encoded_password, timestampStep 2: The API Utilities (The Engine)
Next, create mpesa_utils.py. This file handles the actual HTTP requests to Safaricom, one function to trigger the STK push, and another to register our C2B webhooks.
# mpesa_utils.py
import requests
from .mpesa_config import MpesaConfig, MpesaAuth, MpesaPassword
def trigger_stk_push(phone_number, amount, order_reference):
# Clean phone number to start with 254
if phone_number.startswith('+'):
phone_number = phone_number[1:]
if phone_number.startswith('0'):
phone_number = '254' + phone_number[1:]
access_token = MpesaAuth.get_access_token()
password, timestamp = MpesaPassword.generate()
headers = {"Authorization": f"Bearer {access_token}"}
payload = {
"BusinessShortCode": MpesaConfig.business_short_code,
"Password": password,
"Timestamp": timestamp,
"TransactionType": "CustomerPayBillOnline",
"Amount": int(float(amount)),
"PartyA": phone_number,
"PartyB": MpesaConfig.business_short_code,
"PhoneNumber": phone_number,
"CallBackURL": MpesaConfig.stk_callback_url,
"AccountReference": str(order_reference),
"TransactionDesc": f"Payment for Order {order_reference}"
}
response = requests.post(MpesaConfig.stk_url, json=payload, headers=headers, timeout=30)
response.raise_for_status()
return response.json() # Returns CheckoutRequestID and MerchantRequestID
def register_c2b_urls():
access_token = MpesaAuth.get_access_token()
if not access_token:
return {"error": "Failed to get access token"}
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
payload = {
"ShortCode": MpesaConfig.business_short_code,
"ResponseType": "Cancelled",
"ConfirmationURL": MpesaConfig.c2b_confirmation_url,
"ValidationURL": MpesaConfig.c2b_validation_url
}
response = requests.post(MpesaConfig.c2b_register_url, json=payload, headers=headers)
return response.json()
Step 3: Handling STK Push in Views
Now we move to views.py. We need a view to initiate the payment via an AJAX request from the frontend, and a webhook to receive the result from Safaricom.
# views.py
import json
from datetime import datetime
from django.utils.timezone import make_aware
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.decorators import login_required
from .mpesa_utils import trigger_stk_push
from .models import Order, PaymentTransaction
@login_required
def initiate_mpesa_payment(request, order_id):
order = Order.objects.get(pk=order_id)
if request.method == 'POST':
data = json.loads(request.body)
phone_number = data.get('phone_number')
try:
response = trigger_stk_push(phone_number, order.amount, order.reference_number)
# Save pending transaction to DB
PaymentTransaction.objects.create(
order=order,
amount=order.amount,
phone_number=phone_number,
checkout_request_id=response.get('CheckoutRequestID'),
is_complete=False
)
return JsonResponse({
'success': True,
'message': "STK push sent! Please check your phone.",
'checkout_request_id': response.get('CheckoutRequestID')
})
except Exception as e:
return JsonResponse({'success': False, 'message': str(e)}, status=500)
@csrf_exempt
def stk_callback(request):
data = json.loads(request.body)
callback = data.get('Body', {}).get('stkCallback', {})
result_code = callback.get('ResultCode', 1)
checkout_id = callback.get('CheckoutRequestID', '')
try:
tx = PaymentTransaction.objects.get(checkout_request_id=checkout_id)
except PaymentTransaction.DoesNotExist:
return JsonResponse({'ResultCode': 1, 'ResultDesc': 'Transaction not found.'})
if result_code != 0:
tx.is_complete = False
tx.save()
return JsonResponse({'ResultCode': result_code, 'ResultDesc': 'Failed/Cancelled'})
# Extract successful payment data
items = callback.get('CallbackMetadata', {}).get('Item', [])
for item in items:
if item.get('Name') == 'MpesaReceiptNumber':
tx.mpesa_receipt_number = item.get('Value')
tx.is_complete = True
tx.save()
# Mark order as paid
tx.order.status = 'PAID'
tx.order.save()
return JsonResponse({'ResultCode': 0, 'ResultDesc': 'Accepted'})
Step 4: Handling C2B Webhooks (Validation & Confirmation)
Sometimes STK push fails (network issues, timeouts), so users must pay manually via Paybill. Safaricom will ping your Validation URL to ask "Does this account exist?" and then your Confirmation URL to say "Here is the money."
# views.py (continued)
@csrf_exempt
def c2b_validation(request):
data = json.loads(request.body)
bill_ref_number = data.get('BillRefNumber', '').strip().upper()
trans_amount = data.get('TransAmount')
try:
order = Order.objects.get(reference_number=bill_ref_number)
# Optional: Validate Amount
if float(trans_amount) < float(order.amount):
return JsonResponse({
"ResultCode": "C2B00013",
"ResultDesc": "Rejected: Amount is less than required."
})
return JsonResponse({"ResultCode": "0", "ResultDesc": "Accepted"})
except Order.DoesNotExist:
return JsonResponse({
"ResultCode": "C2B00012",
"ResultDesc": "Rejected: Invalid Account Number."
})
@csrf_exempt
def c2b_confirmation(request):
data = json.loads(request.body)
trans_id = str(data.get('TransID', ''))
trans_amount = data.get('TransAmount')
bill_ref_number = data.get('BillRefNumber', '').strip().upper()
msisdn = str(data.get('MSISDN', ''))
# Prevent duplicate processing
if PaymentTransaction.objects.filter(mpesa_receipt_number=trans_id).exists():
return JsonResponse({"ResultCode": "0", "ResultDesc": "Already processed"})
try:
order = Order.objects.get(reference_number=bill_ref_number)
# Create completed transaction record
PaymentTransaction.objects.create(
order=order,
amount=trans_amount,
phone_number=msisdn,
mpesa_receipt_number=trans_id,
is_complete=True
)
order.status = 'PAID'
order.save()
return JsonResponse({"ResultCode": "0", "ResultDesc": "Processed Successfully"})
except Exception as e:
# Always return success to Safaricom to stop them from retrying infinitely,
# but log the error on your side.
print(f"C2B Error: {e}")
return JsonResponse({"ResultCode": "0", "ResultDesc": "Error Handled"})
Step 5: Wiring the URLs
Finally, expose these views in your urls.py.
# urls.py
from django.urls import path
from . import views
urlpatterns = [
# STK Push URLs
path('initiate-payment/<int:order_id>/', views.initiate_mpesa_payment, name='initiate_payment'),
path('mpesa-callback/', views.stk_callback, name='mpesa_callback'),
# C2B URLs
path('c2b/validation/', views.c2b_validation, name='c2b_validation'),
path('c2b/confirmation/', views.c2b_confirmation, name='c2b_confirmation'),
# Admin helper to register C2B URLs
path('c2b/register-urls/', views.register_urls_view, name='c2b_register_urls'),
]
Conclusion
By separating your configuration, API logic, and views, your codebase remains clean and easy to debug. Remember to always catch Exceptions in your callbacks, if your Django server throws a 500 Internal Server Error back to Safaricom, they will continually retry the webhook, which can clutter your logs and cause duplicate data.
Always test thoroughly in the Safaricom Sandbox before pushing to production. Happy coding!
Want More Tutorials Like This? Let’s Connect!
I hope this guide helped demystify M-Pesa integration in Django for you. Dealing with payment gateways can be frustrating at first, but once you understand the flow of STK Pushes and C2B callbacks, it becomes incredibly powerful for your web applications.
If you found this tutorial valuable and want to level up your programming skills, I’d love for you to join my developer community. I regularly share practical coding tips, project walkthroughs, and insights into software development.
You can find me and follow my journey here:
- 📺 Subscribe on YouTube: For full video tutorials, system design breakdowns, and deep dives into Python and Django. Click here to subscribe
- 📱 Follow on TikTok: For quick, bite-sized coding tips, debugging hacks, and a behind-the-scenes look at my day-to-day life as a developer. Follow me here
- 📘 Connect on Facebook: Join the conversation, ask questions about this tutorial, and interact with other developers. Like the page here
If you use this code in your project, shoot me a message on any of these platforms, I'd love to see what you build! Drop a comment on my latest video or post and let me know what topic you want me to cover next.