To avoid creating a project from scratch, let's continue with the basic Django project in my GitHub:
https://github.com/38130/mysite
Througout this post, please assume the following project:
mysite/
polls/
templates/
polls/
index.html
views.py
urls.py

This post focus in the PayPal logic.
The ecommerce logic is intentionally hardcoded.
Credentials & PayPal Configuration
Signup or Login into PayPal Developers platform: https://developer.paypal.com/home/
- Select Sandbox
- Select API Credentials
- Create App
- Copy/paste Client ID and Secret into mysite/polls/views.py
- Scroll down and click Add Webhook
- Choose a name
-
Choose a URL
Ngrok
Because PayPal can't reach us in localhost, we create a bridge from localhost to the internet by using Ngrok.Download: https://ngrok.com/download
After the download unzip ngrok.exe
Doubleclick itSignup/Login : https://ngrok.com
In https://dashboard.ngrok.com/get-started/setup, copy ngrok config add-authtoken
ngrok config add-authtoken **************************************************Execute it in ngrok prompt
ngrok http 80
Copy the link next to «Forwarding» (Right click and select mark if you can't select the text)
ALLOWED_HOSTS = ['localhost', '7b86-159-255-57-146.ngrok-free.app']
The local project is now accessible externaly through:
https://7b86-159-255-57-146.ngrok-free.app
Add the WebHook URL:
https://7b86-159-255-57-146.ngrok-free.app/webhook
(We will create this url later in Django)
- Select All events
- Save
- Copy the Webhook ID into Django
clientID = "***********"
secret = "***********"
webhookID = "***********"
The PayPal button
Optionally we can define the client ID in the backend.
class index(TemplateView):
template_name = 'polls/index.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['price'] = 0.005
context['quantity'] = 2
context['clientID'] = clientID
return context
The button is customizable in different ways. Design and styling, credit card, shipping, currency, success/cancel links are some of the customizations we can make.
<div id="paypal-button-container"></div>
<script src="https://www.paypal.com/sdk/js?client-id={{clientID}}¤cy=EUR&disable-funding=card"></script>
<script type="text/javascript">
var price = "{{price}}";
var quantity = "{{quantity}}";
var total = parseFloat(price) * parseFloat(quantity);
var orderData = {
user: '{{user.pk}}',
order: '123456789',
quantity: '{{quantity}}',
};
paypal.Buttons({
style: {
shape: 'pill',
color: 'gold',
layout: 'vertical',
label: 'pay'
},
createOrder: function(data, actions) {
return actions.order.create({
intent: 'CAPTURE',
purchase_units: [{
amount: { value: total },
custom_id: JSON.stringify(orderData),
}],
application_context: { shipping_preference: 'NO_SHIPPING' }
});
},
onApprove: function(data, actions) {
return actions.order.capture().then(function(details) {
window.location.href = '/';
});
},
onCancel: function(data) {
window.location.href = '/';
},
commit: true
}).render('#paypal-button-container');
</script>
Allowing Django to open popups
...
SECURE_CROSS_ORIGIN_OPENER_POLICY = "same-origin-allow-popups"
Understanding the WebHook event flow

What will trigger the PayPal payment is a button in the webpage.
This button knows how to handle the payment because
it is associated with the client ID information and the amount of money to collect.
Upon triggering the button, PayPal gets notified with a request.
Its action is now to respond with a webhook to the webhook URL we have previously configured.
And here's something to keep in mind:
The response contains many information but it's not a payment confirmation.
The PayPal response still needs to be validated with PayPal itself.
To do this we need to communicate to the PayPal API through a Token.
We get this token from them. These tokens are valid for 9 hours and we could store it.
To keep things simple for the tutorial, we will request always a new token.
With this said, we create a verification request, and ask PayPal to confirm it.
Only afterwards we know the payment is valid or not.
This prevents people from simulating payments by simply accessing the webhook URL.
Handling the Webhook
from django.urls import path
from Webhooks.views import *
urlpatterns = [
...
path('webhook/', webhook.as_view(), name="webhook"),
]
from django.views.generic import View
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from django.http import HttpResponse
import json
import requests
#paypal_url = "https://api.paypal.com" #Live
paypal_url = "https://api.sandbox.paypal.com" #SandBox
...
@method_decorator(csrf_exempt, name='dispatch')
class webhook(View):
def post(self, request, *args, **kwargs):
try:
res = json.loads(request.body)
# Getting the Token
token_url = paypal_url + '/v1/oauth2/token'
headers = {
'Accept': 'application/json',
'Accept-Language': 'en_US',
}
data = {'grant_type': 'client_credentials'}
token_res = requests.post(token_url, headers=headers, data=data, auth=(clientID, secret))
access_token = token_res.json().get('access_token')
if not access_token:
return HttpResponse(status=401)
# Webhook signature verification
verify_headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
verify_data = {
"auth_algo": request.headers.get("Paypal-Auth-Algo"),
"cert_url": request.headers.get("Paypal-Cert-Url"),
"transmission_sig": request.headers.get("Paypal-Transmission-Sig"),
"transmission_id": request.headers.get("Paypal-Transmission-Id"),
"transmission_time": request.headers.get("Paypal-Transmission-Time"),
"webhook_id": webhookID,
"webhook_event": res
}
verify_res = requests.post(
paypal_url + "/v1/notifications/verify-webhook-signature",
json=verify_data,
headers=verify_headers
)
if verify_res.status_code != 200 or verify_res.json().get("verification_status") != "SUCCESS":
return HttpResponse(status=400) # not valid signature
# Payment Confirmation
event_type = res.get('event_type')
resource = res.get('resource', {})
custom_id = resource.get('custom_id')
event_code = None
if event_type == "PAYMENT.CAPTURE.COMPLETED":
event_code = "C"
elif event_type == "PAYMENT.CAPTURE.DENIED":
event_code = "D"
print("Custom ID:", custom_id)
print("Event code:", event_code)
except Exception:
return HttpResponse(status=500)
return HttpResponse(status=200)
Custom ID
When we receive a payment confirmation, we can use the custom ID to reference the order and a specific user within our system.
SandBox
In PayPal developers:
- Go to SandBox accounts
- Click the email with your personal account
- Use these credentials to login while simulating payments
Live
Going Live and accepting real payments, it's now a matter of changing:
- Changing the paypal_url
- Selecting Live
- Repeating the process to create an App this time in Live
- Repeating the process to create a WebHook this time in Live
- Adding the URL of your website to the webhook instead of the Ngrok
- Replacing the Client ID, secret and webhook ID in views.py
Keep in mind that
You can't be a client of your own PayPal Business account.
This means that in order to test, you need to use a different account than the one that is receiving the payments.
Tested with: Django 5.2
07 Jan. 2026
|
Last Updated: 08 Jan. 2026
|
jaimedcsilva Related