DEV Community

Joy Nyayieka
Joy Nyayieka

Posted on

Integrating Pesapal API 3.0 on Django

Pesapal is a fintech company that provides secure and convenient digital payment services to individuals and businesses in African countries, including Kenya, Uganda, Tanzania, Malawi, Rwanda, Zambia, and Zimbabwe. I was recently working on a Django project that required integrating the Pesapal API to facilitate seamless transactions within the application. The first step was to review the Pesapal API documentation; however, upon further research, it became unclear how exactly to implement this integration into the project. This article aims to provide a more detailed explanation of how to implement the basic steps specifically within a Django project.

1. Set Up Pesapal Account

  • Navigate to the Pesapal Developers Portal.
  • Create a live/production business account.
  • You will then receive the Consumer Key and Consumer Secret on the registered email.
  • Ensure your Django project is set up.

2. Install Required Dependencies

Create a requirements.txt file and install the following dependencies:

requests==2.32.4             # HTTP calls to Pesapal API
python-decouple==3.8         # Manage API keys & secrets via .env
djangorestframework==3.16.0  # Build API endpoints for Pesapal integration

Enter fullscreen mode Exit fullscreen mode

3. Configure Environment Variables

Create a .env file in your project root:

# PesaPal Configuration
PESAPAL_CONSUMER_KEY=your_consumer_key_here
PESAPAL_CONSUMER_SECRET=your_consumer_secret_here
PESAPAL_BASE_URL=https://cybqa.pesapal.com/pesapalv3/api/  # Sandbox
# PESAPAL_BASE_URL=https://pay.pesapal.com/pesapalv3/api/  # Production
PESAPAL_IPN_ID=your_ipn_id_here  # Will be generated in Step 7
Enter fullscreen mode Exit fullscreen mode

And to your settings.py:

from decouple import config

# PesaPal Settings
PESAPAL_CONSUMER_KEY = config('PESAPAL_CONSUMER_KEY')
PESAPAL_CONSUMER_SECRET = config('PESAPAL_CONSUMER_SECRET')
PESAPAL_BASE_URL = config('PESAPAL_BASE_URL')
PESAPAL_IPN_ID = config('PESAPAL_IPN_ID', default='')
Enter fullscreen mode Exit fullscreen mode

4. Create pesapal_service.py in the Main App

Create the file inside your main app, eg:

Purpose of pesapal_service.py

This file is the core integration layer between the Django application layer and the Pesapal API. It contains all the functions required to:

a. Authenticate with Pesapal.

  • Every request to Pesapal requires an OAuth access token.
  • This function calls the RequestToken endpoint and returns a token that is valid for a limited time.
def generate_access_token():
    url = f"{BASE_URL}/api/Auth/RequestToken"
    payload = {
        "consumer_key": settings.PESAPAL_CONSUMER_KEY,
        "consumer_secret": settings.PESAPAL_CONSUMER_SECRET
    }
    headers = {
        "Accept": "application/json",
        "Content-Type": "application/json"
    }
    response = requests.post(url, json=payload, headers=headers)
    return response.json().get("token") if response.status_code == 200 else None
Enter fullscreen mode Exit fullscreen mode

b. Registering IPN URL and fetching registered IPNs.

  • An IPN is an Instant Payment Notification that Pesapal uses to notify your system about payment status changes asynchronously.
  • The IPN URL is where Pesapal sends asynchronous updates.
  • Fetching registered IPNs verifies which IPNs are active.
def register_ipn_url(access_token, ipn_url):
    url = f"{BASE_URL}/api/URLSetup/RegisterIPN"
    payload = {
        "url": ipn_url,
        "ipn_notification_type": "GET"
    }
    headers = {
        "Authorization": f"Bearer {access_token}",
        "Accept": "application/json",
        "Content-Type": "application/json"
    }
    return requests.post(url, json=payload, headers=headers)

def get_registered_ipns(access_token):
    url = f"{BASE_URL}/api/URLSetup/GetIpnList"
    headers = {
        "Authorization": f"Bearer {access_token}",
        "Accept": "application/json",
        "Content-Type": "application/json"
    }
    return requests.get(url, headers=headers)
Enter fullscreen mode Exit fullscreen mode

c. Submitting an order.

  • Sends transaction details to Pesapal, eg, the amount and description, and returns a redirect URL where customers can complete the payment on Pesapal's secure checkout page.
def submit_order_request(access_token, payload):
    url = f"{BASE_URL}/api/Transactions/SubmitOrderRequest"
    headers = {
        "Authorization": f"Bearer {access_token}",
        "Accept": "application/json",
        "Content-Type": "application/json"
    }
    return requests.post(url, json=payload, headers=headers)
Enter fullscreen mode Exit fullscreen mode

d. Checking transaction status.

  • Important to confirm if a payment was successful before delivering a product or a service.
def get_transaction_status(access_token, tracking_id, merchant_reference):
    url = f"{BASE_URL}/api/Transactions/GetTransactionStatus"
    headers = {
        "Authorization": f"Bearer {access_token}",
        "Accept": "application/json"
    }
    params = {
        "order_tracking_id": tracking_id,
        "order_merchant_reference": merchant_reference
    }
    response = requests.get(url, headers=headers, params=params)

    if response.status_code == 200:
        return response.json().get("payment_status")  # "COMPLETED", "FAILED", etc.
    else:
        return None
Enter fullscreen mode Exit fullscreen mode

It is worth noting that to make the code above production-ready, consider;

  1. Token caching to avoid calling Pesapal for a new token on every request.
  2. Wrap with try/except to ensure error handling.
  3. Add docstrings to functions to ensure a clear description, args, and return values.

After receiving the payment details, store them in the system under the models.py file as in this example;

from django.db import models

