If you are familiar with Stripe, you know how big of a player they are in the online payment processing field. Their API not only allows programmers to easily create one-time payments for sites such as e-commerce stores, but also provides quick integrations for monthly subscriptions and routing payouts. If new to Django and Stripe, check out our recent on integrating one time payments. Otherwise, let's get into setting up monthly payments with Django and Stripe. article Why a Monthly Subscription? Monthly subscriptions are a common practice found across the web especially among companies who promote software-as-a-service (SAAS) as their delivery model. For example, SAAS companies such as Hubspot (marketing), Dropbox (data storage), and Mailchimp (email marketing) all offer tiered pricing options to their prospective customers. Many consider this model to be favorable given that revenue is easier to predict once basic metrics are calculated (customer acquisition cost, lifetime value, churn rate). Predictable revenue of course creates stability and produces more accurate financial forecasts. Mailchimp pricing page Django Setup Start by setting up a virtual environment and creating a basic Django project. We'll create a new virtual environment called . saas on Mac, use instead of for all commands Note: python3 py C:\Users\Owner\Desktop\code>py -m venv saas Next change directory into the virtual environment, install Django, and setup your project and app. C:\Users\Owner\Desktop\code>cd saas C:\Users\Owner\Desktop\code\saas>Scripts\activate (saas) C:\Users\Owner\Desktop\code\saas>pip install Django (saas) C:\Users\Owner\Desktop\code\saas>django-admin startproject mysite (saas) C:\Users\Owner\Desktop\code\saas\mysite>py manage.py startapp main Add main app to the in . settings.py mysite settings.py INSTALLED_APPS = [ , , , , , , , ] 'main.apps.MainConfig' #add this 'django.contrib.admin' 'django.contrib.auth' 'django.contrib.contenttypes' 'django.contrib.sessions' 'django.contrib.messages' 'django.contrib.staticfiles' Create in the folder and include in . urls.py main mysite > urls.py mysite > urls.py django.contrib admin django.urls path, include urlpatterns = [ path( , include( )), path( , admin.site.urls), ] from import from import #add include '' 'main.urls' #add path 'admin/' Stripe Integration Start by installing the official library for connecting to Stripe's API. pip install --upgrade stripe Next create a Stripe account and create products in their dashboard. While this can be performed using Stripe CLI's we'll use the dashboard since we are already on the page from creating an account. Make sure you are in test mode and can only view test data. By default, you should be in test mode after creating an account. Click products in the left-hand side navigation menu. Create two new products: Name the product Premium Plan Add description "paid plan for advanced features" Use standard pricing Enter $15.00 and ensure "recurring" is selected Keep billing period as "monthly" Name the product Enterprise Plan Add description "enterprise plan for advanced features" Use standard pricing Enter $30.00 and ensure "recurring" is selected Keep billing period as "monthly" Now let's sync the product in the Stripe dashboard. While we could create a model and store the relevant product information, such as as product id, as model fields, an easier solution is to simply install the and use the sync command. We also add need to add our API keys in . Note your live keys should always be secured and never listed in the settings. For more information on securing environment variables, check out Python securing. dj-stripe package settings.py pip install dj-stripe mysite/settings.py STRIPE_TEST_PUBLIC_KEY = STRIPE_TEST_SECRET_KEY = STRIPE_LIVE_MODE = DJSTRIPE_WEBHOOK_SECRET = 'pk_test_E52Nh0gTCRpJ7h4JhuEX7BIO006LVew6GG' 'sk_test_L87kx7GQNbz9tajOluDts7da00mSbze3dW' False # Change to True in production "whsec_xxx" Finally migrate the database. Note: if database is sqlite, migrating make take longer than usual. py manage.py migrate Add products to database automatically with the following command: py manage.py djstripe_sync_plans_from_stripe View the admin page to see the changes we just made. First create an admin user. Include an email such as since we'll use this as the Stripe customer name then visit . You should see a variety of new models including our newly created products as well as plans for the products. test@example.com http://127.0.0.1:8000/admin/ py manage.py createsuperuser Checkout Page Now that we have our data synced, let's create a checkout page where users can select their plan and checkout. We'll start with the view and checkout template. Note: we have imported the Bootstrap CDN in home.html: views.py django.shortcuts render, redirect stripe json django.http JsonResponse djstripe.models Product django.contrib.auth.decorators login_required render(request, ) products = Product.objects.all() render(request, ,{ : products}) from import import import from import from import from import # Create your views here. : def homepage (request) return "home.html" @login_required : def checkout (request) return "checkout.html" "products" checkout.html {% extends "home.html" %} {% block content %} {% for p in products %} {{p.name}} {{p.description}} {% for plan in p.plan_set.all %} {{ plan.human_readable_price }} {% endfor %} {% endfor %} Checkout Enter card details. Your subscription will start immediately Plan: Total: Loading... Subscribe {% endblock %} < = > script src "https://js.stripe.com/v3/" </ > script < > br < > br < = > div class "container " < = > div class "row " < = > div class "col-6" < = = > div class "card mx-5 shadow" style "border-radius: 10px; border:none; " < = > div class "card-body" < = > h5 class "card-title font-weight-bold" </ > h5 < = > p class "card-text text-muted" < = = = = = = > svg class "bi bi-check" width "1em" height "1em" viewBox "0 0 16 16" fill "currentColor" xmlns "http://www.w3.org/2000/svg" < = = /> path fill-rule "evenodd" d "M10.97 4.97a.75.75 0 0 1 1.071 1.05l-3.992 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.236.236 0 0 1 .02-.022z" </ > svg </ > p < > h5 </ > h5 < = > div class "text-right" < = = = = > input type "checkbox" name "{{p.name}}" value "{{p.id}}" onclick "planSelect('{{p.name}}' ,'{{plan.human_readable_price}}', '{{plan.id}}')" </ > div </ > div </ > div </ > div </ > div < > br < > br < > hr < > br < > br < > div < = > div class "row" < = > div class "col-12" < = = > div class "card mx-5 shadow rounded" style "border-radius:50px;border:none" < = > div class "card-body" < = > h5 class "card-title font-weight-bold" </ > h5 < = > p class "text-muted " </ > p < = > div class "row" < = > div class "col-6 text-muted" < > p </ > p < > p </ > p </ > div < = > div class "col-6 text-right" < = > p id "plan" </ > p < = > p id "price" </ > p < = > p hidden id "priceId" </ > p </ > div </ > div < > br < = > form id "subscription-form" < = = > div id "card-element" class "MyCardElement" <!-- Elements will create input elements here --> </ > div <!-- We'll put the error messages in this element --> < = = > div id "card-errors" role "alert" </ > div < = = > button id "submit" type "submit" < = = = > div class "spinner-border spinner-border-sm text-light hidden" id "spinner" role "status" < = > span class "sr-only" </ > span </ > div < = > span id "button-text" </ > span </ > button </ > form </ > div </ > div </ > div </ > div </ > div </ > div Now that we have a template for selecting the subscription plan. Let's add some styling and setup the credit card form by using Stripe Elements, a pre-built set of UI components. < > { .StripeElement { : border-box; : ; : ; : solid transparent; : ; : white; : ; : box-shadow ease; : box-shadow ease; } { : ; } { : ; } { : ; } { : none; } { : (120%); } { : ; : ; : ; : ; : ; : ; : ; : -apple-system,BlinkMacSystemFont,sans-serif; : ; : ; : normal; : break-word; : border-box; : inherit; : visible; : button; : antialiased; : ; : inherit; : transparent; : ; : ; : ; : none; : none; : none; : ; : ; : (--radius); : ; : ; : ; : ; : pointer; : all . ease; : block; : (0,0,0,.07); : ; : (--button-color); } </ > style body box-sizing height 40px padding 10px 12px border 1px border-radius 4px background-color box-shadow 0 1px 3px 0 #e6ebf1 -webkit-transition 150ms transition 150ms .StripeElement--focus box-shadow 0 1px 3px 0 #cfd7df .StripeElement--invalid border-color #fa755a .StripeElement--webkit-autofill background-color #fefde5 !important .hidden display #submit :hover filter contrast #submit font-feature-settings "pnum" --body-color #f7fafc --button-color #556cd6 --accent-color #556cd6 --gray-border #e3e8ee --link-color #fff --font-color #697386 --body-font-family --radius 4px --form-width 400px -webkit-box-direction word-wrap box-sizing font overflow -webkit-appearance -webkit-font-smoothing margin 0 font-family -webkit-tap-highlight-color font-size 16px padding 0 12px line-height 32px outline text-decoration text-transform margin-right 8px height 36px border-radius var color #fff border 0 margin-top 16px font-weight 600 cursor transition 2s display box-shadow 0 4px 5.5px 0 rgba width 100% background var style .getElementById( ).disabled = ; stripeElements(); { stripe = Stripe( ); ( .getElementById( )) { elements = stripe.elements(); style = { : { : , : , : , : , : { : } }, : { : , : } }; card = elements.create( , { : style }); card.mount( ); card.on( , { el = .getElementById( ); el.classList.add( ); }); card.on( , { el = .getElementById( ); el.classList.remove( ); }); card.on( , { displayError(event); }); } } { displayError = .getElementById( ); (event.error) { displayError.textContent = event.error.message; } { displayError.textContent = ; } } document "submit" true ( ) function stripeElements 'pk_test_E52Nh0gTCRpJ7h4JhuEX7BIO006LVew6GG' if document 'card-element' let // Card Element styles let base color "#32325d" fontFamily '"Helvetica Neue", Helvetica, sans-serif' fontSmoothing "antialiased" fontSize "16px" "::placeholder" color "#aab7c4" invalid color "#fa755a" iconColor "#fa755a" 'card' style '#card-element' 'focus' ( ) function let document 'card-errors' 'focused' 'blur' ( ) function let document 'card-errors' 'focused' 'change' ( ) function event //we'll add payment form handling here ( ) function displayError event let document 'card-errors' if else '' This should allow the credit card form to render. First, we disabled the subscribe button and then called to create and then mount the credit card form. Next, we add a small script to update the template with the selected subscription plan and to re-enable the subscribe button if a plan is selected. stripeElements() { inputs = .getElementsByTagName( ); ( i = ; i<inputs.length; i++){ inputs[i].checked = ; (inputs[i].name== name){ inputs[i].checked = ; } } n = .getElementById( ); p = .getElementById( ); pid = .getElementById( ); n.innerHTML = name; p.innerHTML = price; pid.innerHTML = priceId; .getElementById( ).disabled = ; } ( ) function planSelect name, price, priceId var document 'input' for var 0 false if true var document 'plan' var document 'price' var document 'priceId' document "submit" false Now we need to handle the payment form submission. We'll add the first code block to the end of the function. First grab the form by its ID and add an event listener. After preventing the default form submission, we change the loading state of the form in order to prevent any issues associated with double clicking the subscribe button. Finally, with the inputted card information, we create a payment method and submit this data to our server along with the price id of the selected subscription. stripeElements() As demonstrated, we use Django's CSRF token to authorize the submission to our server. Note: Stripe's documentation includes some additional validation that you should check out if interested. Also, unlike the documentation, we create a customer and subscription in one request to the server instead of sending separate requests. paymentForm = .getElementById( ); (paymentForm) { paymentForm.addEventListener( , { evt.preventDefault(); changeLoadingState( ); createPaymentMethod({ card }); }); } } { billingName = ; stripe .createPaymentMethod({ : , : card, : { : billingName, }, }) .then( { (result.error) { displayError(result); } { paymentParams = { : .getElementById( ).innerHTML, : result.paymentMethod.id, }; fetch( , { : , : { : , : , }, : , : .stringify(paymentParams), }).then( { response.json(); }).then( { (result.error) { result; } result; }).then( { (result && result.status === ) { .location.href = ; }; }).catch( { displayError(result.error.message); }); } }); } changeLoadingState = { (isLoading) { .getElementById( ).disabled = ; .querySelector( ).classList.remove( ); .querySelector( ).classList.add( ); } { .getElementById( ).disabled = ; .querySelector( ).classList.add( ); .querySelector( ).classList.remove( ); } }; //we'll add payment form handling here let document 'subscription-form' if 'submit' ( ) function evt true // create new payment method & create subscription ( ) function createPaymentMethod { card } // Set up payment method for recurring usage let '{{user.username}}' type 'card' card billing_details name ( ) => result if else const price_id document "priceId" payment_method "/create-sub" method 'POST' headers 'Content-Type' 'application/json' 'X-CSRFToken' '{{ csrf_token }}' credentials 'same-origin' body JSON ( ) => response return ( ) => result if // The card had an error when trying to attach it to a customer throw return ( ) => result if 'active' window '/complete' ( ) function error var ( ) function isLoading if document "submit" true document "#spinner" "hidden" document "#button-text" "hidden" else document "submit" false document "#spinner" "hidden" document "#button-text" "hidden" Add url paths for creating a subscription/customer and when payment is complete. main/urls.py django.urls path . views app_name = urlpatterns = [ path( , views.homepage, name= ), path( , views.checkout, name= ), path( , views.logout_request, name= ), path( , views.login_request, name= ), path( , views.register, name= ), path( , views.create_sub, name= ), path( , views.complete, name= ), ] from import from import "main" "" "homepage" "checkout" "checkout" "logout" "logout_request" "login" "logout_request" "register" "register" "create-sub" "create sub" #add "complete" "complete" #add Next, create a view to handle creating a Stripe customer and subscription. The customer and subscription should also be stored in our local database. Luckily, we can easily syn our data using dj-stripe. main/views.py ... stripe json django.http JsonResponse djstripe.models Product django.contrib.auth.decorators login_required djstripe django.http HttpResponse ... request.method == : data = json.loads(request.body) payment_method = data[ ] stripe.api_key = djstripe.settings.STRIPE_SECRET_KEY payment_method_obj = stripe.PaymentMethod.retrieve(payment_method) djstripe.models.PaymentMethod.sync_from_stripe_data(payment_method_obj) : customer = stripe.Customer.create( payment_method=payment_method, email=request.user.email, invoice_settings={ : payment_method } ) djstripe_customer = djstripe.models.Customer.sync_from_stripe_data(customer) request.user.customer = djstripe_customer subscription = stripe.Subscription.create( customer=customer.id, items=[ { : data[ ], }, ], expand=[ ] ) djstripe_subscription = djstripe.models.Subscription.sync_from_stripe_data(subscription) request.user.subscription = djstripe_subscription request.user.save() JsonResponse(subscription) Exception e: JsonResponse({ : (e.args[ ])}, status = ) : HTTPresponse( ) import import from import from import from import import from import @login_required : def create_sub (request) if 'POST' # Reads application/json and returns a response 'payment_method' try # This creates a new Customer and attaches the PaymentMethod in one API call. 'default_payment_method' # At this point, associate the ID of the Customer object with your # own internal representation of a customer, if you have one. # print(customer) # Subscribe the user to the subscription created "price" "price_id" "latest_invoice.payment_intent" return except as return 'error' 0 403 else return 'requet method not allowed' Also add a view function for the payment complete page: render(request, ) : def complete (request) return "complete.html" Go through and test the integration. As suggested by Stripe, use "4242 4242 4242 4242" as the test credit card and view the results upon submitting your payment. Click on "Subscriptions" under "Customers" in your Stripe dashboard to view the newly created subscription and customer. You should see something similar as below: Conclusion Thanks for reading about using Django and Stripe to create monthly subscriptions. Hopefully this provides a foundation for creating your own SAAS project. We plan on writing additional pieces on Django and Stripe to cover topics such as routing payments with Stripe Connect, the underlying payment technology behind Lyft, Postmates, Kickstarter, and more. Also if you have any suggestions on other Django integrations that you'd like to read about, leave a comment below. Previously published at https://www.ordinarycoders.com/blog/article/django-stripe-monthly-subscription