Chapter 9: Stripe API - API project. Stripe API - APP project. Setup at Stripe dashboard and environmental variables. Email notification for new post API - API project. Amazon API Gateway and AWS Lambda.
We keep our book up to date with recent libraries and packages.
The section below is a preview of SaaS Boilerplate Book. To read the full text, you will need to purchase the book.
In Chapter 9, you will start with the codebase in the 8-begin folder of our saas repo and end up with the codebase in the 9-end folder.
We will cover the following topics in this chapter:
Stripe API - API project - Stripe setup - API - Stripe methods - API - Stripe checkout callback - API - Stripe webhook - API - Team Model - API - User Model - API - Team Leader Express routes - API
Stripe API - APP project - Team Leader API methods - APP - Team data store - APP - User data store - APP - Billing page and testing - APP - TeamSettings page and testing - APP
Setup at Stripe dashboard and environmental variables - Testing Stripe API
Email notification for new post API - API project - Discussion and EmailTemplate Models - API - Express routes for adding and editing Discussion - API - sendDataToLambdaApiMethod API method - APP - Discussion data store - APP - CreateDiscussionForm, EditDiscussionForm and PostForm - APP - Testing updates to internal API infrastructure
Amazon API Gateway and AWS Lambda - Amazon API Gateway - AWS Lambda - Testing entire Email notification for new post API
Stripe API - API project link
In this section, we will discuss subscriptions and how to implement them using Stripe API. We will outline the entire Stripe API infrastructure for our application. Any SaaS product has to have a way to charge customers. Most non-enterprise SaaS businesses charge customers on a monthly basis, somewhere in the $25-$500 range. In this book, we will show you how to implement the following features in our SaaS boilerplate, so that a Team Leader end user can:
- buy a subscription (subscribe to a monthly plan)
- unsubscribe (stop a subscription)
- update card information
- see a list of all invoices for subscription payments
In your real world SaaS product, you are the decision maker for what features to provide to your customers in terms of billing. In this section, we will build a simple Billing
page that allows an end user to do the above list of actions:
Besides figuring out what billing features to add to your SaaS product, you should decide when to ask customers to buy a monthly plan. Your particular SaaS product may offer a trial period or require all customers to become paying customers right away. For a trial period, you should decide whether you want to collect card information and whether customers are automatically charged at the end of the trial period. By knowing your customers well, you can decide what works best for them and for your business.
In this book, we are showing you a less aggressive approach of offering customers to pay for your SaaS product. We let any end user use our boilerplate, but we set a limit to the number of team members that a Team Leader can invite to their team. By this point in the book, you have two accounts, one for a Team Leader and one for a Team Member. We will set up a limit of two team members including the Team Leader. In other words, the Team Leader has to become a paying customer in order to add a third team member to team:
Team Leader Potato has to go to the Billing
page and buy a subscription in order to add a third member to the Team Builder Book team.
We will be using a newer Stripe API that prescribes using Stripe Checkout, which redirects a potential customer to a Stripe-hosted checkout page to add payment information and confirm payment. This newer API also prescribes creating a Stripe session object that contains information about the customer, subscription, and redirect URLs for different outcomes, among other data. There are three possible outcomes with different redirect URLs. A Team Leader in our application will click the Buy Subscription
button and be redirected to Stripe's Checkout Website. Then, this end user can either cancel or proceed with payment. If the end user cancels, he/she gets redirected to the Billing
page and sees a cancel message. If the end user proceeds and an error is caught, he/she gets redirected back to the Billing
page and sees an error message. Finally, if the end user proceeds and payment is successful, he/she gets redirected to the Billing
page and sees an updated UI that shows (1) that this end user is indeed a paying customer, (2) that our application has saved this end user's card information, and (2) a list of invoices with a first invoice.
We can summarize Stripe API infrastructure in our application like this:
Stripe setup - API link
As we mentioned a bit earlier, we will be using the most recent Stripe API approach as of August 2020. The older approach was using a Stripe popup within your application. The newer approach prescribes us to use a checkout page hosted by Stripe, Stripe Checkout:
https://stripe.com/docs/payments/checkout
Checkout is a checkout page that works across devices, including mobile browsers. Our application has to redirect a potential customer to Checkout and then back to our application.
For every attempt to manually pay in our application, Stripe prescribes creating and retrieving a Session object:
https://stripe.com/docs/api/checkout/sessions
Stripe Session should be created for single payments and first subscription payments. In our case, we use it for first subscription payments. We need to create a Session object on our API
server, then pass the Session's id, sessionId
, to our APP
project on the browser and call redirectToCheckout({ sessionId })
to redirect an end user from our Billing
page to Stripe's Checkout page.
https://stripe.com/docs/js/checkout/redirect_to_checkout
When our API
server gets a request from Stripe's server - API
has to run some code to update the corresponding Team
and User
MongoDB documents. For example, we want to set the field isSubscriptionActive
to true
for the Team
document and save a stripeCard
object as a field to the User
document. How do we find the correct Team
and User
to update? We retrieve a Session object that contains all necessary information to find the correct Team
and User
documents.
Here is a visual explanation of when we need to create and retrieve a Session. We need to create a Session before redirecting to Checkout. We need to retrieve a Session after getting a request from Stripe's server:
Go to Stripe's API documentation and find the API method for creating a Session:
https://stripe.com/docs/api/checkout/sessions/create
Select Node.js
in the Select library
dropdown:
As you can see, the Stripe API method we are looking for is stripe.checkout.sessions.create
.
Go to Stripe's API documentation and find the API method to retrieve a Session:
https://stripe.com/docs/api/checkout/sessions/retrieve
The Stripe API method we are looking for is stripe.checkout.sessions.retrieve
.
Let's define createSession
on our API
server. We, as developers, can specify the shape of a Session object; however, some properties are required. For example, payment_method_types
, success_url
, and cancel_url
are required properties of any Session object. We want to save userId
and teamId
to be able to find User
and Team
documents later and update them.
createSession
Stripe method:
const dev = process.env.NODE_ENV !== 'production';
const stripeInstance = new Stripe(
dev ? process.env.STRIPE_TEST_SECRETKEY : process.env.STRIPE_LIVE_SECRETKEY,
{ apiVersion: '2023-10-16' },
);
function createSession({ userId, teamId, teamSlug, customerId, subscriptionId, userEmail, mode }) {
const params: Stripe.Checkout.SessionCreateParams = {
customer_email: customerId ? undefined : userEmail,
customer: customerId,
payment_method_types: ['card'],
mode,
success_url: `${process.env.URL_API}/stripe/checkout-completed/{CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.URL_APP}/team/${teamSlug}/billing? redirectMessage=Checkout%20canceled`,
metadata: { userId, teamId },
};
if (mode === 'subscription') {
params.line_items = [
{
price: dev ? process.env.STRIPE_TEST_PRICEID : process.env.STRIPE_LIVE_PRICEID,
quantity: 1,
},
];
} else if (mode === 'setup') {
if (!customerId || !subscriptionId) {
throw new Error('customerId and subscriptionId required');
}
params.setup_intent_data = {
metadata: { customer_id: customerId, subscription_id: subscriptionId },
};
}
return stripeInstance.checkout.sessions.create(params);
}
Once we defined params
, we called the Stripe API method stripeInstance.checkout.sessions.create(params)
. We should discuss a few parts of this method in more detail:
- The type of params
is Stripe.Checkout.SessionCreateParams
. You should have the stripe
package already installed, so Stripe.Checkout.SessionCreateParams
is available.
- success_url
has an API endpoint, meaning we have to define a matching Express route, /stripe/checkout-completed/:sessionId
. Then we access sessionId
as req.params.sessionId
and retrieve a Session object, update Team
and User
documents, and send res.redirect
to the browser to redirect an end user from Checkout back to our application - either the Billing
page with success UI or the Billing
page with an error message via the redirectMessage
prop and notify
.
- cancel_url
is straightforward to understand. req.query.redirectMessage
is accessed by our code inside App.getInitialProps
and populated as the redirectMessage
prop in all pages. We need to call notify
on the Billing
page to show a cancel message to an end user.
- mode
can have two values in our application. For making the first subscription payment and becoming a subscriber, the value for mode
is subscription
. For updating card information, the value of mode
is setup
: https://stripe.com/docs/payments/checkout/subscriptions/update-payment-details#create-checkout-session.
- When the mode
value is setup
, we have to check for customerId
and subscriptionId
to make sure a Stripe customer exists and a Stripe Subscription exists. Otherwise, it makes no sense to let a user edit their payment method.
The Stripe method retrieveSession
takes one argument and expands properties of the Session object it retrieves with the Stripe API method stripeInstance.checkout.sessions.retrieve
:
function retrieveSession({ sessionId }: { sessionId: string }) {
return stripeInstance.checkout.sessions.retrieve(sessionId, {
expand: [
'setup_intent',
'setup_intent.payment_method',
'customer',
'subscription',
'subscription.default_payment_method',
],
});
}
Expanding a response in Stripe API means getting additional data - in this case, an object's properties:
https://stripe.com/docs/api/expanding_objects
Place the above code into a new file, book/9-begin/api/server/stripe.ts
. At the top of the file, add imports and a line that disables the camelcase rule (for example, the success_url
parameter is not camelcase):
/* eslint-disable @typescript-eslint/camelcase */
import * as bodyParser from 'body-parser';
import Stripe from 'stripe';
import Team from './models/Team';
import User from './models/User';
const dev = process.env.NODE_ENV !== 'production';
const stripeInstance = new Stripe(
dev ? process.env.STRIPE_TEST_SECRETKEY : process.env.STRIPE_LIVE_SECRETKEY,
{ apiVersion: '2023-10-16' },
);
function createSession({ userId, teamId, teamSlug, customerId, subscriptionId, userEmail, mode }) {
const params: Stripe.Checkout.SessionCreateParams = {
customer_email: customerId ? undefined : userEmail,
customer: customerId,
payment_method_types: ['card'],
mode,
success_url: `${process.env.URL_API}/stripe/checkout-completed/{CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.URL_APP}/team/${teamSlug}/billing?redirectMessage=Checkout%20canceled`,
metadata: { userId, teamId },
};
if (mode === 'subscription') {
params.line_items = [
{
price: dev ? process.env.STRIPE_TEST_PRICEID : process.env.STRIPE_LIVE_PRICEID,
quantity: 1,
},
];
} else if (mode === 'setup') {
if (!customerId || !subscriptionId) {
throw new Error('customerId and subscriptionId required');
}
params.setup_intent_data = {
metadata: { customer_id: customerId, subscription_id: subscriptionId },
};
}
return stripeInstance.checkout.sessions.create(params);
}
function retrieveSession({ sessionId }: { sessionId: string }) {
return stripeInstance.checkout.sessions.retrieve(sessionId, {
expand: [
'setup_intent',
'setup_intent.payment_method',
'customer',
'subscription',
'subscription.default_payment_method',
],
});
}
Stripe methods - API link
In this subsection, we will define four Stripe methods:
- The updateCustomer
and updateSubscription
Stripe methods will be used inside the Express route /stripe/checkout-completed/:sessionId
, which we have yet to define.
- The cancelSubscription
Stripe method will be used inside the static methods cancelSubscription
and cancelSubscriptionAfterFailedPayment
of the Team
model.
- The getListOfInvoices
Stripe method will be used inside the static method getListOfInvoicesForCustomer
of the User
model.
In the previous subsection, you familiarized yourself with Stripe API documentation. Inside the Express route /stripe/checkout-completed/:sessionId
, we want to use the updateCustomer
and updateSubscription
Stripe methods to update Stripe Customer and Stripe Subscription default_payment_method
parameter. After checking Stripe's API docs:
https://stripe.com/docs/api/customers/update
https://stripe.com/docs/api/subscriptions/update
You've reached the end of the Chapter 9 preview. To continue reading, you will need to purchase the book.
We keep our book up to date with recent libraries and packages.