flask and stripe

Adding a Custom Stripe Checkout to a Flask App

Adding a Custom Stripe Checkout to a Flask App




In this tutorial we're going to look at how to add a custom Stripe checkout to a Flask application for processing payments.

Contents

Initial Setup

The first step is to set up a basic Python environment and install Flask. Create a new project folder, activate a virtual environment, and install Flask:

$ mkdir flask-stripe-checkout
$ python3.7 -m venv env
$ source env/bin/activate
(env)$ pip install flask

The above commands may differ depending on your OS as well as your Python virtual environment tool (i.e., venv, virtualenvwrapper, Pipenv).

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

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

You should see "hello, world!" in your browser at http://127.0.0.1:5000/hello.

Add Stripe

Time for Stripe. Install the Stripe library:

(env)$ pip install stripe

Register a new Stripe account, if you don't already have one. Then, store your test API keys as environment variables.

stripe dashboard

$ export STRIPE_PUBLISHABLE_KEY=<YOUR_STRIPE_PUBLISHABLE_KEY>
$ export STRIPE_SECRET_KEY=<YOUR_STRIPE_SECRET_KEY>

Add the basic Stripe configuration to the app:

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()

Stripe Checkout

With the API keys in place, we can now use Stripe Checkout to process payments.

Add the route:

@app.route('/')
def index():
    return render_template('index.html', key=stripe_keys['publishable_key'])

This renders a template called index.html along with Stripe's publishable key, which will be used later on in the checkout form.

Make sure to import render_template:

from flask import Flask, jsonify, render_template

For the template, first add a new folder called "templates", and then add a base template called base.html:

<!DOCTYPE html>
<html>
<head>
  <title>Stripe Checkout</title>
</head>
<body>
  {% block content %}{% endblock %}
</body>
</html>

Now, add the checkout form to a new template called index.html:

{% extends "base.html" %}

{% block content %}
  <div>
    <h2>Buy for $5.00</h2>
    <form action="/charge" method="post">
      <script
        src="https://checkout.stripe.com/checkout.js"
        class="stripe-button"
        data-key="{{ key }}"
        data-description="A Flask Charge"
        data-amount="500"
        data-locale="auto">
      </script>
    </form>
  </div>
{% endblock %}

This uses the key variable that that we passed through to the template to set Stripe's publishable key. Take note of the remaining attributes. Refer to the Configuration options for more info on these attributes.

Run the server, and navigate to http://127.0.0.1:5000. You should see:

flask checkout

Click the "Pay with Card" button to view the fancy credit card from, styled by Stripe for us.

flask checkout

Take note of the opening form HTML tag:

<form action="/charge" method="post">

On submit, a POST request will be sent to the server-side, to a route called /charge. Add the Flask route to handle this request:

@app.route('/charge', methods=['POST'])
def charge():

    # amount in cents
    amount = 500

    customer = stripe.Customer.create(
        email='[email protected]',
        source=request.form['stripeToken']
    )

    stripe.Charge.create(
        customer=customer.id,
        amount=amount,
        currency='usd',
        description='Flask Charge'
    )

    return render_template('charge.html', amount=amount)

Import request:

from flask import Flask, jsonify, render_template, request

Stripe Flow:

  1. On form submit, the credit card info and publishable key are sent to Stripe
  2. Stripe then stores this info securely and sends back a unique token
  3. That token is then sent to the server (POST request to the /charge route)
  4. In the route handler, we-
    • Use the token (stripeToken), customer email, and secret key to create a customer on Stripe
    • Process a charge with the customer id, amount, currency, description, and secret key

Add the charge.html template:

{% extends "base.html" %}

{% block content %}
  <h2>Thanks, you paid <strong>$5.00</strong>!</h2>
{% endblock %}

Back in your browser at http://127.0.0.1:5000, test the form using one of the test card numbers.

flask checkout

This should redirect to the charge page. You should be able to see a customer along with the charge (under "Payments") in the Stripe dashboard.

stripe dashboard

You should handle any Stripe exceptions as well. Basic example:

@app.route('/charge', methods=['POST'])
def charge():
    try:
        amount = 500   # amount in cents
        customer = stripe.Customer.create(
            email='[email protected]',
            source=request.form['stripeToken']
        )
        stripe.Charge.create(
            customer=customer.id,
            amount=amount,
            currency='usd',
            description='Flask Charge'
        )
        return render_template('charge.html', amount=amount)
    except stripe.error.StripeError:
        return render_template('error.html')

Add the error template, error.html, to the "templates" folder:

{% extends "base.html" %}

{% block content %}
  <h2>Something went wrong.</h2>
{% endblock %}

Finally, in a real application, you'll want to use the user's actual email address rather than a dummy email. Implement this on your own.

To review, we used the publishable key to send a customer's credit card information to Stripe. The Stripe API then sent us back a unique token, which we used alongside our secret key on the server to create a customer and a subsequent charge.

You can find the code for the simple checkout here.

Custom Stripe Checkout

