Stripe, React and Serverless - Part 1

Simon LHFollow
Founder & CEO, Sigmetic
June 18, 2020

In this article series, we will talk about how we integrated Sigmetic with Stripe using React and Serverless.

Here in part 1, we will implement the back-end using Serverless.
In part 2 we will integrate the front-end in React using Stripe Elements.

Bear in mind that this can be done in various ways and is dependent on the particular purpose.

For Sigmetic we use a setup that includes:

  • Recurrent payments (i.e. subscription model).
  • AWS Lambda to proxy requests to Stripe using Serverless
  • Collecting payment information in React using Stripe Elements
  • We are using TypeScript on both front-end and back-end.

If you are looking for a similar setup to the above, you probably want to read along 😎

Prerequisites

Create an account on Stripe

The first thing you need to do is creating an account on Stripe.
You will need to go through the verification process in order to get your API keys.
This may take a day or two.

Setup cloud functions with Serverless

You will need to create a setup for your cloud functions using Serverless.
Sigmetic is hosted on AWS and is using Lambdas. But you are free to use whichever cloud provider you prefer that is compatible with Serverless.

See here how to get started.

Overview

There are a set of cases that we need to handle, and for each of these cases, we need a cloud function.

  • Create a customer
  • Create a subscription
  • Handle a subscription (e.g. cancel or continue)
  • Retrieve a subscription
  • Retrieve a payment method
  • Update a payment method
  • Retry payment for an invoice
  • Stripe webhooks

Pheww, sounds like a lot, right? 😩
It’s actually not that bad, once you get into it.

We will explain every case and provide code for setting it up 💪
So stay with us a little longer - you will soon have Stripe up and running!

Cloud functions

Let’s get started creating our serverless back-end.
We will go through all the cloud functions first, and then we will integrate the whole thing with the front-end.

Install stripe from NPM

Assuming that you’ve already set up a Serverless project; start by installing stripe from NPM.

npm install stripe

How you structure your folders isn’t highly important, but we created a folder in src/lambdas/stripe/ for all our Stripe functions.
Because we’re using TypeScript, we can then reference the transpiled js-files in dist/lambdas/stripe/.

Create API keys

You also need to go to your Stripe dashboard -> Developers -> API keys and create a new set of API keys.

ℹ️ While developing, flick the “view test data” toggle in your Stripe dashboard

For the cloud functions, we will be using the secret key.

Create a customer

Customer objects allow you to perform recurring charges, and to track multiple charges, that are associated with the same customer. The API allows you to create, delete, and update your customers. You can retrieve individual customers as well as a list of all your customers.Stripe docs

Create a new file src/lambda/stripe/createCustomer.ts

In the file serverless.yml add the function:

# serverless.yml
stripe-create-customer:
handler: dist/lambdas/stripe/createCustomer.handler
events:
- http:
path: stripe/create-customer
method: post
environment:
STRIPE_SECRET_KEY: PUT-YOUR-SECRET-KEY-HERE

In the file src/lambda/stripe/createCustomer.ts, let’s start by instantiating Stripe:

import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || '', {
apiVersion: '2020-03-02',
});

Now, this function is going to be invoked on a POST request, so let’s define an interface for our body.
We expect the body to carry en email and a username:

interface IBody {
email: string;
username: string;
}

Then, let’s define our handler:

export const handler = async (
event: APIGatewayProxyEvent,
context: any,
callback: (err: Error | null, data: any) => void,
) => {
if (!event.body) {
callback(Error('Invalid body'));
return;
}
const body = JSON.parse(event.body) as IBody;
const { email, username } = body;
if (!email || !username) {
callback(null, {
statusCode: 400,
body: 'Missing email or username',
});
return;
}
try {
// Create a new customer
const customer = await stripe.customers.create({
email,
name: username,
});
callback(null, {
statusCode: 200,
body: JSON.stringify(customer),
});
} catch (error) {
callback(error);
}
};

That’s it 💪
Deploy your new cloud function and try performing a POST request to the endpoint specified and verify that it creates a new Stripe Customer.

You will be able to see the new customer in your Stripe dashboard -> Customers

