Setting up Stripe payment

with Django

november 2023 - 15 min read

Content

Intro

In this article we will build a Django payment app that uses Stripe. We'll also cover testing out the payment process locally.

The payment process

What Stripe offers is an API that your backend system can interact with. The simple path to making a payment follows the following steps:

  • a user clicks on the checkout button, the intent to buy the product is initiated
  • the user then enters information such as card details and address. They confirm that they intend to pay by clicking the "pay" button
  • the Stripe API is notified and begins processing the payment
  • if everything goes well, the payment is successful and if something goes wrong, the payment fails. In either case, the user will be redirected to your custom "payment succeeded" or "payment failed" Django template

Webhooks

One thing that you can do after a successful payment goes through is let Stripe notify your server of the user's payment. This allows you to update your database records accordingly.

To let Stripe notify you of a successful payment, you can create a webhook from the Stripe dashboard. The webhook is an endpoint on your server. For example https://mydomain.com/stripe_endpoint. Here's how it works.

Stripe will listen to an event you specify such as a successful payment. It will then call the webhook you provided and depending on what you've configured your webhook to do. It can update your database to indicate that this customer ID has paid successfully.

Account setup

To start, you first need to set up your Stripe account from the Stripe dashboard. If you're only implementing this in test for now, you don't have to go through the whole Stripe account setup where you enter your bank details. But you do have to do that later on if you want to receive real payments.

From your developer dashboard, you can see 2 modes: the test mode and the live or production mode. You would want to create API keys for each mode so the test mode will have its own API keys that are different from the API keys of the production mode.

An API key is made of 1 pair, a secret key that you should not reveal and a publishable key that you can embed in your code.

The next thing to do from the developer dashboard is to create your product.

Each product is a payment you'd like to implement in your web app. For example a one-time fee is a product, a monthly recurring fee is another product.

Code implementation

Once you have your keys and product ready, you can start writing the Django code, you also want to make sure that you have the Stripe Python module installed in your environment.

pip install stripe

To start off, I've created a new Django app called user_payment, and I've added it to the project's settings.py file.

## settings.py
INSTALLED_APPS = [
    ## ...
    'user_payment.apps.UserPaymentConfig',
]

1. Models.py

In the payment app folder, create a payments model that links with the custom user model. Learn more about creating your own custom user model here.

The payment model will have the app_user field, a payment_bool field to determine if a user payment went through and a Stripe checkout ID field which we will use during the Stripe payment process. Something else to do is create a payment record everytime a new user is created. This can be done with the receiver decorator.

## user_payment/models.py
class UserPayment(models.Model):
    app_user = models.ForeignKey(AppUser, on_delete=models.CASCADE)
    payment_bool = models.BooleanField(default=False)
    stripe_checkout_id = models.CharField(max_length=500)
@receiver(post_save, sender=AppUser)
def create_user_payment(sender, instance, created, **kwargs):
    if created:
        UserPayment.objects.create(app_user=instance)

2. URLs.py

Next, create the urls.py file for the payment app. This includes 4 URLs: a product page that contains a "pay now" button which when clicked will initiate the Stripe checkout session,  a "payment successful" endpoint that will be displayed after a successful payment, a "payment failed" endpoint that will be displayed after a failed or canceled payment and a Stripe webhook endpoint.

## user_payment/urls.py
urlpatterns = [
    path('product_page', views.product_page, name='product_page'),
    path('payment_successful', views.payment_successful, name='payment_successful'),
    path('payment_cancelled', views.payment_cancelled, name='payment_cancelled'),
    path('stripe_webhook', views.stripe_webhook, name='stripe_webhook'),
]

3. Views.py

Moving on to writing the views and that's the bulk of the code. The views reflect the URLs created above.

Starting off with a product page view which has 2 options, the first being a regular GET request where the user is just loading the product_page.html file containing the "pay now" button.

## ...
@login_required(login_url='login')
def product_page(request):
    stripe.api_key = settings.STRIPE_SECRET_KEY_TEST
    if request.method == 'POST':
        pass
    return render(request, 'user_payment/product_page.html')

The second option is a POST request where the user initiates a Stripe checkout session by clicking a button. In this part we will use the Stripe module to initiate a Stripe checkout session. We set variables like payment_method_types, price and quantity.

We also set the success and cancel redirection URLs. Note that Stripe will return the checkout session ID for us as a parameter in the success URL.