Want to customize the look a feel of things? You'll need to use the custom Stripe Checkout integration. Let's add a custom button.

Update the base template, adding in Bootstrap, a link to a custom stylesheet, and a block for scripts:

<!DOCTYPE html>
<html>
  <head>
    <title>Stripe Checkout</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='main.css') }}">
    <link rel="stylesheet" href="{{ url_for('static', filename='bootstrap.min.css') }}">
  </head>
  <body>
    <div class="container">
      {% block content %}{% endblock %}
    </div>
    {% block scripts %}{% endblock %}
  </body>
</html>

Be sure to download the Bootstrap stylesheet and add it to a new folder called "static". Then, add a main.css file to that same folder:

.container {
  padding-top: 40px;
}

Next, update index.html:

{% extends "base.html" %}

{% block content %}
  <div>
    <h2>Buy for $5.00</h2>
    <button type="button" class="btn btn-primary" id="custom-button">Pay with Card</button>
  </div>
{% endblock %}

{% block scripts %}
  <script src="https://checkout.stripe.com/checkout.js"></script>
  <script>
    var handler = StripeCheckout.configure({
      key: "{{ key }}",
      image: 'https://stripe.com/img/documentation/checkout/marketplace.png',
      locale: "auto",
      token: function(token) {
        console.log(token);
      }
    });

    document.getElementById("custom-button").addEventListener("click", function(e) {
      // Open Checkout with further options:
      handler.open({
        name: "TestDriven.io",
        description: "A Flask Charge",
        amount: 500
      });
      e.preventDefault();
    });

    // Close Checkout on page navigation:
    window.addEventListener("popstate", function() {
      handler.close();
    });
  </script>
{% endblock %}

On page load, we pass the publishable key and a few other configuration variables to StripeCheckout.configure, which is then used to create a handler object. When the "Pay with Card" button is clicked, open is called and the options passed to it--e.g., name, description, and amount--are used to populate the checkout form.

amount, passed to the open method above, is the charge amount (in cents) that's displayed to the user. It's for usability purposes only. The actual amount that the customer is charged will need to be set on the server-side, in the /charge POST route handler, when you actually process a charge.

Test this out in your browser at http://127.0.0.1:5000. Like before, on a successful form submission, the credit card info and publishable key are sent to Stripe. Stripe then stores the card info and sends back a token, which you can see in the JavaScript console.

flask checkout

Now, we need to send the token to the server-side in order to create the charge. Update the handler to send an AJAX request, via Fetch, in the token callback function:

var handler = StripeCheckout.configure({
  key: "{{ key }}",
  image: "https://stripe.com/img/documentation/checkout/marketplace.png",
  locale: "auto",
  token: function(token) {
    fetch("/charge", {
      method: "POST",
      headers: { "Content-Type": "application/json", },
      body: JSON.stringify({
        token: token.id,
        amount: 500,
        description: "A Flask Charge",
      }),
    })
    .then(function(response) {
      console.log(response);
      return response.json();
    })
    .then(function(jsonResponse) {
      console.log(JSON.stringify(jsonResponse));
    });
  }
});

Update the route handler:

@app.route('/charge', methods=['POST'])
def charge():
    try:
        customer = stripe.Customer.create(
            email='[email protected]',
            source=request.json['token']
        )
        stripe.Charge.create(
            customer=customer.id,
            amount=request.json['amount'],
            currency='usd',
            description=request.json['description']
        )
        return jsonify({'status': 'success!'})
    except stripe.error.StripeError:
        return jsonify({'status': 'error'}), 500

Try it out.

It should process the charge just fine, but from the end user's perspective there's no way of knowing what happened unless the JavaScript console is open. Let's fix that. Back in the template, add a Bootstrap alert to the content block:

{% block content %}
  <div>
    <h2>Buy for $5.00</h2>
    <button type="button" class="btn btn-primary" id="custom-button">Pay with Card</button>
  </div>
  <div id="status">
    <br>
    <div class="alert alert-primary" id="alert" role="alert"></div>
  </div>
{% endblock %}

To hide the alert on page load, add the following CSS properties to main.css:

#status {
  display: none;
}

Finally, update the script tag like so, taking note of the inline comments:

<script>
  var handler = StripeCheckout.configure({
    key: "{{ key }}",
    image: "https://stripe.com/img/documentation/checkout/marketplace.png",
    locale: "auto",
    token: function(token) {
      fetch("/charge", {
        method: "POST",
        headers: { "Content-Type": "application/json", },
        body: JSON.stringify({
          token: token.id,
          amount: 500,
          description: "A Flask Charge",
        }),
      })
      .then(function(response) {
        if (response.ok) {
          return response.json();
        } else {
          throw new Error('Something went wrong.');
        }
      })
      .then(function(jsonResponse) {
        // update the alert message
        document.getElementById("alert").innerText = "Thanks for purchasing!"
        // show the bootstrap alert
        document.getElementById("status").style.display = "inline";
      })
      .catch(function(err) {
        // update the alert message
        document.getElementById("alert").innerText = "Something went wrong."
        // show the bootstrap alert
        document.getElementById("status").style.display = "inline";
      });
    }
  });
  document.getElementById("custom-button").addEventListener("click", function(e) {
    // hide the bootstrap alert
    document.getElementById("status").style.display = "none";
    handler.open({
      name: "TestDriven.io",
      description: "A Flask Charge",
      amount: 500
    });
    e.preventDefault();
  });

  window.addEventListener("popstate", function() {
    handler.close();
  });
