Accept Credit Card Payments in Flask with Stripe Checkout

Posted by
on under

In this article I'm going to show you how to implement an order page for your Flask application that you can use to sell products or services online. The solution I'm going to present to you uses Stripe Checkout, a secure and easy to use credit card charging service that integrates nicely with Flask.

This article was voted by my supporters on Patreon. Would you like to support my work, and as a thank you be able to vote on my future articles and also have access to a chat room where I hang out and answer questions? Become a Patron!

Introduction to Stripe Checkout

Stripe Checkout is service that allows developers to create payment pages that are securely hosted by Stripe. The benefit is that your application does not need to worry about managing payment information for your customers and all the security risks it implies; Stripe takes care of all those ugly details for you and makes monthly deposits of your income to your bank account (minus their fees, of course).

Here is a high-level description of the Stripe Checkout solution:

  • When the user is ready to place an order, it clicks a "Buy" or "Checkout" button in your application. This button sends a POST request to an order endpoint in your Flask back end.
  • The order endpoint uses the Stripe Python client to create a Session object, which includes the line items that the user is purchasing with their prices. The session is configured with the URLs of the order success and cancellation pages, defined by your application.
  • The response from the order endpoint is a redirect to a page hosted on the stripe.com domain associated with the session just created.
  • Stripe takes over and renders an order page for the user.
  • The user can enter their payment details and proceed with the order, in which case they are charged, and then Stripe redirects to the order success page in your Flask application.
  • Alternatively, the user may decide to cancel the order, in which case Stripe redirects to the application's order cancellation page.
  • Separately from all this, a webhook defined by your application is configured to receive Stripe events. Stripe will call this webhook when a purchase is completed, passing all the details to your application so that you can take the appropriate action to fulfill the order.

Project Setup

The first step you need to complete is to open a Stripe account, if you don't have one yet.

If you would like to get the complete code for this project, visit the flask-stripe-orders repository on GitHub. If you prefer to build the project step-by-step, then follow along.

To begin, you need to create a new project directory, which you may call flask-stripe-orders, and define a Python virtual environment for it. You are welcome to use your favorite virtualenv tool for this task.

Install the following dependencies in your virtual environment:

pip install flask stripe python-dotenv pyngrok

Next create a templates subdirectory inside flask-stripe-orders. This is where the HTML templates used by the application will be stored.

Open a new file named .env (note the leading dot) in your text editor of IDE and enter the following contents:

STRIPE_SECRET_KEY=XXXXX

Replace the XXXXX with the testing secret key assigned to your Stripe account. You can find your key in the API Keys section of the Stripe dashboard. Stripe provides two sets of credentials, one set for testing use and another for production use, and each set includes public and secret keys. During development you will use the testing secret key, which always begins with sk_test_. When you deploy your application for production use you will configure the live secret key, which begins with sk_live_, in place of the testing one.

To complete the setup portion of this tutorial, open a new file named app.py and enter the following code, which specifies the imports, the Flask application instance and the Stripe configuration:

import os
from flask import Flask, render_template, abort, redirect, request
import stripe

app = Flask(__name__)
stripe.api_key = os.environ['STRIPE_SECRET_KEY']

Product Definition

The order page will show one or more products that your customers can purchase, so you will need to have some form of product database. In a real world application you will likely have products stored in a database. For this example I have decided to take a simpler approach and define my products in a Python dictionary. Add the products dictionary shown below to the app.py file.

products = {
    'megatutorial': {
        'name': 'The Flask Mega-Tutorial',
        'price': 3900,
    },
    'support': {
        'name': 'Python 1:1 support',
        'price': 20000,
        'per': 'hour',
    },
}

With this dictionary I'm defining two products. I will be using the megatutorial and support keys as my product IDs. Each product has name and price sub-keys, with the price given as an integer in cents. Stripe supports specifying a currency, but to keep things simple I'm going to assume that all my prices are in US dollars.

The support product has an extra key called per. Thiskey indicates the term that applies to each unit of the product. This product is a support service that is charged hourly, so the value of this key is hour. This key will be used by this application to show the price more clearly to the user: instead of $200.00 the price will be shown as $200.00 per hour.

Product Catalog Page

The application needs to show the product catalog to the user, to give them an opportunity to choose what they want to buy. This is going to be an ordinary Flask page with an HTML template. Below you can see the route definition, which you need to add at the bottom of app.py:

@app.route('/')
def index():
    return render_template('index.html', products=products)

This route renders a index.html template, passing the products dictionary defined earlier as an argument. The template goes in the templates subdirectory:

