Back
LukCode

LukCode

Python with Django - Stripe integration 🤑

Python with Django - Stripe integration 🤑

Integrating with external API, in my opinion, is always a little bit tricky especially when you do that first time with a specific product. You need spent tons of hours reading documentation and it often turns out that the answer is on the stack instead of docs…

In this tutorial, I’ll focus on billing product which means you’ll get knowledge about how to create subscriptions and manage them. I’ll use low code examples of docs - don’t be scared that’s easy!

Project structure

In our project, we'll use Docker so we have to create two files Dockerfile and docker-compose.yml. But first let’s create base folder structure of our project.

mkdir django-stripe-integration && cd django-stripe-integration && mkdir src
 
pip3 install django
 
cd src && django-admin startproject config .

Now back to the main project folder this is django-stripe-integration and let’s create Dockerfile and docker-compose.yml.

In Dockerfile we define the python version which we want to use, next is workdir which means folder in docker container, then copy requirements.tsx to workdir and install assets, last step is copying everything to code workdir. If you want to create project like me check this requirements.

Dockerfile

Dockerfile
FROM python:3.9.0
ENV PYTHONUNBUFFERED=1
WORKDIR /code
COPY requirements.txt /code/
RUN pip install --upgrade pip && pip install -r requirements.txt
COPY . /code/

In Docker-compose.yml we define two services postgres and django - this is very simple file only for test project purpose as you can see in database there is password, that should comes from env file.

docker-compose.yml
version: '3'
services:
  db:
    image: postgres
    volumes:
      - pg_data:/var/lib/postgresql/data
    environment:
      - POSTGRES_PASSWORD=postgres
    ports:
      - '9000:5432'
 
  django:
    build: .
    command: python src/manage.py runserver 0.0.0.0:8000
    volumes:
      - .:/code
    depends_on:
      - db
    ports:
      - '8000:8000'
    env_file: .env
volumes:
  pg_data:
    driver: local

Now we have everything to start app so you can hit that command:

docker-compose up – build

After the build you'll have information about migration so you need to check name of your container using docker ps

docker ps
CONTAINER ID   IMAGE                              COMMAND                  CREATED       STATUS       PORTS                                       NAMES
689362efc33f   django-stripe-integration_django   "python src/manage.p…"   2 hours ago   Up 2 hours   0.0.0.0:8000->8000/tcp, :::8000->8000/tcp   django-stripe-integration_django_1
1b62eb1def82   postgres                           "docker-entrypoint.s…"   2 hours ago   Up 2 hours   0.0.0.0:9000->5432/tcp, :::9000->5432/tcp   django-stripe-integration_db_1

In my case my container have django-stripe-integration_django_1 name so lets jump into this container.

docker exec -it django-stripe-integration_django_1 bash
 
cd src
 
python manage.py migrate

First of all before we gonna continue our work we need to jump into settings.py. For keeping good practices of security you must keep your secret variables in .env file, and then you can use that variables using environ like example bellow. By the way here is my .env.example file.

src/settings.py
import environ
 
env = environ.Env()
BASE_DIR = environ.Path(__file__) - 2
 
SECRET_KEY = env("SECRET_KEY")
DEBUG = env("DEBUG")
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS")

Above we defined basic things for django, but now we need to provide things for our database.

src/settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': env("DB_NAME"),
        'USER': env("DB_USER"),
        'PASSWORD': env("DB_PASSWORD"),
        'HOST': 'db',
        'PORT': '5432',
    }
}

And the last major thing is this what we actually doing in this tutorial are things for Stripe.

src/settings.py
import stripe
 
stripe.api_key = env("STRIPE_SECRET_KEY")
STRIPE_WEBHOOK_KEY = env("STRIPE_WEBHOOK_KEY")
STRIPE_CHECKOUT_SUCCESS_URL = env("STRIPE_CHECKOUT_SUCCESS_URL")
STRIPE_CHECKOUT_CANCEL_URL = env("STRIPE_CHECKOUT_CANCEL_URL")
STRIPE_CHECKOUT_RETURN_URL = env("STRIPE_CHECKOUT_RETURN_URL")

Before we gonna start coding let’s create two apps - auth_ex and subscriptions.

python manage.py startapp auth_ex subscriptions

After start app you need of course adjust your settings, because in this example we gonna use djoser for authorization - first of all is installed apps.

src/settings.py
INSTALLED_APPS = [
   'django.contrib.admin',
   'django.contrib.auth',
   'django.contrib.contenttypes',
   'django.contrib.sessions',
   'django.contrib.messages',
   'django.contrib.staticfiles',
   'rest_framework', 
   'djoser', 
   'auth_ex.apps.AuthExConfig', 
   'subscriptions'
]
 
AUTH_USER_MODEL = "auth_ex.User"
 
SIMPLE_JWT = {
    'AUTH_HEADER_TYPES': ('JWT',), 
}

User model

I think we have everything done with the basic setup so we can switch the info User model, To be honest, you don’t need to do this, because in this article I gonna focus on Stripe integration. As you can see I created UserManaged and User model with a pretty simple structure but the crucial thing is field customer_id in User because when you need to deal with Stripe customer id is the really important thing.

src/auth_ex/models.py
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.models import PermissionsMixin
from django.contrib.auth.base_user import BaseUserManager, AbstractBaseUser
from django.utils import timezone
 
 
class UserManager(BaseUserManager):
    def _create_user(self, email, password=None, **extra_field):
        """
        Create and save user
        """
        if not email:
            raise ValueError("The given email must be set")
        email = self.normalize_email(email)
        user = self.model(
            email=email,
            **extra_field
        )
        user.set_password(password)
        user.save(using=self.db)
        return user
 
    def create_user(self, email, password=None, **extra_fields):
        return self._create_user(email, password, **extra_fields)
 
    def create_superuser(self, email, password, **extra_fields):
        """
        Create and save a SuperUser with the given email and password.
        """
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)
        extra_fields.setdefault('is_active', True)
 
        return self.create_user(email, password, **extra_fields)
 
 