curl --header "Content-Type: application/json" \
--request POST \
--data '{"email": "test@test.com", "username":"Test McTest"}' \
https://your-endpoint/stripe/create-customer

Create a subscription

Next, you’ll define a function for creating a new subscription. For this, you are going to need a Stripe Product.
Go to your Stripe dashboard -> Products -> Add product
Once you have created a product, go and get the Price ID of that product. We will be using that for this function.

Create a new file src/lambda/stripe/createSubscription.ts

In the file serverless.yml add the function:

# serverless.yml
stripe-create-subscription:
handler: dist/lambdas/stripe/createSubscription.handler
events:
- http:
path: stripe/create-subscription
method: post
environment:
STRIPE_SECRET_KEY: PUT-YOUR-SECRET-KEY-HERE
STRIPE_PRICE_ID: PUT-YOUR-PRICE-ID-HERE

In the file src/lambda/stripe/createSubscription.ts, once again we instantiate Stripe:

import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || '', {
apiVersion: '2020-03-02',
});

Specify the interface of the body:

interface IBody {
paymentMethodID: string;
customerID: string;
}

Then, define the handler:

export const handler = async (
event: APIGatewayProxyEvent,
context: any,
callback: (err: Error | null, data: any) => void,
) => {
if (!event.body) {
callback(Error('Invalid body'));
return;
}
const body = JSON.parse(event.body) as IBody;
const { paymentMethodID, customerID } = body;
if (!paymentMethodID || !customerID) {
callback(null, {
statusCode: 400,
body: 'Missing payment method or customer',
});
return;
}
try {
// Attach the payment method
await stripe.paymentMethods.attach(paymentMethodID, {
customer: customerID,
});
// Set the payment method as the 'default' for this customer
await stripe.customers.update(customerID, {
invoice_settings: {
default_payment_method: paymentMethodID,
},
});
// Create the subscription
const subscription = await stripe.subscriptions.create({
customer: customerID,
items: [{ price: process.env.STRIPE_PRICE_ID }],
expand: ['latest_invoice.payment_intent'],
});
callback(null, {
statusCode: 200,
body: JSON.stringify(subscription),
});
} catch (error) {
callback(error);
}
};

This one, we cannot easily test with a POST request, but we’ll get back to this one later when we integrate the front-end.

Handle a subscription

Typically, you want to provide the customer with the ability to cancel their subscription.
The subscription will then end after the end of the current billing period.
Meanwhile, the customer has the ability to continue their subscription, and everything will be back to the way it was.

With Stripe, we can simply flag whether the subscription will end after the current billing period.

Create a new file src/lambda/stripe/handleSubscription.ts

In the file serverless.yml add the function:

# serverless.yml
stripe-handle-subscription:
handler: dist/lambdas/stripe/handleSubscription.handler
events:
- http:
path: stripe/handle-subscription
method: post
environment:
STRIPE_SECRET_KEY: PUT-YOUR-SECRET-KEY-HERE

In the file src/lambda/stripe/handleSubscription.ts, once again we instantiate Stripe:

import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || '', {
apiVersion: '2020-03-02',
});

Specify the interface of the body:

interface IBody {
subscriptionID: string;
end: boolean;
}

Then, define the handler:

export const handler = async (
event: APIGatewayProxyEvent,
context: any,
callback: (err: Error | null, data: any) => void,
) => {
if (!event.body) {
callback(Error('Invalid body'));
return;
}
const body = JSON.parse(event.body) as IBody;
const { subscriptionID, end } = body;
// Note that 'end' can be 'false'.
// We need to strictly check if it's undefined
if (!subscriptionID || end === undefined) {
callback(null, {
statusCode: 400,
body: 'Missing subscription id',
});
return;
}
try {
// Update the subscription
const subscription = await stripe.subscriptions.update(subscriptionID, {
cancel_at_period_end: end,
});
callback(null, {
statusCode: 200,
body: JSON.stringify(subscription),
});
} catch (error) {
callback(error);
}
};

