Add Stripe Payments to Django: Setup Guide (2026)

30 Jun 2026 · Bank K.

Add Stripe payments to your Django app with Checkout Sessions, CSRF-exempt webhooks, and subscriptions. Working Python code examples.

You built a Django app, you want to charge for it, and now you need to add Stripe payments to your Django app. Django’s batteries-included philosophy gets you users, an admin, and an ORM for free — but payments aren’t in the box. The good news is that the integration is mostly two views plus one model, and Django’s structure makes it clean.

This guide uses Stripe’s hosted Checkout, which is the right default for a server-rendered Django app: Stripe hosts the payment page, and your Django views only create sessions and handle webhooks. You’ll hit one Django-specific landmine — CSRF protection on the webhook — and I’ll show you exactly how to handle it.

What You Need First

  • A Stripe account with your secret key and webhook signing secret (test mode first)
  • A Django project (4.x or 5.x) with the auth system enabled (the default)
  • A model to record what users have paid for, or a flag on your existing user/profile model

Because Checkout is hosted, you don’t need Stripe.js or any frontend payment code. Your Django views do all the work.

Step 1: Install and Configure Stripe

pip install stripe

Put your keys in settings, loaded from environment variables — never commit them:

# settings.py
import os

STRIPE_SECRET_KEY = os.environ["STRIPE_SECRET_KEY"]
STRIPE_WEBHOOK_SECRET = os.environ["STRIPE_WEBHOOK_SECRET"]

You can use the stripe SDK directly (shown here) or the dj-stripe package, which syncs Stripe objects into your Django models automatically. dj-stripe is worth it once you have lots of Stripe data to query locally; for a first integration, the raw SDK is fewer moving parts.

Step 2: Create a Checkout Session View

This view builds a hosted Checkout Session and redirects the user to Stripe. Define prices in the Stripe Dashboard and reference them by price ID, so amounts live in Stripe rather than scattered through your code.

# views.py
import stripe
from django.conf import settings
from django.shortcuts import redirect
from django.contrib.auth.decorators import login_required

stripe.api_key = settings.STRIPE_SECRET_KEY

@login_required
def create_checkout_session(request):
    session = stripe.checkout.Session.create(
        mode="payment",  # "subscription" for recurring plans
        line_items=[{"price": "price_xxx", "quantity": 1}],
        success_url=request.build_absolute_uri("/success/")
            + "?session_id={CHECKOUT_SESSION_ID}",
        cancel_url=request.build_absolute_uri("/cancel/"),
        client_reference_id=str(request.user.id),
    )
    return redirect(session.url, code=303)

client_reference_id carries your Django user’s ID into the session. When the webhook fires after payment, you’ll use it to find the right user. {CHECKOUT_SESSION_ID} is a placeholder Stripe substitutes into the success URL automatically. Wiring it through request.build_absolute_uri keeps the URLs correct across dev and production.

Step 3: Handle the Webhook (Mind the CSRF)

Here’s the Django-specific catch. Django checks every POST for a CSRF token. Stripe’s servers don’t send one — they sign the request instead. If you don’t exempt the webhook view, Django returns a 403 and your webhooks silently fail. This is the single most common Django + Stripe bug.

Exempt the view with @csrf_exempt, then verify Stripe’s signature against the raw request body so you don’t lose the security CSRF was providing:

# views.py
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST

@csrf_exempt
@require_POST
def stripe_webhook(request):
    payload = request.body  # raw bytes — required for signature verification
    sig_header = request.META.get("HTTP_STRIPE_SIGNATURE")

    try:
        event = stripe.Webhook.construct_event(
            payload, sig_header, settings.STRIPE_WEBHOOK_SECRET
        )
    except (ValueError, stripe.error.SignatureVerificationError):
        return HttpResponse(status=400)

    if event["type"] == "checkout.session.completed":
        session = event["data"]["object"]
        user_id = session.get("client_reference_id")
        # Look up the user, grant access, record the payment
    elif event["type"] == "invoice.payment_failed":
        # Notify the user, log the failure
        pass

    return HttpResponse(status=200)

request.body gives you the raw bytes Stripe signed. Don’t use request.POST or json.loads(request.body) before verification — the signature check has to run against the exact bytes Stripe sent.

Register the URL:

# urls.py
from django.urls import path
from . import views

