Flask Stripe Subscriptions

Last updated July 20th, 2022

This tutorial looks at how to set up and collect monthly recurring subscription payments with Flask and Stripe.

Need to accept one-time payments? Check out Flask Stripe Tutorial.

Contents

Stripe Subscription Payment Options

There are multiple ways to implement and handle Stripe subscriptions, but the two most well-known are:

  1. Fixed-price subscriptions
  2. Future payments

In both cases, you can either use Stripe Checkout (which is a Stripe-hosted checkout page) or Stripe Elements (which are a set of custom UI components used to build payment forms). Use Stripe Checkout if you don't mind redirecting your users to a Stripe-hosted page and want Stripe to handle most of the payment process for you (e.g., customer and payment intent creation, etc.), otherwise use Stripe Elements.

The fixed-price approach is much easier to set up, but you don't have full control over the billing cycles and payments. By using this approach Stripe will automatically start charging your customers every billing cycle after a successful checkout.

Fixed-price steps:

  1. Redirect the user to Stripe Checkout (with mode=subscription)
  2. Create a webhook that listens for checkout.session.completed
  3. After the webhook is called, save relevant data to your database

The future payments approach is harder to set up, but this approach gives you full control over the subscriptions. You collect customer details and payment information in advance and charge your customers at a future date. This approach also allows you to sync billing cycles so that you can charge all of your customers on the same day.

Future payments steps:

  1. Redirect the user to Stripe Checkout (with mode=setup) to collect payment information
  2. Create a webhook that listens for checkout.session.completed
  3. After the webhook is called, save relevant data to your database
  4. From there, you can charge the payment method at a future date using the Payment Intents API

In this tutorial, we'll use the fixed-price approach with Stripe Checkout.

Billing Cycles