<!doctype html>
<html>
  <head>
    <title>Stripe and Flask Demo</title>
    <style>
      table {
        border: 1px solid black;
      }
      table th {
        padding: 10px;
        background: #ddd;
      }
      table td {
        padding: 10px;
      }
    </style>
  </head>
  <body>
    <h1>Stripe and Flask Demo</h1>
    <table>
      <tr>
        <th>Product</th>
        <th>Price</th>
        <th>Order</th>
      </tr>
      {% for id in products %}
      <tr>
        <td>{{ products[id].name }}</td>
        <td>
          {{ "$%.2f"|format(products[id].price / 100) }} USD
          {% if products[id].per %}per {{ products[id].per }}{% endif %}</td>
        <td>
          <form method="POST" action="/order/{{ id }}">
            <input type="submit" value="Order Now!">
          </form>
        </td>
      </tr>
      {% endfor %}
    </table>
  </body>
</html>

The heart of this template is in the <table> element, with its for-loop that renders one row per product in the products dictionary. For each row the template renders the product name in the first column, the price in the second column, and a form with an order button in the third.

You may be wondering why I use a form with just a submit button to trigger an order, instead of a much simpler link. This is a nice trick that you can use to force the browser to send a POST request when the user clicks the button. With a regular link the request will go out as a GET request, which is less secure and at risk of CSRF attacks. By using a POST request as a result of a form submission, the application can then be extended to incorporate a CSRF token, which would be given as a hidden field in the form, to protect against this type of attack.

The action attribute in the form elements is set to a dynamic URL with the format /order/<product_id>. This will allow the Flask application to determine which product is being ordered.

Do you want to see how this product catalog page looks like? Open a terminal window on the project's directory, and type the following command to start the Flask web server:

$ flask run

Now you can open the http://localhost:5000 URL in your web browser to see the rendered catalog. Here is how this page looks like:

Product Catalog

Creating a Stripe Order Form

And now the fun part! When the /order/<product_id> endpoint is invoked, the Flask application has received a request from the user to buy the given product. At this point Stripe needs to be informed of what the user wants to buy, and then control needs to be transferred so that the ordering process is carried out on Stripe's servers.

To tell Stripe about the order, a Session object must be created with the order information. Once the session is created, the Flask application just needs to issue a redirect to the URL assigned to the session, which is going to be hosted by Stripe.

See below the definition of the order endpoint. This goes at the bottom of app.py:

@app.route('/order/<product_id>', methods=['POST'])
def order(product_id):
    if product_id not in products:
        abort(404)

    checkout_session = stripe.checkout.Session.create(
        line_items=[
            {
                'price_data': {
                    'product_data': {
                        'name': products[product_id]['name'],
                    },
                    'unit_amount': products[product_id]['price'],
                    'currency': 'usd',
                },
                'quantity': 1,
            },
        ],
        payment_method_types=['card'],
        mode='payment',
        success_url=request.host_url + 'order/success',
        cancel_url=request.host_url + 'order/cancel',
    )
    return redirect(checkout_session.url)

There is a lot to unpack from this code, so let's go through it step-by-step.

First of all, if the product_id is not a key in the products dictionary, then it is an unknown product, so the application returns a 404 error.

Then, a Stripe Checkout session is created, by calling the stripe.checkout.Session.create() function. The line_items argument specifies the product that the user wishes to purchase. In this application the user can only purchase one product at a time, but line_items is a list, so multiple products can be given here if the application has a shopping cart type interface that allows the user to make multiple selections. Each line item includes a price_data dictionary with the name, unit price and currency. Note that the price is given as an integer in cents, which is the format I have chosen to use in the products dictionary.

The payment_method_types argument defines what methods of payment you want to accept. In most cases you will want this set to a list with a single element set to card, but Stripe supports several other payment methods that you may want to include as well.

The mode argument specifies the type of payment for this order. For a traditional order in which the user makes a single payment, it needs to be set to payment. Stripe also supports subscriptions.

The success_url and cancel_url are set to URLs in the Flask application that display order success and cancellation pages to the user. Stripe will redirect to one of these pages when the order process ends. These URLs are currently undefined in the the Flask application, but will be added soon.

The Stripe Checkout Session object has many other options, so be sure to check the documentation to learn what other configuration options exist.

Once the session object is created, its url attribute contains the page in Stripe's servers that the application needs to redirect to so that the user is presented with the order form, so the response to the POST request is a redirect to that URL.

Let's add the order success and cancellation endpoints at the end of app.py. These endpoints render HTML templates.

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


@app.route('/order/cancel')
def cancel():
    return render_template('cancel.html')

This is the templates/success.html page:

