TL;DR I will show you how to build a page with multiple pricing tiers in this article. Visitors can press the button and go to a checkout page. "Purchase" Once completed, we will send the customers to a success page and save them into our database. This use case can be helpful in: Purchase a course Purchase a subscription Purchase a physical item Donate money Buy you a coffee 😉 And so many more. Can you help me out? ❤️ I love making open-source projects and sharing them with everybody. If you could help me out and I would be super, super grateful ⭐️ star the project (this is also the source code of this tutorial) https://github.com/github-20k/growchief Let's set it up 🔥 Let's start by creating a new NextJS project: npx create-next-app@latest Just click enter multiple times to create the project.I am not a big fan of Next.JS's new App router - so I will use the old folder, but feel free to do it your way. pages Let's go ahead and add the pricing packages.Let's make a new folder called components and add our pricing component. mkdir components cd components touch pricing.component.tsx And add the following content: export const PackagesComponent = () => { return ( <div className="mt-28"> <h1 className="text-center text-6xl max-sm:text-5xl font-bold"> Packages </h1> <div className="flex sm:space-x-4 max-sm:space-y-4 max-sm:flex-col"> <div className="flex-1 text-xl mt-14 rounded-xl border border-[#4E67E5]/25 bg-[#080C23] p-10 w-full"> <div className="text-[#4d66e5]">Package one</div> <div className="text-6xl my-5 font-light">$600</div> <div> Short description </div> <button className="my-5 w-full text-black p-5 max-sm:p-2 rounded-3xl bg-[#4E67E5] text-xl max-sm:text-lg hover:bg-[#8a9dfc] transition-all" > Purchase </button> <ul> <li>First feature</li> <li>Second feature</li> </ul> </div> <div className="flex-1 text-xl mt-14 rounded-xl border border-[#9966FF]/25 bg-[#120d1d] p-10 w-full" > <div className="text-[#9967FF]">Package 2</div> <div className="text-6xl my-5 font-light">$1500</div> <div> Short Description </div> <button className="my-5 w-full text-black p-5 max-sm:p-2 rounded-3xl bg-[#9966FF] text-xl max-sm:text-lg hover:bg-[#BB99FF] transition-all" > Purchase </button> <ul> <li>First Feature</li> <li>Second Feature</li> <li>Thired Feature</li> </ul> </div> <div className="flex-1 text-xl mt-14 rounded-xl border border-[#F7E16F]/25 bg-[#19170d] p-10 w-full" > <div className="text-[#F7E16F]">Package 3</div> <div className="text-6xl my-5 font-light">$1800</div> <div> Short Description </div> <button className="my-5 w-full text-black p-5 max-sm:p-2 rounded-3xl bg-[#F7E16F] text-xl max-sm:text-lg hover:bg-[#fdf2bb] transition-all" > Purchase </button> <ul> <li>First Feature</li> <li>Second Feature</li> <li>Thired Feature</li> <li>Fourth Feature</li> <li>Fifth Feature</li> </ul> </div> </div> </div> ); }; This is a very simple component with Tailwind (CSS) to show three types of packages ($600, $1500, and $1800). Once clicked on any of the packages, we will move the visitor to a purchase page where they can purchase the package. Go to the root dir and create a new index page (if it doesn't exist) cd pages touch index.tsx Add the following code to the file: import React from 'react'; import {PackagesComponent} from '../components/pricing.component'; const Index = () => { return ( <> <PackagesComponent /> </> ) } Setting up your payment provider 🤑 💰 Most payment providers work in the same way. Send an API call to the payment provider with the amount you want to charge and the success page to send the user after the payment. You get a URL back from the API call with a link to the checkout page and redirect the user (user leaving your website). Once the purchase is finished, it will redirect the user to the success page. The payment provider will send an API call to a specific route you choose to let you know the purchase is completed (asynchronically) I use Stripe - it is primarily accessible everywhere, but feel free to use your payment provider. Head over to Stripe, click on the developer's tab, move to "API Keys," and copy the public and secret keys from the developer's section. Go to the root of your project and create a new file called and paste the two keys like that: .env PAYMENT_PUBLIC_KEY=pk_test_.... PAYMENT_SECRET_KEY=sk_test_.... Remember that we said Stripe would inform us later about a successful payment with an HTTP request? Well... we need to Set the route to get the request from the payment Protect this route with a key So while in the Stripe dashboard, head to "Webhooks" and create a new webhook. You must add an "Endpoint URL". Since we run the project locally, Stripe can only send us a request back if we create a local listener or expose our website to the web with ngrok. I prefer the ngrok option because, for some reason, the local listener didn't always work for me (sometimes send events, sometimes not). So while your Next.JS project runs, just run the following commands. npm install -g ngrok ngrok http 3000 And you will see Ngrok serves your website in their domain. Just copy it. And paste it into the Stripe webhook "Endpoint URL," also adding the path to complete the purchase /api/purchase After that, click "Select Events."Choose "checkout.session.async_payment_succeeded" and "checkout.session.completed" Click "Add event" and then "Add endpoint." Click on the created event Click on "Reveal" on "Signing key", copy it and open , and add .env PAYMENT_SIGNING_SECRET=key Sending users to the checkout page 🚀 Let's start by installing Stripe and also some types (since I am using typescript) npm install stripe --save npm install -D stripe-event-types Let's create a new API route to create a checkout URL link for our users, depending on the price. cd pages/api touch prepare.tsx Here is the content of the file: import type { NextApiRequest, NextApiResponse } from 'next' const stripe = new Stripe(process.env.PAYMENT_SECRET_KEY!, {} as any); export default async function handler( req: NextApiRequest, res: NextApiResponse ) { if (req.method !== 'GET') { return res.status(405).json({error: "Method not allowed"}); } if (!req.query.price || +req.query.price <= 0) { return res.status(400).json({error: "Please enter a valid price"}); } const { url } = await stripe.checkout.sessions.create({ payment_method_types: ["card"], line_items: [ { price_data: { currency: "USD", product_data: { name: "GrowChief", description: `Charging you`, }, unit_amount: 100 * +req.query.price, }, quantity: 1, }, ], mode: "payment", success_url: "http://localhost:3000/success?session_id={CHECKOUT_SESSION_ID}", cancel_url: "http://localhost:3000", }); return req.json({url}); } Here is what's going on here: We set a new Stripe instance with the SECRET key from our file. .env We make sure the of the route is METHOD GET. We check that we get a query string of higher than 0. price We make a Stripe call to create a Stripe checkout URL. We purchased 1 item; you can probably see that the is multiplied by 100. If we send 1, it would be $0.01; multiplied by a hundred will make it $1. unit_amount We send the back to the client. URL Let's open back our component and add the API call. packages.component.tsx const purchase = useCallback(async (price: number) => { const {url} = await (await fetch(`http://localhost:3000/api/prepare?price=${price}`)).json(); window.location.href = url; }, []); And for the full code of the page export const PackagesComponent = () => { const purchase = useCallback(async (price: number) => { const {url} = await (await fetch(`http://localhost:3000/api/prepare?price=${price}`)).json(); window.location.href = url; }, []); return ( <div className="mt-28"> <h1 className="text-center text-6xl max-sm:text-5xl font-bold"> Packages </h1> <div className="flex sm:space-x-4 max-sm:space-y-4 max-sm:flex-col"> <div className="flex-1 text-xl mt-14 rounded-xl border border-[#4E67E5]/25 bg-[#080C23] p-10 w-full"> <div className="text-[#4d66e5]">Package one</div> <div className="text-6xl my-5 font-light">$600</div> <div> Short description </div> <button onClick={() => purchase(600)} className="my-5 w-full text-black p-5 max-sm:p-2 rounded-3xl bg-[#4E67E5] text-xl max-sm:text-lg hover:bg-[#8a9dfc] transition-all" > Purchase </button> <ul> <li>First feature</li> <li>Second feature</li> </ul> </div> <div className="flex-1 text-xl mt-14 rounded-xl border border-[#9966FF]/25 bg-[#120d1d] p-10 w-full" > <div className="text-[#9967FF]">Package 2</div> <div className="text-6xl my-5 font-light">$1500</div> <div> Short Description </div> <button onClick={() => purchase(1200)} className="my-5 w-full text-black p-5 max-sm:p-2 rounded-3xl bg-[#9966FF] text-xl max-sm:text-lg hover:bg-[#BB99FF] transition-all" > Purchase </button> <ul> <li>First Feature</li> <li>Second Feature</li> <li>Thired Feature</li> </ul> </div> <div className="flex-1 text-xl mt-14 rounded-xl border border-[#F7E16F]/25 bg-[#19170d] p-10 w-full" > <div className="text-[#F7E16F]">Package 3</div> <div className="text-6xl my-5 font-light">$1800</div> <div> Short Description </div> <button onClick={() => purchase(1800)} className="my-5 w-full text-black p-5 max-sm:p-2 rounded-3xl bg-[#F7E16F] text-xl max-sm:text-lg hover:bg-[#fdf2bb] transition-all" > Purchase </button> <ul> <li>First Feature</li> <li>Second Feature</li> <li>Thired Feature</li> <li>Fourth Feature</li> <li>Fifth Feature</li> </ul> </div> </div> </div> ); }; We have added on each button of the page with the right price to create the Checkout page. onClick Notion blew my mind 🤯 Notion is an excellent tool for knowledge & documentation. I have been working for for over a year and used Notion primarily for our team. Novu If you have ever played with Notion, you have probably noticed they have a slick editor - one of the best I have ever played with (at least for me). I HAVE REALIZED YOU CAN USE NOTION CONTENT WITH AN API. I opened a notion-free account and went out to check their pricing - I was sure they would not offer API for their free tier; I was very wrong, they do, and it's super fast. Their most significant limitation is that they let you make a maximum of 3 requests per second - but that's not a big problem if you cache your website - aka getStaticProps. Processing the purchase request and adding the leads to Notion 🙋🏻♂️ Remember we set a webhook for Stripe to send us a request once payment is completed? Let's build this request, validate it, and add the customer to Notion. Since the request is not a part of the user journey and sits on a different route, it's exposed to the public. It means that we have to protect this route - Stripe offers a great way to validate it with Express, but since we are using NextJS, we need to modify it a bit, so let's start by installing Micro. npm install micro@^10.0.1 And open a new route for the purchase: cd pages touch purchase.tsx Open it up and add the following code: /// <reference types="stripe-event-types" /> import Stripe from "stripe"; import { buffer } from "micro"; import type { NextApiRequest, NextApiResponse } from "next"; const stripe = new Stripe(process.env.PAYMENT_SECRET_KEY!, {} as any); export const config = { api: { bodyParser: false } }; export default async function handler( req: NextApiRequest, res: NextApiResponse ) { const signature = req.headers["stripe-signature"] as string; const reqBuffer = await buffer(req); const event = stripe.webhooks.constructEvent( reqBuffer, signature, process.env.PAYMENT_SIGNING_SECRET! ) as Stripe.DiscriminatedEvent; if ( event.type !== "checkout.session.async_payment_succeeded" && event.type !== "checkout.session.completed" ) { res.json({invalid: true}); return; } if (event?.data?.object?.payment_status !== "paid") { res.json({invalid: true}); return; } /// request is valid, let's add it to notion } This is the code to validate the request; let's see what's going on here: We start by import typing (remember we installed before). stripe-event-types We set a new Stripe instance with our secret key. We tell the route not to parse it into JSON because Stripe sends us the request in a different format. We extract the from the header and use the function to validate the request and tell us the event Stripe sent us. stripe-signature constructEvent We check that we get the event ; if we get anything else, we ignore the request. checkout.session.async_payment_succeeded If we succeeded, but the customer didn't pay, we also ignored the request. We have a place to write the logic of the purchase. After this part, this is your chance to add your custom logic; it could be any of the: Register the user to a newsletter Register the user to the database Activate a user subscription Send the user a link with the course URL And so many more. For our case, we will add the user to Notion. Setting up notion ✍🏻 Before playing with Notion, let's create a new Notion integration. Head over to "My integrations." https://www.notion.so/my-integrations And click "New Integration" After that just add any name and click Submit Click on Show and copy the key Head over to your .env file and add the new key NOTION_KEY=secret_... Let's head over to notion and create a new Database This database won't be exposed to the API unless we specify that, so click on the "..." and then "Add connections" and click the newly created integration. Once that is done, copy the ID of the database and add it to your file. .env NOTION_CUSTOMERS_DB=your_id Now you can play with the field in the database any way you want. I will stick with the "Name" field and add the customer's name from the Stripe purchase. Not let's install notion client by running npm install @notionhq/client --save Let's write the logic to add the customer's name to our database. import { Client } from "@notionhq/client"; const notion = new Client({ auth: process.env.NOTION_KEY, }); await notion.pages.create({ parent: { database_id: process.env.NOTION_CUSTOMERS_DB!, }, properties: { Name: { title: [ { text: { content: event?.data?.object?.customer_details?.name, }, }, ], }, }, }); This code is pretty straightforward.We set a new Notion instance with the Notion secret key and then create a new row in our database with the prospect's name. And the full purchase code: /// <reference types="stripe-event-types" /> import Stripe from "stripe"; import { buffer } from "micro"; import type { NextApiRequest, NextApiResponse } from "next"; import { Client } from "@notionhq/client"; const notion = new Client({ auth: process.env.NOTION_KEY, }); const stripe = new Stripe(process.env.PAYMENT_SECRET_KEY!, {} as any); export const config = { api: { bodyParser: false } }; export default async function handler( req: NextApiRequest, res: NextApiResponse ) { const signature = req.headers["stripe-signature"] as string; const reqBuffer = await buffer(req); const event = stripe.webhooks.constructEvent( reqBuffer, signature, process.env.PAYMENT_SIGNING_SECRET! ) as Stripe.DiscriminatedEvent; if ( event.type !== "checkout.session.async_payment_succeeded" && event.type !== "checkout.session.completed" ) { res.json({invalid: true}); return; } if (event?.data?.object?.payment_status !== "paid") { res.json({invalid: true}); return; } await notion.pages.create({ parent: { database_id: process.env.NOTION_CUSTOMERS_DB!, }, properties: { Name: { title: [ { text: { content: event?.data?.object?.customer_details?.name, }, }, ], }, }, }); res.json({success: true}); } You should have something like this: You nailed it 🚀 That's all. You can find the entire source code here: https://github.com/github-20k/growchief You will find more stuff there, such as Displaying DEV.TO analytics Collecting information from Notion and displaying it on the website (CMS style) Entire purchase flow Can you help me out? ❤️ I hope this tutorial was helpful for you 🚀 Any star you can give me would help me tremendously https://github.com/github-20k/growchief Also published . here