Secure Payments with Stripe

Build a secure checkout flow that prevents price manipulation and verifies payments cryptographically - the same patterns used by Shopify and OpenAI

Introduction

This project has been archived

We're no longer updating this project - we recommend checking out our active projects instead!

If you've already completed this project, don't worry! Your documentation is still available for you to check out and share.

If you have any questions, leave a question in our Discord and we'll get back to you ASAP!

⚡️ 30 second Summary

Companies like Shopify, OpenAI, and Etsy process billions of dollars through Stripe - the payment infrastructure that handles PCI compliance with 300+ security requirements so you don't have to. Your job is to never break that boundary by mishandling keys, skipping webhook verification, or trusting frontend prices.

Most developers make the same mistakes: hardcoding secret keys in frontend code, accepting prices from JavaScript, or assuming a successful redirect means payment completed. One compromised key or skipped verification, and you're liable for fraud.

Professional teams use battle-tested patterns: secret keys in environment variables, server-side price lookups, and cryptographic webhook verification. These aren't optional security theater - they're the minimum for handling real money. Stripe processes over $1.4 trillion annually because developers trust these patterns.

What you'll build

In this project, you'll integrate Stripe Checkout into a Next.js application with proper security practices. You'll use Cursor 2.0 to build server-side checkout sessions, configure Vercel environment variables for key management, and implement webhook signature verification.

Is Stripe free to use?

Yes for development! Stripe's test mode lets you build and test complete payment flows without processing real money. You'll use test API keys and test card numbers throughout this project. No credit card required to get started.

💡 New to Stripe?

Stripe powers payments for over a million merchants worldwide, from startups to Fortune 500 companies. Instead of building your own payment form (and taking on massive liability), you redirect users to Stripe's hosted checkout page. Card details never touch your servers - Stripe handles card validation, fraud detection, and PCI compliance.

In this project, you'll create checkout sessions server-side and verify payment confirmations through webhooks - the same patterns used by professional e-commerce platforms.

💡 Prerequisite

This project builds on Ship a Landing Page with v0 and Vercel. You'll reuse your deployed Next.js application and Vercel project from Part 1.

Not sure if this project is right for you? Check if it matches your goals

How you'll build it:

First, you'll set up Stripe with proper key management - learning why publishable and secret keys exist and where each belongs. Then you'll create a server-side checkout flow that ignores frontend-submitted prices. Finally, you'll implement webhook verification so you only fulfill orders that Stripe cryptographically confirms.

By the end of this project, you'll have:

  • A secure checkout flow that redirects to Stripe's hosted payment page.
  • Server-side price calculation that's immune to frontend manipulation.
  • Webhook signature verification that prevents fake payment confirmations.
  • 💎 Secret Mission: A discount code system validated entirely server-side.

Before you begin, try this quick quiz to get familiar with the concepts:

Want a complete demo of how to do this project, from start to finish? Check out our 🎬 walkthrough with Kahu 🎬

This project is part of a series:

  1. Part 1: v0 + Vercel (recommended)
  2. Part 2: You are here!
  3. Part 3: PostHog Analytics

Connect Stripe to Your App

Time to connect your app to Stripe! We'll set up a Stripe account, get your API keys, and configure them so your app can accept payments securely.

In this step, get ready to:

  • Create a Stripe account and get your API keys
  • Configure keys in Vercel and locally

Do you have a deployed Vercel project from Part 1?

✅ I have my Vercel project

Perfect! You're ready to add Stripe payments.

❌ I need to catch up

No worries! You'll need a deployed Next.js application on Vercel. Here's the fast-track setup (takes about 10 minutes).

What you'll set up:

  • Vercel account connected to GitHub
  • E-commerce app generated with v0
  • Live site deployed

1. Create Your Vercel Account

  • Select I'm working on personal project.
  • Choose Continue with GitHub to link your accounts.

2. Generate Your E-commerce App

  • Navigate to v0.dev and sign in with your Vercel account.
  • Enter this prompt:
Create an e-commerce web app with:
- A landing page with hero section
- A product detail page with a Buy Now button (placeholder for Stripe integration later)
- A checkout page with a Stripe placeholder (not actual integration)
- Modern, clean design with a tech/startup aesthetic
- Everything needs to be on one page WITHOUT scrolling
  • Click Generate and wait for v0 to create your UI.

What is v0?

v0 uses AI to generate React components from plain English descriptions. It's like having a frontend developer who builds exactly what you describe!

Click through the generated pages to preview:

  • Landing page with hero section.
  • Product page with Buy Now button.
  • Checkout page ready for Stripe.

3. Export to GitHub

  • Click the GitHub icon in v0's top bar.
  • Enter a repository name (e.g., v0-e-commerce-web-app) and click Create Repository.
  • Set the main branch as active.

✅ Repository created

Your code is now in GitHub!

❌ Having issues

  • Authorization error? Go to vercel.com → Settings → Git Integration → Connect GitHub
  • Name exists? Choose a different repository name

Get help or ask the NextWork community!

4. Deploy to Vercel

  • Click the Publish button in the top right.
  • Select Production and click Publish to Production.

✅ Deployment succeeded

Once it shows Ready, click Visit Site to see your live app!

❌ Having issues

  • Build failed? Try clicking Update to redeploy
  • TypeScript errors? We'll fix these in Cursor later

Get help or ask the NextWork community!

You're all set! Continue below to add Stripe.

Do you have a Stripe account already?

✅ I have a Stripe account

Perfect! Make sure you're in test mode (Sandbox) - you should see "Test mode" in the dashboard header.

  • Go to your Stripe Dashboard.
  • If you're not in test mode, click Developers → Overview → Go to test mode.

❌ I need to create one

Let's set up Stripe's test environment where you can safely experiment without processing real payments.

  • Go to stripe.com and click Start now or Sign up.
  • Complete the registration with your email and password.

Skip the Welcome Flow

Stripe shows a welcome wizard - we'll skip it to get to the dashboard.

  • Click Skip on the welcome screens.
  • Click Go to Sandbox to enter test mode.

Get to the Dashboard

Stripe may ask for business information - just refresh the page (Ctrl+R) to skip.

  • Close the setup guide by clicking the X icon.

Locate Your API Keys

  • In the Stripe Dashboard, find API keys in the menu.
  • You'll see two keys (click "Reveal" to see the secret key).

What are these two different types of keys?

  • Publishable key (pk_test_...): Safe for frontend code. Can only create payment tokens.
  • Secret key (sk_test_...): NEVER expose in frontend! Can charge cards, issue refunds, access all data. Learn more

Add Secret Key to Vercel

The secret key must be stored as an environment variable, not in code.

  • Select your e-commerce project.
  • Navigate to Settings → Environment Variables.
  • Add a new variable:
    • Name: STRIPE_SECRET_KEY
    • Value: Your secret key (sk_test_...)
    • Select all environments
  • Click Save.

You should see a popup telling you that everything has been saved!

Hey! Don't I need to redeploy?

Not yet, we'll redeploy after adding the webhook secret in Step 3. Good thinking though!

Add Keys Locally

Now create a .env.local file for local development.

  • Right-click in your project explorer and create a new file.
  • Name it .env.local.
STRIPE_SECRET_KEY=[[YOUR_STRIPE_SECRET_KEY="sk_test_your_key_here"]]

What if my secret key gets exposed?

Rotate it immediately! Go to Stripe Dashboard → Developers → API keys → Roll key. Then update your Vercel environment variable.

  • In the Stripe Dashboard, click Developers.
  • Click the menu next to your secret key and select Roll key.

Need help with API keys? Get debugging tips or ask the NextWork community!

Build the Checkout Flow

Great work! Your Stripe keys are configured. Time to build the actual payment flow!

You'll integrate Stripe Checkout - the same hosted payment page used by Shopify and thousands of e-commerce platforms. By redirecting to Stripe's page, card details never touch your servers. This drops your PCI compliance scope from 300+ requirements to around 20!

