How to Integrate Stripe Checkout With Flask to Accept Payments
Get the project source code below, and follow along with the lesson material.
Download Project Source CodeTo set up the project on your local machine, please follow the directions provided in the README.md
file. If you run into any issues with running the project source code, then feel free to reach out to the author in the course's Discord channel.
Stripe Implementation
We want people to be able to actually purchase products, so we'll have to integrate with a payment processor to handle payments with credit cards. Stripe is an example of a payment processor that can help businesses accept one time payments, recurring payments, and distributing payments.
For this section, we'll go through an example of integrating with Stripe's Checkout product, which provides us with a hosted payment form that supports many different methods of payment, including payments for countries outside the US. Stripe can also handle sending receipts, fraud detection, and a lot of other tricky details around payment processing.
In the process of implementing this integration, we will see how to build our own custom Flask extension, receive webhooks, and robustly mock external APIs in tests.
High Level Implementation.
To implement Stripe Checkout, we will redirect users to a page on stripe.com
when they click purchase. Here the user will see a payment form. When a user successfully completes their transaction, Stripe will first ping our server to tell us that the transaction has completed successfully, and then will redirect users back to our site.
Signup for Stripe
Sign up for an account on Stripe.com. You can instantly get test environment credentials.
To get your Test API keys, you can navigate to dashboard.stripe.com/test/apikeys which will provide you withΒ a "publishable key" and a "secret key".
We're going to need the stripe
Python library, so add that to your requirements.txt
as well and run pip install -r requirements.txt
.
We'll also want the Stripe command line interface, which you can install at https://stripe.com/docs/stripe-cli.
Configuring our Stripe extension
In order to actually call the Stripe API, we need to set some credentials. First, let's set some defaults in the application configuration within yumroad/config.py
class BaseConfig:
...
STRIPE_SECRET_KEY = os.getenv('STRIPE_SECRET_KEY', 'sk_test_k1')
STRIPE_PUBLISHABLE_KEY = os.getenv('STRIPE_PUBLISHABLE_KEY', 'sk_test_k1')
In our terminal, we can export the actual values into our environment variables.
export STRIPE_WEBHOOK_KEY='...'
export STRIPE_PUBLISHABLE_KEY='...'
Now our Stripe credentials are accessible through the application config
attribute. Instead of directly calling the stripe
library from within our controllers, we should wrap all of our Stripe specific logic into a separate module.
In order to pass in the application configuration variables, we can follow a similar pattern to all of the extensions we have been using by creating our own custom library following the pattern that Flask extensions use to get application configuration information. By having a single instance that is already configured during the create_app
function, we will have a object that we can access from anywhere in our app without worrying about configuration.
Our first step is to create our library in a file as yumroad/payments.py
.
import stripe
class Checkout
def init_app(self, app):
# Store application config credentials & perform setup here
stripe.api_key = app.config.get('STRIPE_SECRET_KEY')
self.publishable_key = app.config.get('STRIPE_PUBLISHABLE_KEY')
def do_action(self, args):
# Perform Stripe specific actions
pass
In yumroad/extensions.py
, we'll instantiate the object.
from yumroad.payments import Checkout
checkout = Checkout()
And finally, within yumroad/__init__.py
, we will call create_app
from yumroad.extensions import (..., checkout)
def create_app(environment_name='dev'):
...
checkout.init_app(app)
If we were making this a Flask Extension used by other applications, we'd also want to support initialization by passing
app
during instantiation as that is used by applications that don't use the application factory pattern (create_app
) of setting up a Flask app.
class Checkout:
def __init__(self, app=None):
if app:
self.init_app(app)
...
# Usage: checkout = Checkout(app)
Redirecting
In order to redirect users to a checkout page, we first need to create a Session
on Stripe with the details of a product.
In order to create a session, we need to pass a few things into Stripe.
stripe.checkout.Session.create(
payment_method_types=['card'],
client_reference_id="the product id",
line_items=[{
'name': "Product Name",
'description': "A description",
'amount': 1000, # in cents
'currency': 'usd',
'images': ["https://example.com/image.png"],
'quantity': 1,
}],
mode='payment',
success_url="page to send users after success",
cancel_url="page to send users if they cancel",
)
Most of the attributes we can get directly as the attributes of the Product
model, but we don't have a specific URL to redirect users to after checkout, so we'll make one that will accept the ID of the Stripe session and whether or not the payment was successful as URL parameters.
In the products blueprint (blueprints/products.py
), add a new post_checkout
route.
from flask import ..., flash
@product_bp.route('/product/<product_id>/post_checkout')
def post_checkout(product_id):
product = Product.query.get_or_404(product_id)
purchase_state = request.args.get('status')
post_purchase_session_id = request.args.get('session_id')
if purchase_state == 'success' and post_purchase_session_id:
flash("Thanks for purchasing {}. You will receive an email shortly".format(product.name), 'success')
elif purchase_state == 'cancel' and post_purchase_session_id:
flash("There was an error while attempting to purchase this product. Try again", 'danger')
return redirect(url_for('.details', product_id=product_id))
We can't rely on the success argument here to fulfill our order as anyone could potentially load this page. Instead we'll get a webhook before Stripe redirects our users back. That allows us to safely show a success message.
The success URL that we pass to Stripe can now look something like http://oursite.com/product/1/post_checkout?status=success
.
We don't know what the ID of the session will be before we create it and since we want the success_url
to have access to the session ID, we need to tell Stripe to include it. If the string {CHECKOUT_SESSION_ID}
appears in the redirect URL that we pass into Stripe, their API will replace it with the real session ID before redirecting our users.
To generate that URL, you might think that something like this call to url_for
would work (using _external=True
to generate the full URL including the hostname).
url_for('product.post_checkout', product_id=product.id,
session_id='{CHECKOUT_SESSION_ID}',
status='success',
_external=True)
However, url_for
escapes the value of {
and }
and produces http://localhost:5000/product/1/post_checkout?session_id=%7BCHECKOUT_SESSION_ID%7D&status=success
That's not quite what we need, so to unescape the URL, we'll import a function from the built in urllib
.
from urllib.parse import unquote
unquote(url_for('product.details', product_id=product.id,
session_id='{CHECKOUT_SESSION_ID}',
status='success',
_external=True))
This will produce a redirect URL of http://localhost:5000/product/1/post_checkout?session_id={CHECKOUT_SESSION_ID}&status=success
.
Now that we have our redirect URLs, create a method called create_session
within payments.py
where we'll pass in a Product
.
from urllib.parse import unquote
from flask import url_for
import stripe
class Checkout:
...
def create_session(self, product):
if not product.price_cents:
return
success_url = unquote(url_for('product.details', product_id=product.id,
session_id='{CHECKOUT_SESSION_ID}',
status='success',
_external=True))
failure_url = unquote(url_for('product.details', product_id=product.id,
session_id='{CHECKOUT_SESSION_ID}',
status='cancel',
_external=True))
session = stripe.checkout.Session.create(
payment_method_types=['card'],
client_reference_id=product.id,
line_items=[{
'name': product.name,
'description': product.description,
'amount': product.price_cents,
'currency': 'usd',
'images': [product.primary_image_url],
'quantity': 1,
}],
mode='payment',
success_url=success_url,
cancel_url=failure_url,
)
return session
This lesson preview is part of the Fullstack Flask: Build a Complete SaaS App with Flask course and can be unlocked immediately with a single-time purchase. Already have access to this course? Log in here.
Get unlimited access to Fullstack Flask: Build a Complete SaaS App with Flask with a single-time purchase.