django-khalti-integration

Khalti Logo

Acknowledgements

Quick Start !

Integrating Khalti with Django is pretty simple! It might have some learning curve for individual developers but trust the process!


Installation

Initializing virtual env (Linux/Mac):

python3 -m venv .venv

Activating the env:

source .venv/bin/activate

Initializing virtual env (Windows):

python -m venv .venv

Activating the env:

source .venv/Scripts/activate

Install Django with pip:

pip install django

Check your Django Version:

django-admin --version

After that create your project:

django-admin startproject {project_name} .

Create your Django App:

django-admin startapp {app_name}

Configuring the Project

Adding installed apps in INSTALLED_APPS

Add your {app_name}, e.g. 'home':

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'home',
]
Adding installed Apps

Adding Routing

In {project_name}/urls.py, add:

from django.urls import path, include

Add this in urlpatterns:

path('', include('{app_name}.urls'))
Routing

For more detailed information visit the official Django Docs (v5.2).


Setting up the config for the app

Add urls.py in {app_name} folder.

Creating templates

Create a templates folder in {app_name}. Your folder structure should look like:

{app_name}/templates

Add home and payment folders with their respective HTML files:

{app_name}/templates/home/home.html
{app_name}/templates/payment/payment.html

Payment.html

payment.html preview

Variable names:


Database — Storing User Payment Data

We'll use a Custom User model via AbstractUser and UUID to generate unique IDs.

Adding Custom User Auth Model

In {project_name}/settings.py:

AUTH_USER_MODEL = "home.Custom_user"
Custom user model

Importing necessary packages in {app_name}/models.py:

from django.db import models
from django.contrib.auth.models import AbstractUser
import uuid
Import model.py

Custom User Model:

class Custom_user(AbstractUser):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    age = models.IntegerField(null=True, blank=True)
    contact = models.IntegerField(null=True, blank=True)
Adding custom fields

Payment Model:

class Payment(models.Model):

    # saving purchase details from our side !

    class Status(models.TextChoices):
        SUCESS = "SUCESS", "Sucess"
        PENDING = "PENDNG", "Pending"
        FAILURE = "FAILURE", "Failure"
        INITIATED = "INITIATED", "Initiated"
        REFUNDED = "REFUNDED", 'Refunded'

    user = models.ForeignKey(Custom_user, on_delete=models.CASCADE)
    purchase_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    purchase_order_name = models.CharField(verbose_name='purchase_order_name')
    purchse_status = models.CharField(choices=Status.choices, default=Status.INITIATED)
    purchase_amount = models.FloatField(null=True)
    initiated_at = models.DateTimeField(auto_now_add=True)

    # saving purchase Details from Khalti Side !

    pidx = models.CharField(verbose_name='pidx', editable=False)
    khalti_status = models.CharField(verbose_name='Status From Khalti', editable=False)
    khalti_transaction_id = models.CharField(verbose_name='Khalti Transaction ID', editable=False)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return f"Transaction of user {self.user.username} => Status : {self.purchse_status}"
Payment model

Now make migrations:

Add it in admin.py:

from django.contrib import admin
from .models import Custom_user, Payment

admin.site.register(Custom_user)

class PAYMENT_ADMIN(admin.ModelAdmin):
    list_display = [
        'user',
        'purchase_id',
        'purchse_status',
        'purchase_amount',
        'khalti_status',
        'pidx',
        'khalti_transaction_id',
        'initiated_at',
        'updated_at'
    ]

admin.site.register(Payment, PAYMENT_ADMIN)
Admin page

Views and URL Routing

In {app_name}/views.py, render the home page first, passing user ID as context:

from django.http import HttpResponse
from django.shortcuts import render

def home(request):
    context = {
        'id': request.user.id
    }
    return render(request, 'home/home.html', context)
Basic home page

Adding routing to that view ({app_name}/urls.py):

from django.urls import path
from . import views

