Payments
Take payments safely with Stripe
Many apps need to take money: an online shop, a paid course, a booking page, a monthly subscription. In this lesson, you'll learn how online payments work and how to add them safely with Stripe, using test mode so no real money moves.
How online payments work
Building your own card form is like building your own bank vault. It is hard, risky, and comes with strict security rules. Instead, almost every small app hands the payment to a payment provider, a company that handles cards safely for you.
Stripe is a popular choice. Every payment has the same three parts:
- Your app decides what is being bought and how much it costs
- The payment provider shows a secure payment page and takes the card
- Your server confirms the payment really happened, then saves the order
Your app never sees or stores card numbers. That is the whole point.
What is Stripe Checkout?
Stripe Checkout is a ready-made payment page run by Stripe. Your app sends the customer there, Stripe takes the card, and then sends the customer back to your app.
A checkout session is one visit to that page. Your server creates it with the items and prices, and Stripe gives back a link.
Test mode and test keys
Stripe has a test mode. It works exactly like the real thing, but no real money moves. Always build and practice in test mode.
- Create a free Stripe account and make sure the dashboard shows Test mode
- Find your test API keys (look for Developers, then API keys)
- Put them in your environment file, never in your code
STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
NEXT_PUBLIC_SITE_URL=http://localhost:3000Let's break this down:
- The publishable key (
pk_test_) is safe in the browser - The secret key (
sk_test_) can create charges, so it stays on the server - The site URL is used to build the success and cancel links
NEXT_PUBLIC_, because those values are visible in the browser.Paying with a test card
Stripe gives you fake cards for testing. The most common one is:
Card number: 4242 4242 4242 4242
Expiry: any future date (for example 12/34)
CVC: any 3 digits (for example 123)
Name / ZIP: anythingAdding checkout with a prompt
Here is a reusable prompt for any app that sells something. Use plan mode first, then let the AI build it.
Add Stripe Checkout (test mode) to my [type of app].
- Add a "Buy" / "Checkout" button on [page].
- The button calls a server-side API route that looks up the real
prices from the database and creates a Stripe Checkout Session.
Use STRIPE_SECRET_KEY from the environment file.
- Success URL: /checkout/success. Cancel URL: [page to go back to].
Build both from NEXT_PUBLIC_SITE_URL.
- Never trust prices or totals sent from the browser.
Show me the plan first. Do not edit files yet.Success and cancel pages
After paying, Stripe sends the customer to your success page. If they click back or close the form, Stripe sends them to your cancel page.
- The success page says thank you and shows an order number
- The cancel page says "No problem, nothing was charged" and links back
Here's the catch. Anyone can type /checkout/success into the address bar. So reaching the success page does not prove that anyone paid.
Confirming payment on the server
Your server must ask Stripe "did this payment really go through?" before saving an order. There are two common ways:
- Check on the success page: the server looks up the checkout session with Stripe and checks it is paid
- Webhooks: Stripe sends your server a message when a payment completes
A webhook is like a delivery company texting you "your parcel arrived". You don't keep checking. Stripe calls a special address on your server (for example /api/stripe-webhook) and tells you what happened. Webhooks still work if the customer closes the tab before the success page loads, so they are the most reliable choice.
Confirm payments on the server.
- Add a Stripe webhook route that listens for completed checkout sessions.
- Verify the webhook signature with STRIPE_WEBHOOK_SECRET.
- When a payment is confirmed, save the order and its items in the
database, linked to the user. Make sure the same order is never
saved twice.
- Explain how to test the webhook on my computer in test mode.Saving orders
Once a payment is confirmed, save it. Most apps use two tables:
orders: one row per purchase (who, total, status, date)order_items: the items inside each order, with the price actually paid
Then add a "My orders" page so customers can see their history. With Row Level Security on, each user only sees their own orders.
Example feature: carts and wishlists
Many shops let people collect items before paying. A cart is the basket you carry around the shop. A wishlist is a note of things you might buy later. Both are just database tables linked to a user.
Add a cart and a wishlist, stored per user in the database
(cart_items and wishlist_items tables, with Row Level Security).
- Item page: "Add to cart" with a quantity picker, and a heart
button to save to the wishlist.
- Header: a cart icon showing the number of items.
- /cart page: items, quantity + and -, Remove, and the total.
- /wishlist page: saved items with "Move to cart".
- Logged-out users who click "Add to cart" go to /login.
- Empty the cart after a confirmed payment.Test it: add two items, change a quantity, refresh the page, then pay with the 4242 card. The order should appear in your database and in the Stripe dashboard.
Common mistakes
Trusting the price from the browser
Wrong: "Send the cart total from the page to Stripe."
Right: "On the server, look up each price in the database and build the total there."Saving the order too early
Wrong: "Save the order when the customer clicks Checkout."
Right: "Save the order only after the server confirms the payment with Stripe."- Using live keys while building. Test keys start with
sk_test_andpk_test_. - Skipping the webhook signature check
- Forgetting the cancel page, so customers land on an error
Practice
Try these exercises to lock in what you just learned.
What's next?
Your app can now take (test) payments. Next, you'll learn how to give some users special powers with admin roles, so you can manage your content without touching the database by hand.