python
46 lines · 7 steps
Handling Stripe webhooks in Django
A Django view that verifies Stripe webhook signatures and fulfills paid orders idempotently.
Explained by
highlit
1import json
2import logging
3
4import stripe
5from django.conf import settings
6from django.http import HttpResponse, HttpResponseBadRequest
7from django.views.decorators.csrf import csrf_exempt
8from django.views.decorators.http import require_POST
9
10from .models import Order
11from .tasks import send_receipt_email
12
13logger = logging.getLogger(__name__)
14
15
16@csrf_exempt
17@require_POST
18def stripe_webhook(request):
19 payload = request.body
20 sig_header = request.META.get("HTTP_STRIPE_SIGNATURE")
21
22 try:
23 event = stripe.Webhook.construct_event(
24 payload=payload,
25 sig_header=sig_header,
26 secret=settings.STRIPE_WEBHOOK_SECRET,
27 )
28 except ValueError:
29 logger.warning("Stripe webhook received malformed payload")
30 return HttpResponseBadRequest("Invalid payload")
31 except stripe.error.SignatureVerificationError:
32 logger.warning("Stripe webhook signature verification failed")
33 return HttpResponseBadRequest("Invalid signature")
34
35 if event.type == "checkout.session.completed":
36 session = event.data.object
37 order = Order.objects.filter(
38 stripe_session_id=session.id, status=Order.Status.PENDING
39 ).first()
40 if order is not None:
41 order.mark_paid(payment_intent=session.payment_intent)
42 send_receipt_email.delay(order.pk)
43 else:
44 logger.info("Ignoring unhandled Stripe event %s", event.type)
45
46 return HttpResponse(status=200)
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Always verify webhook signatures against a shared secret before trusting the payload.
- 2Filtering on a pending status makes fulfillment idempotent against duplicate webhook deliveries.
- 3Offload slow side effects like email to a background task and return 200 promptly.
Related explainers
python
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status from pydantic import BaseModel, EmailStr from sqlalchemy.orm import Session
Building a signup endpoint in FastAPI
dependency-injection
request-validation
background-tasks
Intermediate
8 steps
python
from django import forms from django.utils import timezone from .models import Reservation
Multi-field validation in a Django ModelForm
form validation
cross-field validation
modelform
Intermediate
7 steps
python
from django import template from django.urls import reverse, NoReverseMatch from django.utils.html import format_html
Active nav-link template tags in Django
template tags
url routing
active state
Intermediate
7 steps
python
from functools import wraps import asyncio from fastapi import APIRouter, FastAPI, Request
Per-route request timeouts in FastAPI
decorators
async
timeouts
Intermediate
6 steps
python
import os from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent.parent
How a Django settings module is wired
configuration
environment-variables
middleware
Intermediate
8 steps
python
def is_valid_card_number(number: str) -> bool: digits = [int(c) for c in number if c.isdigit()] if len(digits) < 13 or len(digits) > 19:
Validating card numbers with the Luhn check
checksum
validation
luhn-algorithm
Intermediate
6 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/handling-stripe-webhooks-in-django-explained-python-e56e/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.