</script>

Now, the Bootstrap alert is displayed after a success or error is sent back from the server. The appropriate message is displayed as well. The alert is then hidden when the "Pay with Card" button is clicked.

flask checkout

One last thing: How can we pass the displayed amount to the checkout form from the route handler to further customize the page?

Remove the index route and add a product route in its place in app.py:

@app.route('/products/<int:product_id>')
def product(product_id):
    product = get_product(product_id)
    if product:
        product['amount_in_dollars'] = product['amount'] / 100
        return render_template(
            'index.html',
            key=stripe_keys['publishable_key'],
            product=product
        )
    return abort(404)

Add the abort import:

from flask import Flask, jsonify, render_template, request, abort

Then, add the list of products along with the get_product function:

products = [
    {
        'id': 1,
        'name': 'Something Special',
        'description': 'Something really, really special',
        'amount': 600
    },
    {
        'id': 2,
        'name': 'More Special',
        'description': 'Something even more special',
        'amount': 700
    },
]


def get_product(product_id):
    for product in products:
        if product['id'] == product_id:
            return product
    return False

Now, in the index route, which maps to /products/<product_id>, we grab the product ID from the URL parameter, pass it to the get_product helper function, and then return the product info if it exists. This info will then be used to populate the checkout form on the client-side.

In the real-world you should probably store the products in a database.

In the index template, update the <h2> like so to display the dollar amount correctly:

<h2>Buy for ${{ "{:,.2f}".format(product.amount_in_dollars) }}</h2>

Then, update the script tag again:

<script>
  var handler = StripeCheckout.configure({
    key: "{{ key }}",
    image: "https://stripe.com/img/documentation/checkout/marketplace.png",
    locale: "auto",
    token: function(token) {
      fetch("/charge", {
        method: "POST",
        headers: { "Content-Type": "application/json", },
        body: JSON.stringify({
          token: token.id,
          product: "{{ product.id }}"
        }),
      })
      .then(function(response) {
        if (response.ok) {
          return response.json();
        } else {
          throw new Error('Something went wrong.');
        }
      })
      .then(function(jsonResponse) {
        // update the alert message
        document.getElementById("alert").innerText = "Thanks for purchasing!"
        // show the bootstrap alert
        document.getElementById("status").style.display = "inline";
      })
      .catch(function(err) {
        // update the alert message
        document.getElementById("alert").innerText = "Something went wrong."
        // show the bootstrap alert
        document.getElementById("status").style.display = "inline";
      });
    }
  });
  document.getElementById("custom-button").addEventListener("click", function(e) {
    // hide the bootstrap alert
    document.getElementById("status").style.display = "none";
    handler.open({
      name: "{{ product.name }}",
      description: "{{ product.description }}",
      amount: parseInt("{{ product.amount }}")
    });
    e.preventDefault();
  });

  window.addEventListener("popstate", function() {
    handler.close();
  });
</script>

What's new?

First, in the payload body, we pass the token and product ID back:

body: JSON.stringify({
  token: token.id,
  product: "{{ product.id }}"
}),

Next, in handler.open we use the product info to populate the checkout form:

handler.open({
  name: "{{ product.name }}",
  description: "{{ product.description }}",
  amount: parseInt("{{ product.amount }}")
});

Finally, update the charge route to grab the product ID from the payload and create a new customer and charge if that product ID is vaild:

@app.route('/charge', methods=['POST'])
def charge():
    response = jsonify('error')
    response.status_code = 500
    product = get_product(int(request.json['product']))
    if product:
        try:
            product = get_product(int(request.json['product']))
            customer = stripe.Customer.create(
                email='[email protected]',
                source=request.json['token']
            )
            stripe.Charge.create(
                customer=customer.id,
                amount=product['amount'],
                currency='usd',
                description=product['description']
            )
            response = jsonify('success')
            response.status_code = 202
        except stripe.error.StripeError:
            return response
    return response

That's it. Test it out.

What's Next

Looking for some challenges?

  1. Move the products to a database
  2. Add user registration
  3. Create your own credit card form, making sure to validate the credit card info
  4. Add TLS

Grab the final code from the flask-stripe-checkout repo.





Join our mailing list to be notified about course updates and new tutorials.

 

Building Your Own Python Web Framework

Get the full course. Learn how to build your own Python web framework.

View the Course

Building Your Own Python Web Framework

Get the full course. Learn how to build your own Python web framework.


Table of Contents