In this step, get ready to:

  • Create an API route for checkout sessions
  • Connect your checkout button and test the payment flow

Open Your Project in Cursor

  • Open Cursor and load your e-commerce project.
  • Open Cursor's chat (Ctrl+L or Cmd+L) and send this prompt:
Run this e-commerce web app by first installing the packages
required, then running the app.

Open Cursor's Built-in Browser

Cursor has a built-in browser that lets you see your app and select components directly.

  • Press Ctrl+Shift+B to open Cursor's browser.
  • Enter localhost:3000 in the address bar.

🎉 Woohoo, look at that - your web app is running inside Cursor 2.0!

What is Cursor's built-in browser?

It's a browser inside your IDE! You can view your app AND select UI elements to send directly to Cursor's AI chat. No more copy-pasting file paths.

Create the Checkout API Route

Now we need a server-side API route that creates Stripe checkout sessions. This is where we set the actual price - never trust prices from the frontend!

  • In Cursor's chat, send this prompt:
Install the Stripe npm package and create a checkout API route at
app/api/checkout/route.ts that:

1. Uses Stripe to create checkout sessions
2. Defines the product price SERVER-SIDE - never accept prices from the frontend
3. Accepts product info from the request body but looks up the real price server-side
4. Creates a Stripe checkout session with the server-side price
5. Returns the checkout URL
6. Uses STRIPE_SECRET_KEY from environment variables
7. Redirects to /success on completion, /checkout on cancel

Why define prices server-side?

A malicious user could modify frontend JavaScript to send price: 1 for a $599 item. If your backend accepts the frontend price, they pay $0.01 instead of $599. By defining the price on the server, we ignore any price the frontend might send. Learn more about price manipulation attacks

Connect Your Checkout Button

Nice! Now let's wire up your checkout button to call the API.

  • In the Cursor browser, enable element select mode.
  • Hover over and click the checkout/purchase button.
  • The selected component gets attached to your chat. Ask Cursor to implement the Stripe payment API here.

Test the Payment Flow

Time to see it all work together!

  • Click the checkout button to start the payment flow.
  • You should be redirected to checkout.stripe.com - the card form is hosted by Stripe.
  • Complete a test payment using card 4242 4242 4242 4242 (any future expiry, any CVC).

✅ Payment successful

Excellent! You've just processed your first Stripe payment!

The redirect to Stripe's domain is crucial - it means you're using Stripe Checkout (hosted), which handles all the card security for you.

❌ Something went wrong

No worries - let's debug! Check the browser console (F12 → Console) for errors.

Common issues:

  • Environment variable not found: Make sure STRIPE_SECRET_KEY is in your .env.local
  • Invalid API key: Verify you're using the test key (starts with sk_test_)
  • Network error: Check that your API route path matches the fetch URL

Get debugging help or ask the NextWork community!

Verify Payments with Webhooks

Excellent work! Your checkout flow is live. But here's the thing: the success page isn't proof of payment.

Anyone could navigate directly to /success without paying. The checkout redirect tells users payment succeeded, but you need cryptographic proof. That's where webhooks come in - they're how Stripe officially confirms payment completion.

Let's first see why this is dangerous, then fix it with signature verification.

In this step, get ready to:

  • Create a webhook and expose its vulnerability
  • Add signature verification to block fake webhooks

Create a Basic Webhook Endpoint

First, let's create a webhook endpoint WITHOUT security - to see why it's dangerous.

  • Open Cursor's chat (Ctrl+L or Cmd+L) and ask Cursor to create a basic Stripe webhook endpoint:
Create a Stripe webhook endpoint at app/api/webhooks/stripe/route.ts that:

1. Accepts POST requests with Stripe event data
2. Handles the checkout.session.completed event
3. Logs the event details to the console
4. Returns a 200 status on success

Test the Vulnerability

Now for a scary demo! 😱 Let's prove this endpoint is dangerous by sending a fake webhook.

  • Open a terminal and run this command:

Windows (PowerShell)