<!doctype html>
<html>
  <head>
    <title>Stripe and Flask Demo</title>
  </head>
  <body>
    <h1>Stripe and Flask Demo</h1>
    <p>Thank you for your order!</p>
    <p><a href="/">Home</a></p>
  </body>
</html>

And here is the templates/cancel.html page:

<!doctype html>
<html>
  <head>
    <title>Stripe and Flask Demo</title>
  </head>
  <body>
    <h1>Stripe and Flask Demo</h1>
    <p>The order was canceled.</p>
    <p><a href="/">Home</a></p>
  </body>
</html>

Testing the Order Form

Can you believe that this is pretty much all you need to be able to sell stuff online?

If you are still running the Flask server from before, stop it with Ctrl-C and then start the flask run command again. This is so that all the recent changes are incorporated. Now visit the application at http://localhost:5000 on your web browser and click one of the "Buy now!" buttons to see the Stripe order form.

Stripe Order Form

The application is using the testing key in your Stripe account, which means that all transactions use pretend money. For the credit card, there is a list of credit card predefined numbers that you can use to test different scenarios. All testing card numbers accept any future date as expiration, and any 3 or 4 digit number as CVC. Here are two of the most interesting numbers:

  • 4242 4242 4242 4242: Purchases made with this card will always succeed. You will see the purchases made with this card number in the Stripe testing dashboard.
  • 4000 0000 0000 0002: This credit card number is always declined.

Adjusting Order Quantities

The session object from Stripe has a long list of options that allow you to customize different aspects of the order page. Just as an example, I'm going to show you how to allow the customer to edit order quantities on certain products. For this example application, I'm going to configure the support product, which provides a hourly support service, to let the user pick how many support hours they would like to purchase.

To make an item in the order page have editable quantities, the adjustable_quantity option needs to be added to the line item in the session. For my implementation, I decided to add this item as an optional key in the product definition. Below you can see the updated products dictionary:

products = {
    'megatutorial': {
        'name': 'The Flask Mega-Tutorial',
        'price': 3900,
    },
    'support': {
        'name': 'Python 1:1 support',
        'price': 20000,
        'per': 'hour',
        'adjustable_quantity': {
            'enabled': True,
            'minimum': 1,
            'maximum': 4,
        },
    },
}

Note that I will not be enabling quantities in the megatutorial product, so I did not make any changes to that product.

With the product now updated, the line_items argument in the session creation can be expanded to have the adjustable_quantity argument:

    checkout_session = stripe.checkout.Session.create(
        line_items=[
            {
                'price_data': {
                    'product_data': {
                        'name': products[product_id]['name'],
                    },
                    'unit_amount': products[product_id]['price'],
                    'currency': 'usd',
                },
                'quantity': 1,
                'adjustable_quantity': products[product_id].get(
                    'adjustable_quantity', {'enabled': False}),
            },
        ],
        payment_method_types=['card'],
        mode='payment',
        success_url=request.host_url + 'order/success',
        cancel_url=request.host_url + 'order/cancel',
    )

The logic here sets adjustable_quantity for a given product to the contents of the key with that name in the product definition. If the product does not have this key, then a default value that disables adjustable quantities is used.

Restart the Flask server and make a purchase for the support product to see how the order form now allows you to purchase up to 4 units of this product.

Order Fulfillment with a Webhook

If all you need is to accept money, then you might have a complete solution already. In many cases, however, a customer purchase needs to be followed by some action. If you are selling digital products, maybe you want to send the customer an email with a download link. If you sell physical items, you may want to update your database so that the order is in your system and can move to the fulfillment stage.

Stripe can help automate this type of processing as well. Your application can define a webhook that Stripe invokes when a significant event such as a new order occurs. The webhook will allow your application to react to events related to your Stripe account.

The problem that presents to test webhooks is that during development the Flask application runs privately inside your own computer on http://localhost:5000, so a webhook in this application is not reachable by Stripe. There is a way to work around this problem with the Stripe CLI. Another option is to use the ngrok utility. You may have noticed that I asked you to install the pyngrok package (a Python friendly ngrok wrapper) at the start of the article, so this is the method that I'm going to use.

Running ngrok

Ngrok is a cool little utility that allocates a temporary public URL for your local web server. While ngrok is running, your local server can receive requests from anywhere in the world, through ngrok's public URL.

Let's give ngrok a try. Make sure you are running the Flask web server as shown above. Then open a second terminal window on your project, activate the virtual environment and start ngrok as follows:

ngrok http 5000

This tells ngrok to allocate a public URL for an HTTP web server running on your local port 5000. Ngrok's output is going to look more or less like this:

ngrok by @inconshreveable                                                                              (Ctrl+C to quit)

Session Status                online
Session Expires               1 hour, 59 minutes
Update                        update available (version 2.3.40, Ctrl-U to update)
Version                       2.3.35
Region                        United States (us)
Web Interface                 http://127.0.0.1:4040
Forwarding                    http://954a-78-18-108-95.ngrok.io -> http://localhost:5000
Forwarding                    https://954a-78-18-108-95.ngrok.io -> http://localhost:5000

Connections                   ttl     opn     rt1     rt5     p50     p90
                              0       0       0.00    0.00    0.00    0.00

Note the two "Forwarding" lines, which show the public URL that was allocated for your web server, in http:// and https:// forms. Normally you will want to use the https:// URL.

Pay attention to the "Session Expires" line, which tells you how much longer this URL is going to be valid for. Each time you run ngrok a new randomly generated URL is allocated (unless you become a paid customer of theirs, in which case you can have your own fixed URL).

If you want to quickly test this out, go to a different computer or your phone and enter the https:// URL shown in the ngrok output. This should give you access to the application, identically to how you accessed it through http://localhost:5000 on the local computer.

Creating a Stripe webhook

As mentioned above, Stripe can be configured to invoke an application defined webhook for interesting events. Below you can see a very simple webhook that handles new orders by printing the order contents to the terminal. Add this endpoint at the bottom of app.py:

@app.route('/event', methods=['POST'])
def new_event():
    event = None
    payload = request.data
    signature = request.headers['STRIPE_SIGNATURE']

    try:
        event = stripe.Webhook.construct_event(
            payload, signature, os.environ['STRIPE_WEBHOOK_SECRET'])
    except Exception as e:
        # the payload could not be verified
        abort(400)

    if event['type'] == 'checkout.session.completed':
      session = stripe.checkout.Session.retrieve(
          event['data']['object'].id, expand=['line_items'])
      print(f'Sale to {session.customer_details.email}:')
      for item in session.line_items.data:
          print(f'  - {item.quantity} {item.description} '
                f'${item.amount_total/100:.02f} {item.currency.upper()}')

    return {'success': True}

This endpoint has two parts. In the first part, a verification is carried out, to ensure that the data that was passed is legitimate. All webhook invocations from Stripe include a cryptographic signature, which needs to be verified before the data is trusted. The construct_event() function from the Stripe Python library takes care of this, returning a validated event object (or raising an exception when the signature cannot be validated).

To validate Stripe signatures, a "webhook secret" that is different for every Stripe account must be used. Not only each account has its own secret, but there are also different secrets for the testing and production sections of each account. You can see that this secret is imported from an environment variable, as os.environ['STRIPE_WEBHOOK_SECRET']. Keep that in mind, because this endpoint is not going to work until this environment variable is configured.

The second part of the endpoint checks that the event being reported is of type checkout.session.completed, which corresponds to completed orders. In that case I use the Stripe API to retrieve the Session object that corresponds to the order being notified. By default Session objects are returned in a compact format, which does not include the line items. I'm requesting this attribute to be "expanded", because I want to print the items that are part of the order.

Configuring a Stripe Webhook

In this section you are going to tell Stripe the URL of the webhook defined in the previous seciton. Navigate to the Webhook Configuration page, and make sure that the "test mode" switch is enabled.

Click the "Add an endpoint" button, and then enter the ngrok https:// URL with /event added at the end in the "Endpoint URL" field. For example, with my own ngrok session that you see above, the endpoint URL that I need to use is https://954a-78-18-108-95.ngrok.io/event.

Click the "Select events" button, expand the "Checkout" section and check the checkout.session.completed event and then press the "Add endpoint" button to save the webhook configuration.

The webhook page now has a "Signing secret" section, with a "Click to reveal" button. Reveal the webhook secret, copy it to the clipboard, then open your .env file and add it below the Stripe secret key as follows:

STRIPE_WEBHOOK_SECRET=XXXXX

If you are currently running the Flask server, stop it with Ctrl-C and then restart it, so that the new environment variable is imported. It is not necessary to restart ngrok.

