Build a payment system with Django 3.1 and Stripe from scratch. Part 4
By: Alberto Naranjo
Tags: Stripe Integration, Software Development, Python, Django, Programming

We will let Stripe know where our public endpoint is and Stripe will try several times to call to the webhook and pass the event object on every event. We can do this going to Dashboard → Developers → Webhooks We can add different endpoints to handle different type of events. For example on webhook for successful payments, another for cancelled payments or subscriptions. For every endpoint we add Stripe will generate a Signing Secret we will need to store securely in our environment as we did with the Secret Key ideally in an environment variable. Our webhook will use this secret to validate any call with event objects it receives. Again this secret is critical for you business operations, if we are not able to validate the calls or the secret was compromised a hacker or malicious user would generate fake payment events for you to process and literally steal your services or products.

Maybe we haven’t say it clearly before but these endpoints securely connected to Stripe as event webhooks need to be public. To test the endpoints in our local environment with real objects we are going to use the Stripe command line interface to listen for events and forward them to local endpoints. But first let’s setup the webhook in our app.

The Webhook endpoint

In your stripe app go to views.py where we defined previously the session checkout creator and add these lines:

...

from django.views.decorators.http import require_POST

from django.http import JsonResponse, HttpResponse

... @require_POST

@csrf_exempt

def stripe_webhook_paid_endpoint(request):



payload = request.body

sig_header = request.META['HTTP_STRIPE_SIGNATURE']

event = None



# Try to validate and create a local instance of the event

try:

event = stripe.Webhook.construct_event(payload, sig_header, settings.STRIPE_SIGNING_SECRET)

except ValueError as e:

# Invalid payload

return SuspiciousOperation(e)

except stripe.error.SignatureVerificationError as e:

# Invalid signature

return SuspiciousOperation(e)



# Handle the checkout.session.completed event

if event['type'] == 'checkout.session.completed':

checkout_session = event['data']['object']

# Make sure is already paid and not delayed

if checkout_session.payment_status == "paid":

_handle_successful_payment(checkout_session)



# Passed signature verification

return HttpResponse(status=200)



def _handle_successful_payment(checkout_session):

# Define what to do after the user has successfully paid

pass

First we import some extra Django libraries. As we have already initialized the Stripe library we don’t need to initialize again. Then again we are using some default Django decorators. @require_POST will accept only POST requests as this is the method Stripe will only use for calling our webhook. @csrf_exempt will again ignore the secure token from Django as we are handling another layers of security and the call will be outside our project. The next lines are very straightforward, we try to verify the request with our signature secret key (we don’t have it yet) and build a local event object from the JSON in the request. If everything goes right we check for the event we want to handle in the webhook, in this case checkout.session.completed (you can check all events in the official documentation) and we make sure the payment status is paid . We do this because even when a session have been completed successfully the payment have been delayed and we should receive an additional event with invoice payment. This step may be unnecessary at this point but as you grow your business logic this is the right codebase to handle it. Finally we call a function to handle the actual post-payment actions and we return a HTTP 200 OK code to Stripe. Stripe will handle any other response code as an error and will continue trying until the all the message events will be delivered. A good practice will be as well to answer the request fast and don’t wait to finish any batch operation or long task. So for example the best recommended action post-payment would be to update the paid status in our database, or save some kind of order object. But don’t generate a document, make another query or send an email. This kind of tasks would be better to handle with background tasks after we have answered Stripe. Missing Stripe events because your endpoint is busy will be ruinous for any digital business.

Next step would be to setup the path for our new endpoint. Go to myshop/urls.py in our main app and add these lines:

...

from stripe_shop.views import create_stripe_checkout_session, stripe_webhook_paid_endpoint



urlpatterns = [

...

path('stripe-webhook-paid/', stripe_webhook_paid_endpoint),

]

If we browse to our endpoint http://127.0.0.1:8000/stripe-webhook-paid/ it will throw a HTTP 405 ERROR which is expected. Now let’s send some real events with a secret signature in the next section.

Forwarding Stripe events locally

In this section we are going to use the Stripe CLI to forward events to your endpoint in the local environment. First make sure you install the CLI app in your environment, you can follow the install instructions for your OS in the official repo.

In order to give access to the CLI app you need to login into your account, type: stripe login It will open your browser and ask you to login and give access to the CLI app to your account.

Next setup the forwarding to your local endpoint:

$ stripe listen --forward-to 127.0.0.1:8000/stripe-webhook-paid/

Ready! Your webhook signing secret is whsec_xxxxxxxxxxxxxxxxxxxxxx (^C to quit)

Copy the signing secret and update STRIPE_SIGNING_SECRET in your main app settings.py When you setup a live webhook in the live dashboard from your account the signing secret will be shown only one time, make sure you take good care of it. Again never upload, hardcode or show your production secrets in production or any other environment.

Now you are ready to trigger your endpoint producing a local test event for checkout.session.completed event objects. Without closing your forwarding session open a new window or tab in your terminal and type:

stripe trigger checkout.session.completed

This will create several events for your endpoint and if you check the forwarding session terminal it should return successfully accepted [200] codes for all the events.

Local Full Payment Workflow Test

Now you are ready to fully test your payment workflow in your local environment. Starting with a logged user go to your front store and select a product. Fill email and name with any text, fill a future expiration date, any number for CVC and use 4242 4242 4242 4242 for a test card number that is always accepted. After payment you should be redirected to the url success_payment.html we’ve setup in during the checkout session creation (And we haven’t it yet).