This one we can test.
Go your Stripe dashboard -> Customers.
Click on the customer, and scroll down to the ‘Subscriptions’ section.
Click on the subscription and get the ID.

Now deploy your new cloud functions, and make a POST request to your defined endpoint:

curl --header "Content-Type: application/json" \
--request POST \
--data '{"subscriptionID": "THE-ID-YOU-JUST-FOUND", "end": true}' \
https://your-endpoint/stripe/handle-subscription

Go back to your customer, and scroll down to the ‘Subscriptions’ section.
You should see something like this:

Notice the ‘Cancels Jul 9’ badge

subscription scr

Retrieve a subscription

Lastly, we want to be able to retrieve the full subscription object associated with a given subscription.

Create a new file src/lambda/stripe/retrieveSubscription.ts

In the file serverless.yml add the function:

# serverless.yml
stripe-retrieve-subscription:
handler: dist/lambdas/stripe/retrieveSubscription.handler
events:
- http:
path: stripe/retrieve-subscription
method: post
environment:
STRIPE_SECRET_KEY: PUT-YOUR-SECRET-KEY-HERE

In the file src/lambda/stripe/retrieveSubscription.ts, once again we instantiate Stripe:

import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || '', {
apiVersion: '2020-03-02',
});

Specify the interface of the body:

interface IBody {
subscriptionID: string;
}

Then, define the handler:

export const handler = async (
event: APIGatewayProxyEvent,
context: any,
callback: (err: Error | null, data: any) => void,
) => {
if (!event.body) {
callback(Error('Invalid body'));
return;
}
const body = JSON.parse(event.body) as IBody;
const { subscriptionID } = body;
if (!subscriptionID) {
callback(null, {
statusCode: 400,
body: 'Missing subscription id',
});
return;
}
try {
// Get the subscription
const subscription = await stripe.subscriptions.retrieve(subscriptionID);
callback(null, {
statusCode: 200,
body: JSON.stringify(subscription),
});
} catch (error) {
callback(error);
}
};

This one we can also easily test by using the subscription ID as we did just before.
After deploying the cloud function, send a POST request to your defined endpoint:

curl --header "Content-Type: application/json" \
--request POST \
--data '{"subscriptionID": "THE-ID-YOU-JUST-FOUND"}' \
https://your-endpoint/stripe/retrieve-subscription

You should get a detailed subscription object back from Stripe.

Retrieve a payment method

In the same manner, as with a subscription, we also want the ability to retrieve a payment method object from Stripe.
In particular, this is useful for showing the customer which credit card is currently being used. E.g. showing the type of card and the last 4 digits.

Create a new file src/lambda/stripe/retrievePaymentMethod.ts

In the file serverless.yml add the function:

# serverless.yml
stripe-retrieve-payment-method:
handler: dist/lambdas/stripe/retrievePaymentMethod.handler
events:
- http:
path: stripe/retrieve-payment-method
method: post
environment:
STRIPE_SECRET_KEY: PUT-YOUR-SECRET-KEY-HERE

In the file src/lambda/stripe/retrievePaymentMethod.ts, instantiate Stripe:

import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || '', {
apiVersion: '2020-03-02',
});

Specify the interface of the body:

interface IBody {
paymentMethodID: string;
}

Then, define the handler:

export const handler = async (
event: APIGatewayProxyEvent,
context: any,
callback: (err: Error | null, data: any) => void,
) => {
if (!event.body) {
callback(Error('Invalid body'));
return;
}
const body = JSON.parse(event.body) as IBody;
const { paymentMethodID } = body;
if (!paymentMethodID) {
callback(null, {
statusCode: 400,
body: 'Invalid payment method',
});
return;
}
try {
// Get the payment method
const paymentMethod = await stripe.paymentMethods.retrieve(paymentMethodID);
callback(null, {
statusCode: 200,
body: JSON.stringify(paymentMethod),
});
} catch (error) {
callback(error);
}
};

In the same way, is with the subscription ID, go and find your customer in the Stripe Dashboard and grab the payment method ID from the payment method.

Deploy the cloud function and make a POST request to the endpoint defined:

curl --header "Content-Type: application/json" \
--request POST \
--data '{"paymentMethodID": "THE-ID-YOU-JUST-FOUND"}' \
https://your-endpoint/stripe/retrieve-payment-method

You should get a detailed payment method object back from Stripe.

Update a payment method

The customer of your product should be able to update their payment method.

Create a new file src/lambda/stripe/updatePaymentMethod.ts

In the file serverless.yml add the function:

# serverless.yml
stripe-retry-invoice:
handler: dist/lambdas/stripe/updatePaymentMethod.handler
events:
- http:
path: stripe/retry-invoice
method: post
environment:
STRIPE_SECRET_KEY: PUT-YOUR-SECRET-KEY-HERE

In the file src/lambda/stripe/updatePaymentMethod.ts, instantiate Stripe:

import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || '', {
apiVersion: '2020-03-02',
});

Specify the interface of the body:

interface IBody {
paymentMethodID: string;
customerID: string;
invoiceID: string;
}

Then, define the handler:

export const handler = async (
event: APIGatewayProxyEvent,
context: any,
callback: (err: Error | null, data: any) => void,
) => {
if (!event.body) {
callback(Error('Invalid body'));
return;
}
const body = JSON.parse(event.body) as IBody;
const { paymentMethodID, customerID, invoiceID } = body;
if (!paymentMethodID || !customerID || invoiceID) {
callback(null, {
statusCode: 400,
body: 'Invalid payment method or customer ID',
});
return;
}
try {
// Attach the new payment method
await stripe.paymentMethods.attach(paymentMethodID, {
customer: customerID,
});
// Update default payment method on customer
await stripe.customers.update(customerID, {
invoice_settings: {
default_payment_method: paymentMethodID,
},
});
// Retrieve the invoice
const invoice = await stripe.invoices.retrieve(invoiceID, {
expand: ['payment_intent'],
});
callback(null, {
statusCode: 200,
body: JSON.stringify(invoice),
});
} catch (error) {
callback(error);
}
};

This one is a bit more difficult to test with a simple POST request, so we will come back to this later.

Retry payment for an invoice

This case is a little more special.
Sometimes it happens that a payment method is initially accepted (i.e. the credit card is valid), but when the payment method is attached to the customer, something goes wrong.
This could be because the bank denies the process, or due to insufficient funds.

The result will be a failed invoice.
Specifially, there is a field on the subscription object: latest_invoice.payment_intent.status that will have the value requires_payment_method.

When this happens, both a customer and a subscription is successfully created in Stripe, so we simply want to have the customer retry the payment with another card.

Create a new file src/lambda/stripe/retryInvoice.ts

In the file serverless.yml add the function:

# serverless.yml
stripe-update-payment-method:
handler: dist/lambdas/stripe/retryInvoice.handler
events:
- http:
path: stripe/update-payment-method
method: post
environment:
STRIPE_SECRET_KEY: PUT-YOUR-SECRET-KEY-HERE

In the file src/lambda/stripe/retryInvoice.ts, instantiate Stripe:

import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || '', {
apiVersion: '2020-03-02',
});

Specify the interface of the body:

interface IBody {
paymentMethodID: string;
customerID: string;
}

Then, define the handler:

export const handler = async (
event: APIGatewayProxyEvent,
context: any,
callback: (err: Error | null, data: any) => void,
) => {
if (!event.body) {
callback(Error('Invalid body'));
return;
}
const body = JSON.parse(event.body) as IBody;
const { paymentMethodID, customerID } = body;
if (!paymentMethodID || !customerID) {
callback(null, {
statusCode: 400,
body: 'Invalid payment method or customer ID',
});
return;
}
try {
// Attach the new payment method
await stripe.paymentMethods.attach(paymentMethodID, {
customer: customerID,
});
// Set the new payment method as default
await stripe.customers.update(customerID, {
invoice_settings: {
default_payment_method: paymentMethodID,
},
});
callback(null, {
statusCode: 200,
});
} catch (error) {
callback(error);
}
};

Stripe webhooks

The last thing we need to integrate is Stripe’s webhooks.
Stripe’s payment lifecycle is really comprehensive, and Stripe allows you to hook into various of these events.