class Payment(models.Model):
    order_id = models.CharField(max_length=100)
    amount = models.DecimalField(max_digits=10, decimal_places=2)
    status = models.CharField(max_length=50, default='PENDING')
    tracking_id = models.CharField(max_length=100, null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
Enter fullscreen mode Exit fullscreen mode

5. Create Payment Views

Here we define Django views that connect the project to the Pesapal service functions. These views are the entry points for the API consumers.

import json
import uuid
from django.http import JsonResponse, HttpResponse
from django.shortcuts import redirect, render
from django.views.decorators.csrf import csrf_exempt

# Import service functions
from .pesapal_service import (
    generate_access_token,
    register_ipn_url,
    get_registered_ipns,
    submit_order_request,
    get_transaction_status,
)


def get_token_view(request):
    """Generate and return a fresh Pesapal OAuth token (for testing/debugging)."""
    token = generate_access_token(force_refresh=True)
    return JsonResponse({"token": token})


def register_ipn_view(request):
    """Register an IPN URL with Pesapal."""
    ipn_url = request.GET.get("url")
    if not ipn_url:
        return JsonResponse({"error": "IPN URL is required"}, status=400)

    res = register_ipn_url(ipn_url)
    return JsonResponse(res)


def list_ipns_view(request):
    """Fetch all registered IPN URLs from Pesapal for verification."""
    res = get_registered_ipns()
    return JsonResponse(res, safe=False)


def create_order_view(request):
    """
    Create a new Pesapal order and return the redirect URL for checkout.
    Payload includes amount, currency, description, callback, IPN, and billing details.
    """
    # Get amount from request body (JSON or form POST)
    if request.content_type == "application/json":
        data = json.loads(request.body)
        amount = float(data.get("amount", 0))
    else:
        amount = float(request.POST.get("amount", 0))

    payload = {
        "id": str(uuid.uuid4()),  # Unique order ID
        "currency": "KES",
        "amount": amount,
        "description": "Payment description goes here",
        "callback_url": "https://yourdomain.com/pesapal/payment-confirm/",
        "notification_id": "YOUR_REGISTERED_IPN_ID",
        "branch": "Store Name - Example",
        "billing_address": {
            "email_address": "customer@example.com",
            "phone_number": "0723xxxxxx",
            "country_code": "KE",
            "first_name": "Juma",
            "last_name": "Mutua",
            "line_1": "Customer Address",
            "city": "Nairobi",
        },
    }

    res = submit_order_request(payload)

    if "redirect_url" in res:
        return JsonResponse({"redirect_url": res["redirect_url"]})
    else:
        return JsonResponse({"error": "Failed to create order"}, status=400)


@csrf_exempt
def ipn_listener(request):
    """
    Update your database with payment status asynchronously.
    """
    tracking_id = request.GET.get("tracking_id")
    merchant_reference = request.GET.get("merchant_reference")

    print(f"[IPN] Tracking ID: {tracking_id}, Reference: {merchant_reference}")
    return HttpResponse("IPN received", status=200)


def payment_confirm(request):
    """
    Callback endpoint: Pesapal redirects the customer here after payment.
    """
    tracking_id = request.GET.get("order_tracking_id")
    merchant_reference = request.GET.get("order_merchant_reference")

    status_response = get_transaction_status(tracking_id, merchant_reference)
    payment_status = status_response.get("payment_status")

    if payment_status == "COMPLETED":
        return redirect("dashboard")  # Example success page
    else:
        return render(request, "payments/payment-failed.html")
Enter fullscreen mode Exit fullscreen mode

The callback_url is the URL where a user of the system is redirected after a successful payment using Pesapal. During development, since the system is not live yet, you can make use of ngrok.

Ngrok is a reverse proxy that enables a developer to expose a local server running on their machine to the internet. This creates a secure tunnel, giving the local server a public URL that can receive webhooks or API callbacks from services like Pesapal. This is crucial because external APIs need a publicly accessible endpoint to send data back to, which they can't do to a local machine's address like http://localhost.

Once you create the endpoint, replace the callback_url with the valid URL. eg
"callback_url": "https://1234-567-890.ngrok-free.app/dashboard". The base URL should also be included in settings.py under the ALLOWED_HOSTS = [...].

6. Configure URLs

Under the Main app's urls.py(specific to this example tutorial), configure the URLs to connect the Pesapal payment views defined.

from django.urls import path
from . import views

urlpatterns = [
    # Utility views
    path("pesapal/token/", views.get_token_view, name="pesapal-get-token"),
    path("pesapal/ipn/register/", views.register_ipn_view, name="pesapal-register-ipn"),
    path("pesapal/ipn/list/", views.list_ipns_view, name="pesapal-list-ipns"),

    # Payment flow
    path("pesapal/order/create/", views.create_order_view, name="pesapal-create-order"),
    path("pesapal/ipn/", views.ipn_listener, name="pesapal-ipn-listener"),
    path("pesapal/payment-confirm/", views.payment_confirm, name="pesapal-payment-confirm"),
]
Enter fullscreen mode Exit fullscreen mode

7. Register IPN URL

Get the IPN ID by running https://yourdomain.com/pesapal/ipn/ and replacing the placeholder in the .env file.

8. Testing

Test the payment flow by checking whether the redirect works, the callback updates the status, and whether the IPN hits the endpoint. When moving to production, switch the API base URL to production and update the keys.

Before production, you might also consider ensuring security, such as using HTTPS, implementing proper error handling for error logging, and configuring a proper domain for callback URLs.


Above are the fundamental steps to integrate the Pesapal API into a Django project. Of course, Pesapal has many other services like handling recurring payments, refund request and order cancellation that are not included in the article but are equally useful (documentation).

I hope you have found this useful! I'd be happy to get your feedback on it. See you in the next one. 🫡

Top comments (0)