app_name = "home"
urlpatterns = [
    path('', views.home, name='home'),
    path("payment/", views.payment_gate, name="payment_gate"),
    path('payment/validation/', views.payment_validation, name="payment_validation"),
]
URL routing
⚠️ Do not add routing for payment-related views yet — they haven't been implemented.

Integrating Khalti Payment

Creating a Khalti Dev/Test Account

Initiating a Payment Request

Every payment request should first be initiated from the merchant as a server-side POST request. Upon success, a unique request identifier called pidx is provided.

POST /epayment/initiate/
URLMethodFormatAuthorization
/epayment/initiate/POSTRequired. application/jsonRequired

JSON Payload Fields

FieldRequiredDescription
return_urlYesLanding page after the transaction. Must contain a valid URL.
website_urlYesThe URL of the website. Must contain a valid URL.
amountYesTotal payable amount excluding service charge. Amount must be in Paisa.
purchase_order_idYesUnique identifier for the transaction generated by merchant.
purchase_order_nameYesThe name of the product.

Example request:

import requests
import json

url = "https://dev.khalti.com/api/v2/epayment/initiate/"

payload = json.dumps({
    "return_url": "http://example.com/",
    "website_url": "https://example.com/",
    "amount": "1000",
    "purchase_order_id": "1231231231kjhgjkgh",
    "purchase_order_name": "test101",
    "customer_info": {
        "name": "bla bla",
        "email": "example@gmail.com",
        "phone": "9800000000"
    }
})
headers = {
    'Authorization': 'key live_secret_key_68791341fdd94846a146f0457ff7b455',
    'Content-Type': 'application/json',
}

response = requests.request("POST", url, headers=headers, data=payload)
print(response.text)

Implementing Payment in Django

Getting your Secret Key

Once you have your key, create a .env file and add it there. Never share it publicly!

Using python-decouple to manage secrets

pip install python-decouple

Import your secret keys in settings.py:

Importing decouple Importing keys from .env

Key variables used:

Payment Gateway View

Things to consider:

Khalti integration logic
def payment_gate(request):

    Debug = False

    if request.method == 'POST':
        amount = request.POST.get('payment_amount')
        purchse_name = request.POST.get('purchase_name')

        url = settings.KHALTI_URL
        AMOUNT = int(amount) * 100

        payment_obj = Payment.objects.create(
            user=request.user,
            purchase_order_name=purchse_name,
            purchase_amount=amount
        )

        payload = json.dumps({
            "return_url": request.build_absolute_uri(reverse("home:payment_validation")),
            "website_url": request.build_absolute_uri('/'),
            "amount": AMOUNT,
            "purchase_order_id": str(payment_obj.purchase_id),
            "purchase_order_name": payment_obj.purchase_order_name,
            "customer_info": {
                "name": payment_obj.user.username,
                "email": payment_obj.user.email,
                "phone": payment_obj.user.contact
            }
        })
        headers = {
            'Authorization': f'key {settings.SECRET_KEY}',
            'Content-Type': 'application/json',
        }

        response = requests.request("POST", url, headers=headers, data=payload)

        if Debug:
            return JsonResponse(response.json(), safe=False)
        else:
            return redirect(response.json()['payment_url'])
    else:
        return render(request, 'payment/payment.html')

Payment Verification

POST /epayment/lookup/
URLMethodFormatAuthorization
/epayment/lookup/POSTRequired. application/jsonRequired
Payment validation

What does this code do?

📌 Per the official Khalti docs: If any negative consequences occur due to incomplete API integration or providing service without checking lookup status, Khalti won't be accountable for any such losses. Always verify payment status before providing service!

Success screen:

Success screen

Generic Errors

Incorrect Authorization key:

{
   "detail": "Invalid token.",
   "status_code": 401
}

Incorrect pidx:

{
   "detail": "Not found.",
   "error_key": "validation_error"
}

Missing key prefix in Authorization header:

{
    "detail": "Authentication credentials were not provided.",
    "status_code": 401
}

Navigation URLs

💡 Navigation buttons between home and payment are not included — manually add the URL in your browser.

Example Project