## ...
@login_required(login_url='login')
def product_page(request):
    stripe.api_key = settings.STRIPE_SECRET_KEY_TEST
    if request.method == 'POST':
        checkout_session = stripe.checkout.Session.create(
            payment_method_types = ['card'],
            line_items = [
                {
                    'price': settings.PRODUCT_PRICE,
                    'quantity': 1,
                },
            ],
            mode = 'payment',
            customer_creation = 'always',
            success_url = settings.REDIRECT_DOMAIN + '/payment_successful?session_id={CHECKOUT_SESSION_ID}',
            cancel_url = settings.REDIRECT_DOMAIN + '/payment_cancelled',
        )
        return redirect(checkout_session.url, code=303)
    return render(request, 'user_payment/product_page.html')

The code snippet used here is found in the Stripe developer documentation.

It is worth mentioning that certain variables like the product price as well as the redirect domains can actually be extracted from the settings.py file where they can be exported and read as environment variables instead of being hard-coded in views.py.

The next view is the payment_succeeded view. Stripe will redirect our user to this page along with a session ID. We can use this checkout session ID to retrieve the session. We then retrieve the customer from that session. We also get the current logged in user and update their checkout session ID so we link the customer that paid to the user in our database. We then return the payment successful HTML template along with the customer object.

## ...
def payment_successful(request):
    stripe.api_key = settings.STRIPE_SECRET_KEY_TEST
    checkout_session_id = request.GET.get('session_id', None)
    session = stripe.checkout.Session.retrieve(checkout_session_id)
    customer = stripe.Customer.retrieve(session.customer)
    user_id = request.user.user_id
    user_payment = UserPayment.objects.get(app_user=user_id)
    user_payment.stripe_checkout_id = checkout_session_id
    user_payment.save()
    return render(request, 'user_payment/payment_successful.html', {'customer': customer})

The next view is payment cancelled. This one is straight forward where the user is redirected to a template that notifies them that their payment didn't go through.

## ...
def payment_cancelled(request):
    return render(request, 'user_payment/payment_cancelled.html')

The final view is for the Stripe webhook. This will be called by Stripe when an event of interest occurs.

The time.sleep() function that we use just allows some time for the transactions on Stripe to take place. We start by reading the body of the request sent from Stripe. Followed by extracting the HTTP Stripe signature that we will use to verify the source, using our webhook secret that we've stored in settings.py. We then create the webhook event and go through the verification. We will update the payment_bool field in our database for the user that paid.

## ...
@csrf_exempt
def stripe_webhook(request):
    stripe.api_key = settings.STRIPE_SECRET_KEY_TEST
    time.sleep(10)
    payload = request.body
    signature_header = request.META['HTTP_STRIPE_SIGNATURE']
    event = None
    try:
        event = stripe.Webhook.construct_event(
            payload, signature_header, settings.STRIPE_WEBHOOK_SECRET_TEST
        )
    except ValueError as e:
        return HttpResponse(status=400)
    except stripe.error.SignatureVerificationError as e:
        return HttpResponse(status=400)
    if event['type'] == 'checkout.session.completed':
        session = event['data']['object']
        session_id = session.get('id', None)
        time.sleep(15)
        user_payment = UserPayment.objects.get(stripe_checkout_id=session_id)
        user_payment.payment_bool = True
        user_payment.save()
    return HttpResponse(status=200)

Here's the code snippet from the Stripe documentation for interacting with a webhook.

4. HTML templates

Now for the HTML templates, they are pretty straight forward and I've added them to the templates folder of the django-example repo.

Local demo

So how do we test all this out? The minor inconvenience is that the webhook you create from the Stripe dashboard does not allow you to use a localhost address on which your local Django app is running, it only accepts reachable domains.

To deal with this, we have 2 options: you can either buy a staging domain and configure your webhook to this real domain as well as the redirection URLs that you use. Or you can test all this out locally in Terminal.

I will show you how you can test this out locally but I think it is also useful to have a staging domain for your Django app where you can test the payment almost identical to how it runs in production.

To get started, install the Stripe CLI package. It is simple to do and found on the Stripe CLI docs.

The Stripe CLI helps you start a local process in Terminal that receives events from your localhost via direct connection to the Stripe API.

To start that process you can use stripe listen and specify that it should forward to localhost. You can also specify your secret key for the Stripe API. This will produce a webhook secret, this secret is used in the Stripe webhook view.

stripe listen --forward-to http://127.0.0.1:8000/stripe_webhook --api-key $STRIPE_SECRET_KEY

Now when the Stripe CLI is running locally, you can attempt to do a Stripe payment from the Django app running on localhost. The transaction will occur locally mimicing the Stripe API.

Conclusion

I hope this helps you get started with using Stripe with Django. You can also explore the Stripe developer documentation where you'll find even more info and code snippets.

To view a video format of this tutorial, you can check How to use Stripe for payment in Django on Youtube.

You can also find the code on Github.