urlpatterns = [
    path("create-checkout-session/", views.create_checkout_session),
    path("webhooks/stripe/", views.stripe_webhook),
]

Test it locally with the Stripe CLI:

stripe listen --forward-to localhost:8000/webhooks/stripe/

It prints a whsec_ signing secret — set it as STRIPE_WEBHOOK_SECRET during development.

Step 4: Make It Idempotent

Stripe retries webhooks and can deliver the same event twice. Without protection, a retried checkout.session.completed could grant access or send a receipt twice. A tiny model solves it:

# models.py
from django.db import models

class ProcessedStripeEvent(models.Model):
    event_id = models.CharField(max_length=255, unique=True)
    created_at = models.DateTimeField(auto_now_add=True)
# in stripe_webhook, after construct_event:
from .models import ProcessedStripeEvent

_, created = ProcessedStripeEvent.objects.get_or_create(event_id=event["id"])
if not created:
    return HttpResponse(status=200)  # already handled, skip

The unique=True constraint plus get_or_create gives you atomic deduplication straight from the database.

Step 5: Subscriptions

For recurring billing, switch the session mode to "subscription" and point it at a recurring price. Then handle the subscription lifecycle events in your webhook:

if event["type"] == "checkout.session.completed":
    # Initial subscription started — provision access
    pass
elif event["type"] == "invoice.payment_succeeded":
    # Renewal succeeded — extend access
    pass
elif event["type"] == "invoice.payment_failed":
    # Renewal failed — start dunning, warn the user
    pass
elif event["type"] == "customer.subscription.deleted":
    # Canceled — revoke access
    pass

Store the Stripe customer and subscription IDs on your user model so you can later open the Stripe Customer Portal, where users self-manage plans and payment methods without any custom UI from you.

Skip the Boilerplate

Checkout views, CSRF-exempt webhook handlers, idempotency models, subscription lifecycle events, the customer portal — every Django SaaS needs the same scaffolding, and none of it is your actual product. You write it once, then write it again on the next project.

Beag gives you authentication and Stripe payments as a drop-in so you can add both in minutes instead of rebuilding webhook plumbing. If you’ve integrated Stripe with Django before, you know exactly how much of this is repetitive setup with sharp edges.

Common Mistakes to Avoid

Forgetting @csrf_exempt on the webhook. Without it, Django returns 403 and webhooks fail silently. Exempt the view and rely on Stripe’s signature verification for security instead.

Verifying against parsed data. Use request.body (raw bytes). Parsing the JSON first breaks signature verification.

Skipping idempotency. Stripe resends events. Deduplicate by event["id"] with a unique-constrained model.

Hardcoding prices in Python. Define prices in the Stripe Dashboard and reference them by price_id. A price change becomes a dashboard edit, not a redeploy.

FAQ

Why does my Django Stripe webhook return a 403?

Django’s CSRF middleware blocks the POST because Stripe doesn’t send a CSRF token. Add @csrf_exempt to the webhook view, then verify Stripe’s signature against request.body so you keep request authenticity without CSRF.

Should I use dj-stripe or the raw Stripe SDK?

For a first integration, the raw stripe SDK is fewer moving parts. dj-stripe shines once you need to query Stripe objects locally through Django models and the admin — it syncs Stripe data into your database automatically. Both are solid; pick based on how much Stripe data you query.

How do I connect a Stripe payment to a Django user?

Pass str(request.user.id) as client_reference_id when creating the Checkout Session. It returns on the checkout.session.completed webhook so you can look up and provision the right user.

Does Django’s built-in auth work with Stripe?

Yes. Django’s auth gives you users and login; Stripe handles billing. You link them with client_reference_id and store the Stripe customer ID on your user model. Or use Beag to get auth and payments pre-integrated.

Wrapping Up

Adding Stripe to Django comes down to a Checkout Session view, a CSRF-exempt webhook view that verifies the raw body, an idempotency model, and a handful of subscription events. Let Stripe host the payment page and your Django code stays small and readable.

If you’d rather not maintain it all, Beag handles auth and payments together. For more framework-specific guides, browse the Beag blog.

About the Author
Bank K.

Bank K.

Serial entrepreneur & Co-founder of Beag.io

Founder of Beag.io. Indie hacker building tools to help developers ship faster.

Ready to Make Money From Your SaaS?

Turn your SaaS into cash with Beag.io. Get started now!

Start 7-day free trial →