Use Stripe Webhook to implement your business logic in an Amplify project (serverless with AWS Lambda)

Yann
Serveless with React and AWS Amplify
6 min readNov 27, 2019

--

On my last tutorial, I showed you how to implement a pay button in your React/Amplify website so you can start receiving payments. That’s good, but in the vast majority of use cases this will not be enough. You also want to update your own backend (here it will be DynamoDB) with the payment information, for instance to unlock the service your visitor just purchased. So if you’re ready, let’s see what you’ll need!

Prerequisites

  • You already have a React project setup with Amplify initialized with a graphQL api
  • You already have a Stripe account
  • You can proceed to a payment (the method here show a way but it doesn’t really matter if you used another method)

GraphQL setup

For this tutorial purpose, we will assume that the business logic you want to implement is simply to add an entry in your DB when you receive a payment.

Of course, you don’t want users to be able to write directly in it but you want them to be able to read this table. So you can add something like that in your schema.graphql:

type Payment
@model(mutations: null) {
id: ID!
date: AWSDateTime!
stripePaymentIntent: String!
amount: Float!
user: String!
}

The mutation: null insure that only our backend will be able to update this table so we can trust data we get from there.

Now push this table:

amplify push

Create the endpoint

On the previous tutorial, we already added an API named stripeapi with a checkout endpoint. So we will add another endpoint in the same API, but it is exactly the same process if you are creating one from scratch:

amplify update api

and answer as bellow:

? Please select from one of the below mentioned services REST
? Please select the REST API you would want to update stripeapi
? What would you like to do Add another path
? Provide a path (e.g., /items) /webhook
? Choose a Lambda source Create a new Lambda function
? Provide a friendly name for your resource to be used as a label for this category in the project: stripeWebhook
? Provide the AWS Lambda function name: stripeWebhook
? Choose the function template that you want to use: Serverless express function (Integration with Amazon API Gateway)
? Do you want to access other resources created in this project from your Lambda function? No
? Do you want to edit the local lambda function now? No
Succesfully added the Lambda function locally
? Restrict API access No
? Do you want to add another path? No

The new lambda function should now be accessible on you amplify backend project.

Update permissions

The first thing we need to do, is allowing this lambda to access/update DynamoDB. We can do that by updating the cloudformation template in the function folder: stripeWebhook-cloudformation-template.json

Open this json and find lambdaexecutionpolicy. Under this key, you will find Properties/PolicyDocument/Statement. The Statement array is the one listing your permission, thus it is the one you want to update. Add two entry in this array as followed (bold):

"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": {
"Fn::Sub": [
"arn:aws:logs:${region}:${account}:log-group:/aws/lambda/${lambda}:log-stream:*",
{
"region": {
"Ref": "AWS::Region"
},
"account": {
"Ref": "AWS::AccountId"
},
"lambda": {
"Ref": "LambdaFunction"
}
}
]
}
},
{
"Effect": "Allow",
"Action": [
"dynamodb:PutItem",
"dynamodb:DeleteItem",
"dynamodb:GetItem",
"dynamodb:Query",
"dynamodb:UpdateItem"
],
"Resource": "arn:aws:dynamodb:*:*:table/*"
},
{
"Effect": "Allow",
"Action": "dynamodb:Query",
"Resource": "arn:aws:dynamodb:*:*:table/*/index/*"
}

]

Import dependencies

Add the following in app.js.

const AWS = require('aws-sdk');
const ddb = new AWS.DynamoDB();
const uuid = require('uuid/v4');

You’ll also need to install uuid and stripe inside the function folder:

npm install uuid
npm install stripe

Toogle variables based on current environment

I didn’t talk about it before but I am using two environment in this project, one dev and one prod. I am not going to explain in detail how it works as there are plenty of tutorials out there that do it pretty well. If you only use one environment, then you can only add the set of variable you are using. For two environment, this is how you can do:

// Table parameters
const paymentTable = "Payment-*********-dev";
// Stripe parameters
const stripe = require('stripe')('sk_test_*******');
const endpointSecret = 'whsec_********';
if (process.env.ENV === 'prod') {
// Set prod env
console.log("Prod env")
paymentTable = "Payment-*********-prod";
// Stripe parameters
stripe = require('stripe')('sk_live_********');
endpointSecret = 'whsec_*******';
}

You test and live Stripe secrete keys and can be found in your Stripe account. For the endpointSecret, you will obtain it later when you create the Stripe webhook, leave it empty for now. For the payment table, it would be very nice if the name could be injected directly by Amplify as environment variable, let me know in the comments if you have a cleaner way to do it.

Important: Get the raw body

This is the part I spent the most time to have my code working. The issue is that lambda automatically Stringify the body which doesn’t create any issue most of the time. However, to prevent fraud, we will check that Stripe is sending us the request and not someone else. To do so, we will use stripe.webhooks.constructEvent(body, signature, endpointSecret). This function will fail if the body have been even slightly modified (which is exactly what we are expecting from it!).

To prevent that we need to access the rawBody. Add this code before your post function:

app.use(bodyParser.json({
verify: function (req, res, buf) {
req.rawBody = buf.toString()
}
}));

Add your code

Now we are all set, we can start implementing the business logic!

We are going to implement the post request. Inside the app.post(‘/webhook’, function (request, response) add the following lines:

const sig = request.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(request.rawBody, sig, endpointSecret);
console.log(event);
}
catch (err) {
return response.status(400).send(`Webhook Error: ${err.message}`);
}

This ask Stripe to authenticate the request and create an event object we can easily manipulate.

Then we can execute different code, based on the event we received. Here we are interested in the checkout.session.completed, when we receive it we add an entry to our db:

switch (event.type) {
case 'checkout.session.completed':
console.log('Payment checkout session was successful!');
// Create the payment entry in the db
const newPaymentId = uuid();
const date = new Date().toISOString();

let paramsPayment = {
TableName: paymentTable,
Item: {
"id": { "S": newPaymentId },
"date": { "S": date },
"stripePaymentIntent": { "S": event.data.object.payment_intent },
"amount": { "N": event.data.object.display_items[0].amount.toString() },
"user": { "S": event.data.object.client_reference_id },
}
};
console.log(paramsPayment)
ddb.putItem(paramsPayment, function (err, data) {
if (err) {
console.log("Error", err);
response.status(400).send(`Error creating payment: ${err.message}`);
} else {
console.log("Success to create a new payment entry");
return response.status(200).send('Success creating new folder');
}
})
break;
default:
// Unexpected event type
return response.status(400).end();
}

We are done here! Your function can handle a Stripe webhook event! Push it:

amplify push

Create the webhook on Stripe

Now that your backend is ready, you need to ask Stripe to send an event when a checkout session is complete (or any other event you like).

First you’ll need to find the complete address of the lambda you just created. To do so, connect to your aws console and go to apiGateway select stripeapi (if you gave the same name as me), Stages, dev (or the name of your environment). You should see the Invoke URL in blue on the top of the screen. Add the name of your endpoint to this url, for me /webhook. Your url should looks like that:

https://******.execute-api.eu-central-1.amazonaws.com/dev/webhook

Now that you know the url to hit, go on your Stripe dashboard. Click Developers, Webhooks and Add endpoint. Add your url and choose the checkout.session.completed event. Validate.

Now you can obtain your endpoint secret, go back on your lambda function (app.js) and update it. Push the change with amplify push.

Congrats!

That’s done now! When someone pays on your website through a checkout session, your lambda will be triggered and a new entry will be added on your DynamoDB. You can get this information as usual with graphQl and update your frontend accordingly!

Thanks for reading, you can find a the source code of the app.js in my Github.

--

--

Yann
Serveless with React and AWS Amplify

Aeronautical and Software engineer, used to work for drone R&D. Now I develop custom cloud based solutions and I love to share what I Iearn along the way!