For Sigmetic, we use only three:

  • invoice.payment_succeeded
  • invoice.payment_failed
  • customer.subscription.deleted

Let’s go and set up these webhooks for Stripe.
Go your Stripe dashboard -> Developers -> Webhooks -> Add endpoint.
Add your endpoint: https://your-endpoint/stripe/webhooks.
Add the three events from the list above.

When created, you can create a Signing secret. We will be using that right below.

Create a new file src/lambda/stripe/webhooks.ts

In the file serverless.yml add the function:

# serverless.yml
stripe-webhooks:
handler: dist/lambdas/stripe/webhooks.handler
events:
- http:
path: stripe/webhooks
method: post
environment:
STRIPE_SECRET_KEY: PUT-YOUR-SECRET-KEY-HERE
STRIPE_WEBHOOK_SECRET: PUT-YOUR-SIGNING-SECRET-HERE

In the file src/lambda/stripe/webhooks.ts, instantiate Stripe:

import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || '', {
apiVersion: '2020-03-02',
});

Now let’s create three functions, one for each webhook event.
We start with invoice.payment_succeeded.

When a payment has been successfully processed, this event will be fired.
At Sigmetic, we want to send a confirmation to the customer with a link the invoice.

We also want to enable their paid functionality.

const paymentSucceeded = async (dataObject: any) => {
const customerID = dataObject['customer'] as string;
if (!customerID) {
throw Error(`No customer with ID "${customerID}"`);
}
const customerEmail = dataObject['customer_email'] as string;
const customerName = dataObject['customer_name'] as string;
const linkToInvoice = dataObject['hosted_invoice_url'] as string;
// Use the above to send confirmation email with a link to invoice,
// thus enabling the paid functionality.
};

The invoice.payment_failed is fired when a payment failed to process.
In this case, we want to send an email to the customer to notify them.

const paymentFailed = async (dataObject: any) => {
// ...This is more or less identical with the paymentSucceeded function above.
// Send an email notifying about the failed payment.
};

Finally, the customer.subscription.deleted event is fired when a subscription ends.

const subscriptionDeleted = async (dataObject: any) => {
// ...This is more or less identical with the paymentSucceeded function above.
// Send an email confirming that there subscription has now ended.
};

Then, we define a small handlerMapping object, so we can quickly look up which of the above functions to use:

const handlerMapping: { [key: string]: Function } = {
'invoice.payment_succeeded': paymentSucceeded,
'invoice.payment_failed': paymentFailed,
'customer.subscription.deleted': subscriptionDeleted,
};

Finally, we create the handler:

export const handler = async (
event: APIGatewayProxyEvent,
context: any,
callback: (err: Error | null, data: any) => void,
) => {
if (!event.body) {
callback(Error('Invalid body'));
return;
}
try {
// We validate that the event is coming from an
// authentic Stripe origin
const webhookEvent = stripe.webhooks.constructEvent(
event.body,
event.headers['Stripe-Signature'],
process.env.STRIPE_WEBHOOK_SECRET as string,
);
// Pull out the data object (customer, invoice, etc...)
const dataObject = webhookEvent.data.object as any;
// Get the corrosponding handler from the handlerMapping above
const handler = handlerMapping[webhookEvent.type];
if (!handler) {
callback(null, {
statusCode: 400,
body: 'Unexpected event type',
});
return;
}
await handler(dataObject);
callback(null, {
statusCode: 200,
});
} catch (error) {
callback(error);
}
};

In the Stripe dashboard under Webhooks, you can send test-events to verify that it works correctly.
Try it out 😎

Rounding up

This is basically all we need for our back-end.

Make sure to deploy all these cloud functions to the cloud, and test each endpoint to verify that they are all working correctly.

You may also have noticed a bit of redundancy: We put the full examples of the functions in this article, but you may very well benefit from writing some wrapper functionality to keep it DRY.

Next up is integrating it with the front-end.

Read more about that in part 2.

Simon LH, Founder & CEO

Sigmetic © 2020
Follow Sigmetic on