class User(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(_("email address"), unique=True)
    is_superuser = models.BooleanField(default=False)
    is_staff = models.BooleanField(default=False)
    is_active = models.BooleanField(default=True)
    date_joined = models.DateTimeField(default=timezone.now)
    customer_id = models.CharField(null=True, max_length=50, editable=False)
 
    objects = UserManager()
    USERNAME_FIELD = 'email'

Then you need to create really simple serializer.

src/auth_ex/serializers.py
from rest_framework import serializers
from .models import User
 
 
class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ['emails']

Stripe

Before you start coding you need to have access to the developer dashboard in Stripe, when you log in grab your API secret key and pass it into the env file.

Stripe dashboard api keys view

Now we can continue our work and start with viewSets for subscriptions. First is CreateCheckoutSession this endpoint is for creating URL for checkout session in stripe - when in your frontend app user need to add a payment method and create a subscription.

src/subscriptions/views.py
import stripe
from rest_framework.decorators import api_view
from rest_framework.views import APIView
from rest_framework.response import Response
from django.conf import settings
from .serializers import CreateCheckoutSerializer
from rest_framework import status
from auth_ex.models import User
 
 
class CreateCheckoutSession(APIView):
 
    def post(self, request):
        try:
 
            serializer = CreateCheckoutSerializer(data=request.data)
            if serializer.is_valid():
                checkout_session = stripe.checkout.Session.create(
                    line_items=[
                        {
                            'price': request.data.get('price_id'),
                            'quantity': 1,
                        },
                    ],
                    client_reference_id=request.user.pk,
                    mode='subscription',
                    success_url=settings.STRIPE_CHECKOUT_SUCCESS_URL + '?success=true&session_id={CHECKOUT_SESSION_ID}',
                    cancel_url=settings.STRIPE_CHECKOUT_CANCEL_URL + '?canceled=true',
                )
 
                return Response(checkout_session, status=status.HTTP_200_OK)
 
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
 
        except Exception as error:
            return Response(
                {"error": str(error)},
                status=status.HTTP_400_BAD_REQUEST
            )

CustomerPortal endpoint is for managing user subscription, flow is a user from frontend need to just ask API for this, and then redirect.

src/subscriptions/views.py
class CustomerPortal(APIView):
 
    def post(self, request):
        portalSession = stripe.billing_portal.Session.create(
            customer=request.user.customer_id,
            return_url=settings.STRIPE_CHECKOUT_RETURN_URL,
        )
 
        return Response({
            "url": portalSession.url
        }, status=200)

When in your frontend app you need to display all available plans this is simple endpoint for this.

src/subscriptions/views.py
class GetPlans(APIView):
 
    def get(self, request):
        prices = stripe.Price.list()
 
        return Response(prices, status=200)

The last thing is webhooks - actually, this is a crucial thing because in your API you need to detect what happened in stripe API. Jump into stripe dashboard and create webhook there as on a screen bellow. Remember to grab your secret webhook key and pass to env file.

Stripe webhooks view

As you can see you need to provide Endpoint URL and while I was working on local environment I used ngrok for testing webhooks.

Console view

Now we can code webhooks in our API, we gonna listen checkout.session.completed action and on this action we'll update our User and customer_id field.

src/subscriptions/views.py
class Webhook(APIView):
    webhook_secret = settings.STRIPE_WEBHOOK_KEY
 
    def post(self, request):
 
        actions = {
            'checkout.session.completed': self.checkout_session_completed,
        }
 
        if self.webhook_secret:
            signature = request.headers.get('stripe-signature')
            try:
                event = stripe.Webhook.construct_event(
                    payload=request.body, sig_header=signature, secret=self.webhook_secret)
                data = event['data']
            except Exception as e:
                return e
 
            event_type = event["type"]
            map_actions = actions.get(event_type)
 
            if map_actions:
                map_actions(data["object"])
                return Response({"status": "success"})
 
            return Response(
                {"error": f"Error placed in {event_type}"},
                status=status.HTTP_400_BAD_REQUEST,
            )
 
    def checkout_session_completed(self, data):
        user_id = data.get('client_reference_id')
        customer_id = data.get('customer')
 
        User.objects.filter(pk=user_id).update(
            customer_id=customer_id
        )

After creating viewSets we need to define urls - check this out.

src/subscriptions/urls.py
from django.urls import path
from rest_framework.urlpatterns import format_suffix_patterns
from .views import (
    CreateCheckoutSession,
    CustomerPortal,
    GetPlans,
    Webhook
)
 
urlpatterns = [
    path('create-session/', CreateCheckoutSession.as_view()),
    path('get-plans/', GetPlans.as_view()),
    path('create-customer-portal/', CustomerPortal.as_view()),
    path('webhook/', Webhook.as_view())
]
 
urlpatterns = format_suffix_patterns(urlpatterns)
src/subscriptions/serializers.py
from rest_framework import serializers
 
 
class CreateCheckoutSerializer(serializers.Serializer):
    price_id = serializers.CharField(max_length=50)

Conclusions

As I mentioned before dealing with extending API is always tricky, I hope now you will get some knowledge of how to deal with stripe using Python and Django. That project was pretty simple only for testing purposes and gaining knowledge of Python and Stripe. If you want check repo of this project and each files here. Thanks for your time! If you have questions you can ask me about them on my socials YouTube channel or Twitter.

Copyright © 2024 LukCode
All rights reserved

LukCode