Before jumping in, it's worth noting that Stripe doesn't have a default billing frequency. Every Stripe subscription's billing date is determined by the following two factors:

  1. billing cycle anchor (timestamp of subscription's creation)
  2. recurring interval (daily, monthly, yearly, etc.)

For example, a customer with a monthly subscription set to cycle on the 2nd of the month will always be billed on the 2nd.

If a month doesn’t have the anchor day, the subscription will be billed on the last day of the month. For example, a subscription starting on January 31 bills on February 28 (or February 29 in a leap year), then March 31, April 30, and so on.

To learn more about billing cycles, refer to the Setting the subscription billing cycle date page from the Stripe documentation.

Project Setup

Let's start off by creating a new directory for our project. Inside the directory we'll create and activate a new virtual environment and install Flask.

$ mkdir flask-stripe-subscriptions && cd flask-stripe-subscriptions
$ python3.10 -m venv env
$ source env/bin/activate
(env)$ pip install flask

Feel free to swap virtualenv and Pip for Poetry or Pipenv.

Next, create a file called app.py, and add the code for a basic "Hello World" app:

# app.py

from flask import Flask, jsonify

app = Flask(__name__)


@app.route("/hello")
def hello_world():
    return jsonify("hello, world!")


if __name__ == "__main__":
    app.run()

Fire up the server:

(env)$ FLASK_ENV=development python app.py

Navigate to http://localhost:5000/hello and you should see the hello, world! message.

Add Stripe

With the base project ready, let's add Stripe. Install the latest version:

(env)$ pip install stripe

Next, register for a Stipe account (if you haven't already done so) and navigate to the dashboard. Click on "Developers" and then from the list in the left sidebar click on "API keys":

Stripe Developers Key

Each Stripe account has four API keys: two for testing and two for production. Each pair has a "secret key" and a "publishable key". Do not reveal the secret key to anyone; the publishable key will be embedded in the JavaScript on the page that anyone can see.

Currently the toggle for "Viewing test data" in the upper right indicates that we're using the test keys now. That's what we want.

Store your test API keys as environment variables like so:

(env)$ export STRIPE_PUBLISHABLE_KEY=<YOUR_STRIPE_PUBLISHABLE_KEY>
(env)$ export STRIPE_SECRET_KEY=<YOUR_STRIPE_SECRET_KEY>

Next, add the Stripe keys to your app:

# app.py

import os

import stripe
from flask import Flask, jsonify

app = Flask(__name__)

stripe_keys = {
    "secret_key": os.environ["STRIPE_SECRET_KEY"],
    "publishable_key": os.environ["STRIPE_PUBLISHABLE_KEY"],
}

stripe.api_key = stripe_keys["secret_key"]


@app.route("/hello")
def hello_world():
    return jsonify("hello, world!")


if __name__ == "__main__":
    app.run()

Finally specify an "Account name" within your "Account settings" at https://dashboard.stripe.com/settings/account.

Stripe Account Name

Create a Product

Next, let's create a subscription product to sell.

Click "Products" and then "Add product".

Add a product name and description, enter a price, and select "Recurring":

Stripe Add Product

Click "Save product".

Next, grab the API ID of the price:

Stripe Add Product

Save the ID as an environment variable like so:

(env)$ export STRIPE_PRICE_ID=<YOUR_PRICE_API_ID>

Next, add it in the stripe_keys dictionary like so:

# app.py

stripe_keys = {
    "secret_key": os.environ["STRIPE_SECRET_KEY"],
    "publishable_key": os.environ["STRIPE_PUBLISHABLE_KEY"],
    "price_id": os.environ["STRIPE_PRICE_ID"],  # new
}

Authentication

In order to associate your users with Stripe customers and implement subscription management in the future, you'll probably want to enforce user authentication before allowing customers to subscribe to the service. Flask-Login or Flask-HTTPAuth are two extensions you can use to manage this.

Check out this resource for a full list of auth-related extensions for Flask.

Along with authentication, you'll want to store some information in a database related to the customer. Your models will look something like this:

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    email = db.Column(db.String(128), nullable=False, unique=True)
    password = db.Column(db.String(200), nullable=False)
    created_on = db.Column(db.DateTime, default=func.now(), nullable=False)


class StripeCustomer(db.Model):
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    user_id = database.Column(database.Integer, database.ForeignKey('users.id'))
    stripeCustomerId = db.Column(db.String(255), nullable=False)
    stripeSubscriptionId = db.Column(db.String(255), nullable=False)

Go ahead and set this up now if you'd like.

Get Publishable Key

JavaScript Static File

Create a new folder called "static" and then add a new file to that folder called main.js:

(env)$ mkdir static
(env)$ touch static/main.js

Add a quick sanity check to the new main.js file:

// static/main.js

console.log("Sanity check!");

Next, register a new route to app.py that serves up the index.html template:

# app.py

@app.route("/")
def index():
    # you should force the user to log in/sign up
    return render_template("index.html")

Don't forget to import render_template at the top of the file like so:

from flask import Flask, jsonify, render_template

For the template, create a new folder called "templates" inside the project root. Inside of that folder, create a new file called index.html and put the following content inside:

<!-- templates/index.html -->

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Flask + Stripe Subscriptions</title>
    <script src="https://code.jquery.com/jquery-3.5.1.min.js" crossorigin="anonymous"></script>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" crossorigin="anonymous">
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" crossorigin="anonymous"></script>
    <script src="{{ url_for('static', filename='main.js') }}"></script>
  </head>
  <body>
    <div class="container mt-5">
      <button type="submit" class="btn btn-primary" id="submitBtn">Subscribe</button>
    </div>
  </body>
</html>

Run the server again:

(env)$ FLASK_ENV=development python app.py

Navigate to http://localhost:5000/, and open up the JavaScript console. You should see the sanity check inside your console.

JavaScript Sanity Check

Route

Next, add a new route to app.py to handle the AJAX request:

# app.py

@app.route("/config")
def get_publishable_key():
    stripe_config = {"publicKey": stripe_keys["publishable_key"]}
    return jsonify(stripe_config)

AJAX Request

Next, use the Fetch API to make an AJAX request to the new /config endpoint in static/main.js:

// static/main.js

console.log("Sanity check!");

// new
// Get Stripe publishable key
fetch("/config")
.then((result) => { return result.json(); })
.then((data) => {
  // Initialize Stripe.js
  const stripe = Stripe(data.publicKey);
});

The response from a fetch request is a ReadableStream. result.json() returns a promise, which we resolved to a JavaScript object -- i.e., data. We then used dot-notation to access the publicKey in order to obtain the publishable key.

Include Stripe.js in templates/index.html like so:

<!-- templates/index.html -->

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Flask + Stripe Subscriptions</title>
    <script src="https://js.stripe.com/v3/"></script>  <!-- new -->
    <script src="https://code.jquery.com/jquery-3.5.1.min.js" crossorigin="anonymous"></script>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" crossorigin="anonymous">
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" crossorigin="anonymous"></script>
    <script src="{{ url_for('static', filename='main.js') }}"></script>
  </head>
  <body>
    <div class="container mt-5">
      <button type="submit" class="btn btn-primary" id="submitBtn">Subscribe</button>
    </div>
  </body>
</html>

Now, after the page load, a call will be made to /config, which will respond with the Stripe publishable key. We'll then use this key to create a new instance of Stripe.js.

Create Checkout Session

Moving on, we need to attach an event handler to the button's click event which will send another AJAX request to the server to generate a new Checkout Session ID.

Route

First, add the new route:

# app.py

@app.route("/create-checkout-session")
def create_checkout_session():
    domain_url = "http://localhost:5000/"
    stripe.api_key = stripe_keys["secret_key"]

    try:
        checkout_session = stripe.checkout.Session.create(
            # you should get the user id here and pass it along as 'client_reference_id'
            #
            # this will allow you to associate the Stripe session with
            # the user saved in your database
            #
            # example: client_reference_id=user.id,
            success_url=domain_url + "success?session_id={CHECKOUT_SESSION_ID}",
            cancel_url=domain_url + "cancel",
            payment_method_types=["card"],
            mode="subscription",
            line_items=[
                {
                    "price": stripe_keys["price_id"],
                    "quantity": 1,
                }
            ]
        )
        return jsonify({"sessionId": checkout_session["id"]})
    except Exception as e:
        return jsonify(error=str(e)), 403

The full file should now look like this:

# app.py

import os

import stripe
from flask import Flask, jsonify, render_template

app = Flask(__name__)

stripe_keys = {
    "secret_key": os.environ["STRIPE_SECRET_KEY"],
    "publishable_key": os.environ["STRIPE_PUBLISHABLE_KEY"],
    "price_id": os.environ["STRIPE_PRICE_ID"],
}

stripe.api_key = stripe_keys["secret_key"]


@app.route("/hello")
def hello_world():
    return jsonify("hello, world!")


@app.route("/")
def index():
    # you should force the user to log in/sign up
    return render_template("index.html")


@app.route("/config")
def get_publishable_key():
    stripe_config = {"publicKey": stripe_keys["publishable_key"]}
    return jsonify(stripe_config)


@app.route("/create-checkout-session")
def create_checkout_session():
    domain_url = "http://localhost:5000/"
    stripe.api_key = stripe_keys["secret_key"]

    try:
        checkout_session = stripe.checkout.Session.create(
            # you should get the user id here and pass it along as 'client_reference_id'
            #
            # this will allow you to associate the Stripe session with
            # the user saved in your database
            #
            # example: client_reference_id=user.id,
            success_url=domain_url + "success?session_id={CHECKOUT_SESSION_ID}",
            cancel_url=domain_url + "cancel",
            payment_method_types=["card"],
            mode="subscription",
            line_items=[
                {
                    "price": stripe_keys["price_id"],
                    "quantity": 1,
                }
            ]
        )
        return jsonify({"sessionId": checkout_session["id"]})
    except Exception as e:
        return jsonify(error=str(e)), 403


if __name__ == "__main__":
    app.run()

AJAX Request

Add the event handler and subsequent AJAX request to static/main.js:

// static/main.js

console.log("Sanity check!");

// Get Stripe publishable key
fetch("/config")
.then((result) => { return result.json(); })
.then((data) => {
  // Initialize Stripe.js
  const stripe = Stripe(data.publicKey);

  // new
  // Event handler
  document.querySelector("#submitBtn").addEventListener("click", () => {
    // Get Checkout Session ID
    fetch("/create-checkout-session")
    .then((result) => { return result.json(); })
    .then((data) => {
      console.log(data);
      // Redirect to Stripe Checkout
      return stripe.redirectToCheckout({sessionId: data.sessionId})
    })
    .then((res) => {
      console.log(res);
    });
  });
});

Here, after resolving the result.json() promise, we called redirectToCheckout with the Checkout Session ID from the resolved promise.

Navigate to http://localhost:5000/. On button click you should be redirected to an instance of Stripe Checkout (a Stripe-hosted page to securely collect payment information) with the subscription information:

Stripe Checkout

We can test the form by using one of the several test card numbers that Stripe provides. Let's use 4242 4242 4242 4242. Make sure the expiration date is in the future. Add any 3 numbers for the CVC and any 5 numbers for the postal code. Enter any email address and name. If all goes well, the payment should be processed and you should be subscribed, but the redirect will fail since we have not set up the /success/ URL yet.

User Redirect

Next, we'll create success and cancel views and redirect the user to the appropriate page after checkout.

Routes:

# app.py

@app.route("/success")
def success():
    return render_template("success.html")


@app.route("/cancel")
def cancelled():
    return render_template("cancel.html")

Create the success.html and cancel.html templates as well.

Success:

<!-- templates/success.html -->

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Flask + Stripe Subscriptions</title>
    <script src="https://code.jquery.com/jquery-3.5.1.min.js" crossorigin="anonymous"></script>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" crossorigin="anonymous">
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" crossorigin="anonymous"></script>
  </head>
  <body>
    <div class="container mt-5">
      <p>You have successfully subscribed!</p>
    </div>
  </body>
</html>

Cancel:

<!-- templates/cancel.html -->

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Flask + Stripe Subscriptions</title>
    <script src="https://code.jquery.com/jquery-3.5.1.min.js" crossorigin="anonymous"></script>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" crossorigin="anonymous">
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" crossorigin="anonymous"></script>
  </head>
  <body>
    <div class="container mt-5">
      <p>You have cancelled the checkout.</p>
    </div>
  </body>
</html>

The user should now be redirected to /success if the payment goes through and cancel/ if the payment fails. Test this out.

Stripe Webhooks

Our app works well at this point, but we still can't programmatically confirm payments. In the future we'll also need to save relevant information to the database when a customer subscribes successfully. We already redirect the user to the success page after they check out, but we can't rely on that page alone since payment confirmation happens asynchronously.

There are two types of events in Stripe and programming in general. Synchronous events, which have an immediate effect and results (e.g., creating a customer), and asynchronous events, which don't have an immediate result (e.g., confirming payments). Because payment confirmation is done asynchronously, the user might get redirected to the success page before their payment is confirmed and before we receive their funds.

One of the easiest ways to get notified when the payment goes through is to use a callback or so-called Stripe webhook. We'll need to create a simple endpoint in our application, which Stripe will call whenever an event occurs (e.g., when a user subscribes). By using webhooks, we can be absolutely sure that the payment went through successfully.

In order to use webhooks, we need to:

  1. Set up the webhook endpoint
  2. Test the endpoint using the Stripe CLI
  3. Register the endpoint with Stripe

Endpoint

Create a new route called stripe_webhook which will print a message every time someone subscribes.

If you added authentication, you should create a record in the StripeCustomer table rather than printing a message.

# app.py

@app.route("/webhook", methods=["POST"])
def stripe_webhook():
    payload = request.get_data(as_text=True)
    sig_header = request.headers.get("Stripe-Signature")

    try:
        event = stripe.Webhook.construct_event(
            payload, sig_header, stripe_keys["endpoint_secret"]
        )

    except ValueError as e:
        # Invalid payload
        return "Invalid payload", 400
    except stripe.error.SignatureVerificationError as e:
        # Invalid signature
        return "Invalid signature", 400

    # Handle the checkout.session.completed event
    if event["type"] == "checkout.session.completed":
        session = event["data"]["object"]

        # Fulfill the purchase...
        handle_checkout_session(session)

    return "Success", 200


def handle_checkout_session(session):
    # here you should fetch the details from the session and save the relevant information
    # to the database (e.g. associate the user with their subscription)
    print("Subscription was successful.")

Don't forget to import request at the top of the file like so:

from flask import Flask, jsonify, render_template, request

stripe_webhook now serves as our webhook endpoint. Here, we're only looking for checkout.session.completed events which are called whenever a checkout is successful, but you can use the same pattern for other Stripe events.

Testing the webhook

We'll use the Stripe CLI to test the webhook.

Once downloaded and installed, run the following command in a new terminal window to log in to your Stripe account:

$ stripe login

This command should generate a pairing code:

Your pairing code is: peach-loves-classy-cozy
This pairing code verifies your authentication with Stripe.
Press Enter to open the browser (^C to quit)

By pressing Enter, the CLI will open your default web browser and ask for permission to access your account information. Go ahead and allow access. Back in your terminal, you should see something similar to:

> Done! The Stripe CLI is configured for Flask Test with account id acct_<ACCOUNT_ID>

Please note: this key will expire after 90 days, at which point you'll need to re-authenticate.

Next, we can start listening to Stripe events and forward them to our endpoint using the following command:

$ stripe listen --forward-to localhost:5000/webhook

This will also generate a webhook signing secret:

> Ready! Your webhook signing secret is whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (^C to quit)

In order to initialize the endpoint, set a new environment variable:

(env)$ export STRIPE_ENDPOINT_SECRET=<YOUR_STRIPE_ENDPOINT_SECRET>

Next, add it in the stripe_keys dictionary like so:

# app.py

stripe_keys = {
    "secret_key": os.environ["STRIPE_SECRET_KEY"],
    "publishable_key": os.environ["STRIPE_PUBLISHABLE_KEY"],
    "price_id": os.environ["STRIPE_PRICE_ID"],
    "endpoint_secret": os.environ["STRIPE_ENDPOINT_SECRET"],  # new
}

Stripe will now forward events to our endpoint. To test, run another test payment through with 4242 4242 4242 4242. In your terminal, you should see the Subscription was successful. message.

Once done, stop the stripe listen --forward-to localhost:5000/webhook process.

Register the endpoint

Finally, after deploying your app, you can register the endpoint in the Stripe dashboard, under Developers > Webhooks. This will generate a webhook signing secret for use in your production app.

For example:

Flask webhook

Fetch Subscription Data

If you added authentication, you'll probably want to fetch a user's subscription data and display it to them.

For example:

@app.route("/")
@login_required  # force the user to log in/sign up
def index():
    # check if a record exists for them in the StripeCustomer table
    customer = StripeCustomer.query.filter_by(user_id=current_user.id).first()

    # if record exists, add the subscription info to the render_template method
    if customer:
        subscription = stripe.Subscription.retrieve(customer.stripeSubscriptionId)
        product = stripe.Product.retrieve(subscription.plan.product)
        context = {
            "subscription": subscription,
            "product": product,
        }
        return render_template("index.html", **context)

    return render_template("index.html")

Here, if a StripeCustomer exists, we use the subscriptionId to fetch the customer's subscription and product info from the Stripe API.

Next, modify the index.html template to display the current plan to subscribed users:

<!-- templates/index.html -->

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Flask + Stripe Subscriptions</title>
    <script src="https://js.stripe.com/v3/"></script>
    <script src="https://code.jquery.com/jquery-3.5.1.min.js" crossorigin="anonymous"></script>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" crossorigin="anonymous">
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" crossorigin="anonymous"></script>
    <script src="{{ url_for('static', filename='main.js') }}"></script>
  </head>
  <body>
    <div class="container mt-5">
      {% if subscription and subscription.status == "active" %}
        <h4>Your subscription:</h4>
        <div class="card" style="width: 18rem;">
          <div class="card-body">
            <h5 class="card-title">{{ product.name }}</h5>
            <p class="card-text">
              {{ product.description }}
            </p>
          </div>
        </div>
      {% else %}
        <button type="submit" class="btn btn-primary" id="submitBtn">Subscribe</button>
      {% endif %}
    </div>
  </body>
</html>

Our subscribed customers will now see their current subscription plan, while others will still see the subscribe button:

Subscribed View

Restricting user access

If you want to restrict access to specific views to only subscribed users, you can fetch the subscription as we did in the previous step and check if subscription.status == "active". By performing this check you will make sure the subscription is still active, which means that it has been paid and hasn't been cancelled.

Other possible subscription statuses are incomplete, incomplete_expired, trialing, active, past_due, canceled, or unpaid.

Conclusion

We have successfully created a Flask web application that allows users to subscribe to our service. The customers will also be billed automatically every month.

This is just the basics. You'll still need to:

  • Implement user authentication and restrict some routes
  • Create a database that holds user subscriptions
  • Allow users to manage/cancel their current plan
  • Handle future payment failures

Grab the code from the flask-stripe-subscriptions repo on GitHub.

Nik Tomazic

Nik Tomazic

Nik is a software developer from Slovenia. He's interested in object-oriented programming and web development. He likes learning new things and accepting new challenges. When he's not coding, Nik's either swimming or watching movies.

Share this tutorial

Featured Course

Test-Driven Development with Python, Flask, and Docker

In this course, you'll learn how to set up a development environment with Docker in order to build and deploy a microservice powered by Python and Flask. You'll also apply the practices of Test-Driven Development with pytest as you develop a RESTful API.

Featured Course

Test-Driven Development with Python, Flask, and Docker

In this course, you'll learn how to set up a development environment with Docker in order to build and deploy a microservice powered by Python and Flask. You'll also apply the practices of Test-Driven Development with pytest as you develop a RESTful API.