Now open the application in your web browser (either on http://localhost:5000 or on the ngrok public URL) and place a new order. Keep an eye on the Flask's web server output, which will receive the webhook invocation. The product that you ordered along with the customer email should be printed to the terminal:

(venv) stripe-orders $ flask run
 * Serving Flask app 'app.py' (lazy loading)
 * Environment: development
 * Debug mode: on
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 305-445-796
127.0.0.1 - - [30/Sep/2021 17:49:03] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [30/Sep/2021 17:49:03] "GET /favicon.ico HTTP/1.1" 404 -
127.0.0.1 - - [30/Sep/2021 17:49:05] "POST /order/megatutorial HTTP/1.1" 302 -
Sale to miguel@example.com:
  - 1 The Flask Mega-Tutorial $39.00 USD
127.0.0.1 - - [30/Sep/2021 17:49:28] "POST /event HTTP/1.1" 200 -
127.0.0.1 - - [30/Sep/2021 17:49:29] "GET /order/success HTTP/1.1" 200 -

At this point you are ready to customize the webhook to perform any actions as required by your own ordering process.

Deplying to Production

So far all the work that I've done has been under Stripe's "test mode". This is very convenient, because it allows you to place unlimited number of test orders, until you can be certain that your application works as expected.

When you deploy your application on a production server, you will have to edit the .env file and switch the Stripe secret key and the webhook secret to the production values. You can obtain your production key in the API Keys section of the configuration. To access the webhook secret you will first need to define the production version of your webhook, using your domain instead of ngrok's temporary URL. Your application must be deployed on https://, as Stripe does not allow http:// webhooks.

Handling Sales Tax

While sales tax is a topic that is largely non-technical, I think it is important to briefly discuss it, because like it or not you will have to consider it if you plan on setting up an online business.

Sales tax is an extremely complex matter, made even worse by the fact that different countries (or even states in the US) have their own laws and regulations. Some of the options in the Session object from Stripe allow you to configure the collection of sales tax. Stripe can calculate the tax for you based on the customer's location, which it extracts from their IP address or from the shipping details if you have that information in the order form. This service has additional fees, but it can be invaluable if you plan on selling worldwide. If you prefer to calculate your own sales tax, you can just pass your calculation to the Session object and Stripe will just add the amount to the orders, without additional fees.

Regardless of who calculates the tax, Stripe will just collect the money from your customers, and pass the money on to you. So to be perfectly clear, Stripe does not make sales tax payments on your behalf, you will need to create a tax presence in all the countries your customers live, and make your tax payments to them. If you are selling in your own country, it may be relatively easy to manage these tax payments yourself, but if you sell everywhere this can become a major headache, and the only reasonable way to proceed is to pay a third party to do it for you. Stripe can connect you with a tax filing partner if you don't have an accountant that can take on this work.

Conclusion

I hope you now have the basic knowledge to create your own ordering system. For your reference, the complete project is available in the flask-stripe-orders repository on GitHub.

Let me know below in the comments if you have any questions!

Become a Patron!

Hello, and thank you for visiting my blog! If you enjoyed this article, please consider supporting my work on this blog on Patreon!

13 comments
  • #1 Ben said

    Amazing! Would be great to see an advanced version of this working with login pages to track orders or possibly even working with React as a frontend.

  • #2 Jonas said

    Thank you, great article!

  • #3 Adam Lea said

    I would most definitely pay for a complete Flask/Stripe course. I've tried a couple, but they lacked your steady, thorough teaching style, which really clicks with me. What do you think?

  • #4 Miguel Grinberg said

    @Adam: Unfortunately I don't think this topic has enough interesting stuff to be expanded. My hope was that this would provide enough detail that you'll be able to implement your project on your own. The Stripe docs are very thorough, by the way, they are a great resource.

  • #5 Chadler said

    Thanks Miguel!

  • #6 Gitau Harrison said

    Thank you for sharing!

  • #7 Andy said

    Just what i was looking for. Thanks Miguel for all your tutorials they have been invaluable!

  • #8 Chibole said

    Thank you for the nice tutorial. I have a question: What is the alternative to Stripe for countries that do not support it? Can the same be implemented without using Stripe?

  • #9 Miguel Grinberg said

    @Chibole: I'm not sure. PayPal also has an API, so you could code an equivalent solution with it.

  • #10 Vaibhav said

    Thanks for help.You helped me in completing my project.

  • #11 Sandeep said

    Thanks, Miguel. I was wondering how do I restrict users typing in the "success" URL directly. Would you have any help around that? I was trying to redirect users to a page only after successful purchase.

  • #12 Miguel Grinberg said

    @Sandeep: You can write something in the user session at the time the payment is completed. Then in the thank you page you check that this value exists in the session, and if it isn't you redirect away from the page.

  • #13 hermit9 said

    @Miguel Great tutorial. Helped me get started easily.
    @Sandeep AFAICT, the success page is just a static page, so there's really no harm in user typing it directly. The important step is that you store the order id, link it with the user, and then verify that it is paid when the webhook is invoked. This way the user cannot tamper with the payment flow.

Leave a Comment