$body = '{"type":"checkout.session.completed","data":{"object":{"id":"fake_session_123","created":1699123456,"amount_total":29900,"currency":"usd","payment_status":"paid"}}}'

Invoke-WebRequest `
  -Uri "http://localhost:3000/api/webhooks/stripe" `
  -Method POST `
  -Body $body `
  -ContentType "application/json" `
  -Headers @{"stripe-signature"="fake_signature_123"}

Mac/Linux (Terminal)

curl -X POST http://localhost:3000/api/webhooks/stripe \
  -H "Content-Type: application/json" \
  -H "stripe-signature: fake_signature_123" \
  -d '{"type":"checkout.session.completed","data":{"object":{"id":"fake_session_123","created":1699123456,"amount_total":29900,"currency":"usd","payment_status":"paid"}}}'

😱 Status: 200 - It passed!

Yikes! Your server accepted a completely fake webhook. If your app fulfilled orders based on this, an attacker just got free products.

This is why we need signature verification.

🤔 It failed

Your endpoint might already have some protection, or there's an error in the code. Check your terminal output. Either way, we'll add proper signature verification next.

Add Signature Verification

Now let's fix this vulnerability! In Cursor's chat, send this prompt:

Update the webhook endpoint to verify Stripe signatures:

1. Get the raw request body as text (not JSON parsed)
2. Get the stripe-signature header from the request
3. Use STRIPE_WEBHOOK_SECRET from environment variables
4. Verify the signature using stripe.webhooks.constructEvent()
5. If verification fails, return 400 with "Invalid signature"
6. Only process the event if verification succeeds

What does signature verification do?

Stripe signs every webhook with a secret key. Your server uses stripe.webhooks.constructEvent() to verify the signature - if it doesn't match, the webhook is fake. Only Stripe can generate valid signatures!

Configure the Webhook in Stripe Dashboard

Now let's register your webhook endpoint with Stripe and get the signing secret.

  • Select Webhooks from the menu.
  • Click Add destination.
  • Select the checkout.session.completed event.
  • Also select the payment_intent.payment_failed event.
  • Click Continue.
  • Select Webhook endpoint.
  • Enter your endpoint URL: https://your-app.vercel.app/api/webhooks/stripe
  • Copy the Signing secret (starts with whsec_...).

Add Webhook Secret to Vercel

  • Click Settings.
  • Select Environment Variables.
  • Add STRIPE_WEBHOOK_SECRET with your whsec_... value and save.
  • Redeploy your application for the changes to take effect.

Wait! Make sure you don't forget to...

Add STRIPE_WEBHOOK_SECRET to your .env.local file too! The test below runs on localhost, so your local environment needs the signing secret:

STRIPE_WEBHOOK_SECRET=whsec_your_signing_secret_here

Restart your dev server after adding it.

Verify the Security Fix

Now let's prove the vulnerability is fixed! 🔒 Run the same fake webhook command again:

Windows (PowerShell)

$body = '{"type":"checkout.session.completed","data":{"object":{"id":"fake_session_123","created":1699123456,"amount_total":29900,"currency":"usd","payment_status":"paid"}}}'

Invoke-WebRequest `
  -Uri "http://localhost:3000/api/webhooks/stripe" `
  -Method POST `
  -Body $body `
  -ContentType "application/json" `
  -Headers @{"stripe-signature"="fake_signature_123"}

Mac/Linux (Terminal)

curl -X POST http://localhost:3000/api/webhooks/stripe \
  -H "Content-Type: application/json" \
  -H "stripe-signature: fake_signature_123" \
  -d '{"type":"checkout.session.completed","data":{"object":{"id":"fake_session_123","created":1699123456,"amount_total":29900,"currency":"usd","payment_status":"paid"}}}'

✅ Status: 400 - Rejected!

Your server now rejects fake webhooks! The signature verification detected the invalid signature and refused to process the event.

This is cryptographic security in action - only Stripe can generate valid signatures.

❌ It still passed

