How to Accept Payments with Stripe and Express
Learn how to accept payments with Stripe Checkout, Stripe's prebuilt form, using Express. We will also go over creating Stripe Products, Prices, and Checkout Sessions using the Stripe API.
Table of Contents 📖
- Project Demonstration
- Stripe and Payment Collection
- Stripe Checkout
- Project Setup and Express Install
- Connecting Node to Stripe
- Creating a Stripe Product and Price
- Creating a Stripe Checkout Hosted Page
- Handling Checkout Success with Webhooks
- Stripe CLI Setup
- Coding a Webhook Endpoint
- Generating a Custom Success Page and Handling Cancelled Payments
- Running the Program
Project Demonstration
Below is what we will be building. We will handle setting the product information such as its name, price, description, etc. We will also create a custom payment success page displaying the customer's name. This is all done with an Express server and the Stripe API.
Stripe and Payment Collection
Stripe offers multiple ways to collect payment including no-code options like link generation and code required options like using a prebuilt payment form. In this article we will focus on collecting payments using Stripe's prebuilt form. The prebuilt payment form requires minimal coding and is the best choice for most integrations because it has all the needed functionalities for accepting payments while also being customizable.
Stripe Checkout
Stripe's prebuilt payment form is called Stripe Checkout. This form can be embedded into a web page or it can be a separate page hosted by Stripe. Either way, it is used to collect one-time payments or subscriptions.
INFO: Stripe Checkout supports multiple devices and also several languages and currencies, dynamically showing the payment methods and currency most likely to be accepted by the user.
More information on Stripe Checkout can be found at the following URL:
https://docs.stripe.com/payments/checkout
Project Setup and Express Install
Before we start working with Stripe, lets setup our project as an npm project, install express, and install nodemon.
npm init es6 -y
npm i express
npm i nodemon -D
Now lets set the entry point to our application and add a simple start script inside package.json.
"main": "src/server.js",
...
"scripts": {
"start": "nodemon --env-file .env ."
},
INFO: Note that to use the --env-file flag we need to be using Node version 20.11.0 or higher.
Now lets define the location of our Express server using environment variables placed inside a .env file.
HOST=localhost
PORT=6789
URL=http://localhost:6789
Connecting Node to Stripe
Now lets work on connecting our application to the Stripe API. This is done with the Stripe secret key which can be found at the following URL:
https://dashboard.stripe.com/test/apikeys
INFO: A Stripe account is required to get a Stripe secret key.
The key should look similar to the following:
Secret key: sk_test_aweirtuyhADSFVB435646Bsdfgkjhfd
The Stripe secret key identifies our application with Stripe and allows us to use the API.
INFO: Keeping the secret key safe/secure is very important! If the secret key was exposed, whoever has it could refund charges, cancel subscriptions, etc.
Place the secret key in the .env file that houses our Express server location.
STRIPE_SECRET_KEY=sk_test_aweirtuyhADSFVB435646Bsdfgkjhfd
Now lets connect Node to Stripe, and to do this we need to use the npm package stripe. This package wraps the Stripe API, allowing us to use it to create customers, retrieve invoices, etc.
npm i stripe
Now lets start coding. First, lets capture our environment variables inside regular variables and instantiate Express and Stripe.
import Stripe from 'stripe';
import express from 'express';
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
const HOST = process.env.HOST;
const PORT = process.env.PORT;
const URL = process.env.URL;
const stripe = new Stripe(STRIPE_SECRET_KEY);
const app = express();
After providing the Stripe secret key to the Stripe constructor, we can use the instance to work with the Stripe API.
Creating a Stripe Product and Price
Before we continue, lets use the Stripe API to create a product to sell. Note that this can also be done through the Stripe dashboard. The dashboard equivalent can be found at the following URL:
https://dashboard.stripe.com/test/products
Creating a Product includes defining the name, a description, price, etc. Products represent either the physical goods or a service we are selling.
const product = await stripe.products.create({
name: 'WittCepter',
description: 'Chrome extension that gives you control over your browser network traffic.',
});
INFO: WittCepter is the name of my Chrome extension. Go check it out!
The only required property when creating a product is the name. After creating a product, we need to create a price for it. Prices represent the cost of a Product.
const price = await stripe.prices.create({
product: product.id,
unit_amount: 1000,
currency: 'usd',
});
console.log(price);
INFO: The unit_amount property is a positive integer in cents (or local equivalent) (or 0 for a free price) representing how much to charge. In USD, 1000 unit_amount is $10.
Stripe Products and Prices are separate so that we can update a Product's Price without changing the actual Product. Now run this file using Node to create the Product and Price.
node src/product.js
Just like a product, each price has an ID. When creating a Checkout Session we will use this Price ID. So make note of the Price ID logged to the console.
{
id: 'price_54jklhPASDF23432',
object: 'price',
active: true,
billing_scheme: 'per_unit',
created: 1717061924,
currency: 'usd',
custom_unit_amount: null,
livemode: false,
lookup_key: null,
metadata: {},
nickname: null,
product: 'prod_FAAVF34kjhc',
recurring: null,
tax_behavior: 'unspecified',
tiers_mode: null,
transform_quantity: null,
type: 'one_time',
unit_amount: 1000,
unit_amount_decimal: '1000'
}
Take the outputted price ID and add it as an environment variable inside the .env file.
STRIPE_PRICE_ID=price_ASDFASD254pg78asdv
Now add this variable to our server.js file.
const STRIPE_PRICE_ID = process.env.STRIPE_PRICE_ID;
Creating a Stripe Checkout Hosted Page
Now that we have a Product and Price set up, lets work on generating the Stripe Checkout page. To do this, lets first create am HTML form that takes the user to a route that generates a Checkout Session.
const BUY_HTML = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
input[type="submit"] {
height: 40px;
width: 200px;
border: none;
border-radius: 5px;
background-color: #0070f3;
color: #fff;
font-size: 16px;
font-weight: bold;
cursor: pointer;
}
</style>
</head>
<body>
<form action="${URL}/session" method="POST">
<input type="submit" value="Buy WittCepter $10" />
</form>
</body>
</html>`;
We will serve this HTML form up from our main route.
app.get('/', (req, res) => {
res.send(BUY_HTML);
});
Now lets code the route that handles this HTML form. Specifically, when a user click the submit button, we will generate a Checkout Session and then redirect them to it. We can do that using the Stripe API.
app.post('/session', async (req, res) => {
try {
const session = await stripe.checkout.sessions.create({
mode: 'payment',
line_items: [{price: STRIPE_PRICE_ID, quantity: 1}],
success_url: `${URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${URL}/cancel`
});
console.log('Session created!', session);
return res.redirect(303, session.url);
} catch (err) {
console.error('Error generating session', err);
}
});
- mode - The mode of the Checkout Session. This is one of payment, setup, or subscription. Setting to payment is for accepting one-time payments.
- line_items - A list of items the customer is purchasing. Each item includes its price and quantity.
- success_url - The URL the customer will be redirected to after the payment is successful. The session_id parameter here is a template variable that Stripe will replace with the customer's session ID. We can then use it to generate a custom success HTML page.
- cancel_url - If set then a back button is displayed on the Stripe Checkout page. If the back button is clicked then they are redirected to the cancel_url.
The Checkout Session object represents the customer's session as they pay. If the payment is successful it contains a reference to the customer and either the successful PaymentIntent or active Subscription. An important property is the url property which is the URL that takes the customer to a Stripe hosted payment page.
Handling Checkout Success with Webhooks
After the payment is complete we need to handle the success, or in Stripe's words, fulfill the order. This is done using a webhook. A Stripe webhook is an HTTP endpoint that receives events from Stripe. Specifically, after the payment is complete, Stripe will send a POST request to a route we configure on our server containing information about the payment. After we process this payment, Stripe will redirect the user to the success_url we defined.
INFO: If our webhook endpoint is down or the event isn't acknowledged properly, the customer is redirected to the success_url 10 seconds after a successful payment.
Stripe CLI Setup
Like most things with Stripe, we can create a webhook programatically with the API or the dashboard. However, for testing webhooks locally, we need to use the Stripe CLI. The Stripe CLI helps us build and test a Stripe integration locally from the terminal. This includes securely testing the webhooks we have created. The Stripe CLI can be installed by following the install directions on the following URL:
https://github.com/stripe/stripe-cli
We can verify the Stripe CLI was installed successfully by checking its version in the terminal.
stripe -v
stripe version 1.19.5
Next, we need to login to Stripe by running the following command:
stripe login
Your pairing code is: michael-jackson-hee-hee
This pairing code verifies your authentication with Stripe.
Press Enter to open the browser or visit https://dashboard.stripe.com/stripecli/confirm_auth?t=asdsdsg87687RE23sd
Clicking enter will take us to the Stripe dashboard where we are prompted for access. The prompt will contain the pairing code displayed in the Stripe CLI login. After clicking "allow access" the following will be displayed back in the terminal.
Done! The Stripe CLI is configured for MichaelJackson with account id acct_234534dafasdf
Please note: this key will expire after 90 days, at which point you'll need to re-authenticate.
Now we can start to work with the Stripe CLI. We want it to forward events to our webhook. We can do this with the stripe listen command.
stripe listen --forward-to localhost:6789/my-webhook
Ready! You are using Stripe API Version [2024-04-10].
Your webhook signing secret is whsec_sadfg34565kjhlkjh00adsfkj123sfdg72 (^C to quit)
Note the webhook secret printed to the console. Webhook secrets are used along with a Stripe signature to verify that the webhook request came from Stripe. Store the Webhook secret in our .env file and then create a variable out of it in our server.js file.
STRIPE_WEBHOOK=whsec_sadfg34565kjhlkjh00adsfkj123sfdg72
const STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET;
INFO: The Webhook secret is sensitive and should be kept out of view from the public.
Coding a Webhook Endpoint
Now that we have Stripe set up to send events to localhost:6789/my-webhook, we should create a route to handle these events. These events will come in as a JSON payload in a POST request.
app.post('/my-webhook', express.raw({type: 'application/json'}), (req, res) => {
try {
const sig = req.headers['stripe-signature'];
const event = stripe.webhooks.constructEvent(req.body, sig, STRIPE_WEBHOOK_SECRET);
if (event.type === 'checkout.session.completed') {
console.log('Checkout session completed!', event);
// Use data in event to store customer data in database, send receipt email, etc.
// For example, set the customer as "true" for paid customer, etc.
} else {
console.log('Unhandled event', event);
}
return res.sendStatus(200);
} catch (err) {
console.error('Error handling webhook event.', err);
return res.sendStatus(400);
}
});
- Get the signature sent from Stripe and use it to construct the webhook event. The signature is used to verify the source of the Webhook request as Stripe. This prevents fake payloads.
- If the event type is checkout.session.completed then log the event to the console. This event contains a Checkout Session object that contains details about the customer and their payment.
- Use the data in the event to store customer data in the database, send receipt email, etc. This is how we can identify that a user has paid, etc.
- For any other event type just log it to the console.
- Send back a 200 response to indicate that the request was successful.
- Log any errors to the console and send back a 400 response.
INFO: Stripe requires the Webhook payload to be a string or Buffer. This is why we use the express.raw middleware before handling the request. This middleware parses request payloads into a Buffer.
Returning a 200 from the webhook tells Stripe we that recieved the event. After this, the customer will be redirected to the success_url.
Generating a Custom Success Page and Handling Cancelled Payments
After the webhook event has been handled, the user is then redirected to the success_url. We can customize this success page by using the details from the Checkout Session. For example, we can display the customer's name and payment amount. Stripe handles this by replacing the CHECKOUT_SESSION_ID placeholder in the success_url configured in the checkout session creation.
success_url: `${URL}/success?session_id={CHECKOUT_SESSION_ID}`
We can then use the session ID to get the session and customer details.
app.get('/success', async (req, res) => {
const session = await stripe.checkout.sessions.retrieve(req.query.session_id);
const customer = await stripe.customers.retrieve(session.customer);
res.send(`<html><body><h1>Thanks for your order, ${customer.name}!</h1></body></html>`);
});
We can then handle canceled payments using the cancel_url we specified in the checkout session creation.
cancel_url: `${URL}/cancel`
app.get('/cancel', (req, res) => {
res.send('<h1>Cancelled</h1>');
});
Running the Program
Now all we need to do is run the program. First, lets add a listener to our Express server.
app.listen(PORT, HOST, () => {
console.log(`Server running on ${HOST}:${PORT}`);
});
Then run npm start to run our start script.
npm start
[nodemon] restarting due to changes...
[nodemon] starting `node --env-file .env .`
Server running on localhost:6789
Next, visit localhost:6789 in a browser and click "Buy WittCepter $10". On the Stripe page, enter test payment details like below.
- Enter 4242 4242 4242 4242 for the credit card number.
- Enter any future date for the expiration date.
- Enter any 3-digit number for the CVC.
- Enter any billing postcal code.
- Click the Pay button.