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:
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:
- Redirect the user to Stripe Checkout (with
mode=subscription
) - Create a webhook that listens for
checkout.session.completed
- 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:
- Redirect the user to Stripe Checkout (with
mode=setup
) to collect payment information - Create a webhook that listens for
checkout.session.completed
- After the webhook is called, save relevant data to your database
- 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:
- billing cycle anchor (timestamp of subscription's creation)
- 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
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":
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.
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":
Click "Save product".
Next, grab the API ID of the price:
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.
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:
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:
- Set up the webhook endpoint
- Test the endpoint using the Stripe CLI
- 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:
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:
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
, orunpaid
.
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.