No worries - let's debug:

  • Did you add STRIPE_WEBHOOK_SECRET to your .env.local?
  • Does your code call stripe.webhooks.constructEvent() before processing?
  • Did you restart your dev server after adding the environment variable?

Get debugging help or ask the NextWork community!

Verify in Stripe Dashboard

Reminder: Before we test this...

Make sure you've pushed your code to GitHub so Vercel can deploy it:

git add .
git commit -m "Add Stripe webhook with signature verification"
git push

Run a test payment through your checkout flow (use card 4242 4242 4242 4242), then check the webhook logs.

Important

Run this test on your live Vercel site (e.g., https://your-app.vercel.app), not localhost! Stripe's webhook is configured to hit your production URL.

✅ I see successful deliveries

Your webhook is working correctly! Stripe successfully sent the payment confirmation to your server, and your server responded with a 200 status.

❌ I see failed deliveries

Check the following:

  • Your endpoint URL is correct - Make sure it matches your Vercel deployment URL
  • Your server is running and accessible - The endpoint must be publicly reachable
  • The webhook secret matches - Verify STRIPE_WEBHOOK_SECRET in Vercel matches the secret shown in Stripe
  • You redeployed after adding the secret - Environment variable changes require a redeploy

Secret mission

How do platforms like Shopify and WooCommerce handle discount codes without letting attackers apply any discount they want?

By now, you've built a secure checkout flow with server-side pricing and webhook verification. In this secret mission, you'll add a discount code system that follows the same principle - the frontend can only send a code string, not control what that code is worth.

Discount Codes (Done Right)

Clean Up Your Resources

Clean Up Your Resources

Nice work completing this project! Let's make sure your secrets are secure.

Security checklist:

  • Verify your secret key isn't in any committed code
  • Check that .env.local is in your .gitignore

Keep for testing

Test mode resources don't cost anything, so you can leave them:

  • Test payments stay in your Stripe test dashboard
  • Test API keys remain valid
  • Webhook endpoints in test mode are free

This is a good choice if you plan to continue developing or reference this project later.

Move to production

When you're ready for real payments:

  • Switch to live API keys (start with pk_live_ and sk_live_)
  • Update all environment variables in Vercel
  • Create a new webhook endpoint for production
  • Test thoroughly before accepting real payments

What's the difference between test and live mode?

Test and live modes are completely separate. Test mode payments never become real charges. You can safely experiment in test mode without affecting real customers.

Remove everything

To fully remove your Stripe integration:

  • Delete webhook endpoints - Go to Stripe Dashboard → Developers → Webhooks
  • Remove environment variables - Delete STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET from Vercel
  • Roll API keys (optional) - Invalidate your test keys in Stripe Dashboard → Developers → API keys

You can always create new test keys later if you want to experiment again.

That's a wrap!

That's a wrap!

You did it! 🎊 You've built a production-ready payment flow using the same security patterns that power Shopify, OpenAI, and over a million merchants worldwide.

What you learned:

  • 🔐 How Stripe Checkout reduces your PCI compliance scope
  • 🔑 The difference between publishable and secret API keys
  • 💰 Why prices must be calculated server-side
  • ✅ How webhook signatures prevent spoofed payment confirmations
  • 💎 Server-side discount code validation (Secret Mission)

Ready to quiz yourself? 💪

p.s. Does it say "Still tasks to complete!" at the bottom of the screen?

This means you still have screenshots left to upload, or questions left to answer!

  • Press Ctrl+F (Windows) or Command+F (Mac) on your keyboard
  • Search for the text Return to later
  • Jump straight to your incomplete tasks!

📣 If you're EVER stuck - ask the NextWork community. Students like you are already asking questions about this project.

🚀 p.s. Does it say "Still tasks to complete!" at the bottom of the screen?

This means you still have screenshots left to upload, or questions left to answer!

  1. Press Ctrl+F (Windows) or Command+F (Mac) on your keyboard.
  2. Search for the text Return to later.
  3. Jump straight to your incomplete tasks!
  4. 🙋‍♀️ Still stuck? Ask the community!