TL;DR: Stripe is a payment processing platform that handles credit cards, subscriptions, and checkout flows for your app — without you ever touching raw card numbers. It uses an API you talk to from your backend, test mode so you can build safely, and webhooks to tell your app what happened after a payment. Pricing is 2.9% + $0.30 per transaction with no monthly fee. Every AI tool suggests Stripe because it has the best docs and the most training data. This is how you get paid for what you build.
What Stripe Actually Does (And Why You Don't Handle Card Numbers)
Let's start with the thing that trips everyone up the first time: you never see your customers' credit card numbers. Not in your database. Not in your code. Not anywhere. And that's not a limitation — it's the whole point.
Here's the problem Stripe solves. If you wanted to accept credit cards on your own, you'd need to pass a payment security audit called PCI DSS (Payment Card Industry Data Security Standard). That audit is designed for banks and enterprise companies. It costs tens of thousands of dollars, takes months, and requires ongoing compliance work. Nobody building a SaaS side project is doing that.
Stripe solved this problem by becoming the PCI-compliant layer for everyone. Their servers are the ones that talk to Visa and Mastercard. Their infrastructure has passed the audit. When a customer enters their card details into a Stripe checkout form, that data goes directly to Stripe's servers — it never touches yours. Stripe gives you back a token or session ID that represents the successful payment, and that's what your app works with.
Think of it like a bonded subcontractor. On a job site, you don't hand a new hire the keys to the owner's house on day one. You use a licensed locksmith who's gone through the background check, carries insurance, and knows the code of conduct. Stripe is the licensed locksmith for money. You bring them in, they handle the actual transaction, and they hand you the paperwork when it's done.
The result: you get to add payments to your app in an afternoon without becoming a payment security expert, and your customers' card data is protected by infrastructure designed specifically to keep it safe.
Why Every AI Tool Suggests Stripe
You could use PayPal. You could use Square. You could use Braintree, Paddle, or a dozen other options. But when you ask any AI tool — Claude, Copilot, Cursor, Gemini — to add payments to your app, they all reach for Stripe. Understanding why helps you understand when to use it and when to consider something else.
The documentation is exceptional. Stripe's developer docs are widely considered the gold standard in the industry. Every concept is explained clearly. Every API endpoint has working code examples in multiple languages. The guides walk you through complete flows, not just individual function calls. Other payment platforms' documentation often feels like it was written by engineers for engineers who already know how payments work. Stripe's docs assume you're building something for the first time.
There are millions of working examples to learn from. AI tools are trained on code that exists on the internet. There are more GitHub repos, blog posts, tutorials, and Stack Overflow answers about Stripe than any other payment platform by a significant margin. When Claude generates Stripe integration code, it's drawing on an enormous pool of examples that have been verified to work. This isn't the case for less common platforms.
The API is predictable and consistent. Stripe designed their API to be intuitive. Resources follow consistent patterns. Error messages are descriptive and actionable. The SDK handles a lot of complexity for you. This makes AI-generated Stripe code much more likely to be correct on the first try, which is why AI tools reach for it — it generates code that actually works.
It handles the hardest parts automatically. Subscription renewals, failed payment retries, proration when customers upgrade plans, tax calculation, invoice generation — Stripe handles all of this. If you built these yourself, each one would take days. With Stripe, you configure them in the dashboard and they just work.
The one situation where AI tools might steer you wrong on Stripe: if you're selling digital products to European customers at scale, you might want Paddle instead, because Paddle handles EU VAT as the "merchant of record" so you don't have to. But for most US-focused SaaS products, Stripe is the right call.
The Key Concepts: What You Actually Need to Understand
When you start working with Stripe — either directly or through AI-generated code — you'll encounter four concepts over and over. Here's what each one actually means.
API Keys: Your Identity With Stripe
An API key is a long string of characters that identifies your app to Stripe. Think of it like a contractor's license number — it proves who you are and what you're authorized to do. Stripe gives you two types:
- Publishable key (starts with
pk_test_orpk_live_) — safe to include in your frontend JavaScript. It lets you use Stripe's client-side libraries but can't do anything sensitive like create charges. - Secret key (starts with
sk_test_orsk_live_) — only for your backend server. Never expose this in frontend code or commit it to GitHub. This key can create charges, issue refunds, and do anything in your Stripe account.
You store these as environment variables in your app — never hardcoded in your source files. When AI generates your Stripe setup code, it'll reference process.env.STRIPE_SECRET_KEY and process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY. Those values go in your .env.local file, which stays off GitHub.
Prompt to get started:
"Set up Stripe in my Next.js app. Install the Stripe Node SDK, create an API route that creates a checkout session, and add my publishable key to the frontend Stripe initialization. Use environment variables for both keys. I'm on Next.js 14 with the App Router."
Checkout Sessions: The Hosted Payment Page
A Checkout Session is a temporary payment page that Stripe hosts for you. You create it from your backend with the details of what's being purchased (price, quantity, what URL to redirect to after success or cancellation), and Stripe gives you back a URL. You send the customer to that URL, they enter their card details on Stripe's page, and Stripe handles the rest.
This is the fastest way to add payments to any project because you're not building a payment form — you're just building a button that creates a session and redirects. The form, the security, the card validation — all on Stripe's side.
Checkout Sessions expire after 24 hours. If a customer abandons the checkout page and comes back later, your app needs to create a fresh session. AI-generated code handles this automatically when you describe the flow correctly.
Webhooks: How Stripe Tells Your App What Happened
A webhook is an HTTP request that Stripe sends to your server when something happens in your Stripe account. Think of it like a phone call from the bank — they're reaching out to you, not the other way around.
Here's why webhooks matter: after a customer completes a payment on Stripe's checkout page, your app needs to know about it. You could wait for them to redirect back to your success page, but that's unreliable — the customer might close the tab, their internet might drop, or the redirect might fail. Stripe's webhook fires regardless of what happens to the browser session.
The events you'll handle most often:
checkout.session.completed— a customer just paid. Time to activate their account, send a confirmation email, update your database.invoice.paid— a subscription renewed successfully.invoice.payment_failed— a renewal failed. Time to notify the customer their card was declined.customer.subscription.deleted— a subscription was cancelled. Time to downgrade their access.
Stripe signs every webhook with a secret so your server can verify the request actually came from Stripe and not from someone trying to fake a payment event. Always verify this signature — AI-generated code should include this automatically when you specify that security is required.
Products and Prices: How You Define What You're Selling
In Stripe, a Product is what you're selling ("Pro Plan," "One-Time Setup Fee," "Design Consultation"). A Price is how much you charge for it, and whether it's one-time or recurring.
One product can have multiple prices. For example, your "Pro Plan" product might have a monthly price ($29/month) and an annual price ($290/year — saving two months). Both point to the same product but bill differently.
You create products and prices in the Stripe Dashboard (or via the API if you need to do it programmatically). Once created, each price gets a Price ID that looks like price_1OvXaBClCIR8A1M2eKxYzAbC. That ID is what you reference in your checkout session code to say "charge them for this thing."
This separation is useful because you can change prices without breaking existing subscriptions. If you want to raise your price from $29 to $39 per month, you create a new Price. Existing subscribers stay on the old price until you explicitly migrate them. New subscribers see the new price.
Real Example: Adding a Payment Button to a Next.js App
Let's walk through what AI-generated Stripe code actually looks like end-to-end. This is a one-time payment flow — a customer clicks a button, pays, and gets redirected to a success page.
Step 1: Install Stripe
npm install stripe @stripe/stripe-js
Step 2: Create the API route that creates a Checkout Session
In Next.js App Router, this goes in app/api/checkout/route.ts:
import { NextResponse } from 'next/server'
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-06-20',
})
export async function POST(request: Request) {
try {
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [
{
price: 'price_1OvXaBClCIR8A1M2eKxYzAbC', // your Price ID from Stripe Dashboard
quantity: 1,
},
],
mode: 'payment', // 'payment' for one-time, 'subscription' for recurring
success_url: `${process.env.NEXT_PUBLIC_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_URL}/cancel`,
})
return NextResponse.json({ url: session.url })
} catch (error) {
console.error('Stripe error:', error)
return NextResponse.json(
{ error: 'Failed to create checkout session' },
{ status: 500 }
)
}
}
Step 3: Add the button to your frontend
'use client'
export default function BuyButton() {
async function handleCheckout() {
const response = await fetch('/api/checkout', {
method: 'POST',
})
const { url, error } = await response.json()
if (error) {
alert('Something went wrong. Please try again.')
return
}
// Redirect to Stripe's hosted checkout page
window.location.href = url
}
return (
<button onClick={handleCheckout}>
Buy Now — $29
</button>
)
}
Step 4: Handle the webhook to confirm payment
Create app/api/webhooks/stripe/route.ts:
import { NextResponse } from 'next/server'
import Stripe from 'stripe'
import { headers } from 'next/headers'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-06-20',
})
export async function POST(request: Request) {
const body = await request.text()
const signature = headers().get('stripe-signature')!
let event: Stripe.Event
try {
// Verify this actually came from Stripe
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
)
} catch (error) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })
}
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session
// Payment succeeded — activate the customer's account
// e.g., update your database, send confirmation email
console.log('Payment received from:', session.customer_email)
break
}
case 'invoice.payment_failed': {
const invoice = event.data.object as Stripe.Invoice
// Subscription renewal failed — notify the customer
console.log('Payment failed for:', invoice.customer_email)
break
}
}
return NextResponse.json({ received: true })
}
Step 5: Add your environment variables to .env.local
STRIPE_SECRET_KEY=sk_test_your_key_here
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_your_key_here
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret_here
NEXT_PUBLIC_URL=http://localhost:3000
That's the full flow. Customer clicks button → your API route creates a session → customer pays on Stripe's page → Stripe fires a webhook → your app handles the event. Every AI tool will generate something close to this structure when you describe what you're building accurately.
Prompt for the full flow:
"Add Stripe payments to my Next.js 14 App Router project. I need: an API route that creates a Checkout Session for a $29 one-time payment using Price ID 'price_xxx', a buy button component that redirects to checkout, a success page that confirms the payment, and a webhook handler that listens for checkout.session.completed and logs the customer email. Use TypeScript and environment variables for all keys."
The Stripe Dashboard: Your Command Center
The Stripe Dashboard is the web interface at dashboard.stripe.com where you manage everything that isn't code. Understanding what lives here saves you a lot of time — especially when you're debugging why a payment isn't showing up or why a webhook isn't firing.
The toggle at the top is the most important thing on the page. There's a switch labeled "Test mode" and "Live mode." When test mode is on, you're looking at fake data. When it's off, you're in live mode and everything is real money. Get in the habit of checking which mode you're in before taking any action.
Key things to do in the Dashboard:
- Create products and prices. Go to Products → Add product. Set the name, description, and pricing. The Price ID it generates is what you put in your checkout session code.
- Get your API keys. Go to Developers → API Keys. Your test keys are there by default. To see live keys, switch to live mode first.
- Set up webhooks. Go to Developers → Webhooks → Add endpoint. Put in your endpoint URL (like
https://yoursite.com/api/webhooks/stripe), select which events to listen for, and Stripe generates a Webhook Signing Secret. That secret goes in your environment variables. - Test webhooks locally. Use the Stripe CLI (
stripe listen --forward-to localhost:3000/api/webhooks/stripe) to forward webhook events to your local server during development. This is essential — webhooks can't reach your localhost without it. - Inspect payment events. Under Payments, you can see every attempted transaction, whether it succeeded or failed, and the full event log. When debugging, this is where you look first.
The Dashboard also handles customer management, dispute resolution, refunds, and payouts to your bank account. Stripe transfers your balance to your bank account on a rolling basis (typically 2 days after a payment in the US), and the Payouts section shows you exactly when each transfer happens.
Test Mode vs. Live Mode: Build Without Risk
One of the best things Stripe does for developers is make testing feel exactly like the real thing. Test mode is a completely separate environment with its own data, its own API keys, and its own fake payment methods — but everything works identically to live mode. You can create products, run checkouts, fire webhooks, and simulate failures, all without touching real money.
Test card numbers you'll use constantly:
4242 4242 4242 4242— Always succeeds. Use any future expiry date and any 3-digit CVC.4000 0000 0000 0002— Always declined. Tests your error handling.4000 0025 0000 3155— Requires 3D Secure authentication. Tests the extra verification step some cards require.4000 0000 0000 9995— Insufficient funds. Tests that specific failure reason.
The workflow for building payment features:
- Build with test API keys (
sk_test_...andpk_test_...) - Test thoroughly with test card numbers
- Test webhook handling with the Stripe CLI
- When you're confident everything works, swap to live API keys
- Do one real test purchase with a card you actually own (Stripe will charge it, but you can refund it immediately)
- Go live
Never use live API keys during development. The risk isn't just accidentally charging someone — it's that your test data (fake customers, fake orders) ends up in your production analytics and makes everything harder to interpret. Keep them completely separate.
Prompt for testing help:
"I've built a Stripe checkout flow in Next.js. Help me write a test script that verifies: the checkout session creates successfully, the success URL contains the session ID, and the webhook handler correctly processes a checkout.session.completed event. Use the Stripe test mode and the test card 4242 4242 4242 4242."
Subscriptions: The SaaS Money Engine
Subscriptions are where Stripe becomes truly powerful for SaaS builders. Instead of one-time charges, you set up recurring billing that runs automatically — Stripe charges the customer every month (or year), handles the renewals, retries failed payments, and sends you a webhook for every event.
To add subscriptions, change one line in your checkout session code:
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [
{
price: 'price_1OvXaBClCIR8A1M2eKxYzAbC', // a recurring price in Stripe
quantity: 1,
},
],
mode: 'subscription', // changed from 'payment' to 'subscription'
success_url: `${process.env.NEXT_PUBLIC_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_URL}/pricing`,
// Optional: pre-fill the customer's email if you know it
customer_email: userEmail,
})
For subscriptions, the webhook events you need to handle:
checkout.session.completed— First subscription started. Create their account, set their plan to "active."invoice.paid— Monthly renewal succeeded. This fires every billing cycle.invoice.payment_failed— Renewal failed. Stripe will retry automatically, but you should email the customer to update their card.customer.subscription.deleted— Subscription was cancelled (by them or by you). Downgrade their access.customer.subscription.updated— They changed plans. Update their feature access accordingly.
Stripe also handles the customer portal — a hosted page where subscribers can update their card, change plans, or cancel. You don't build this UI. You create a portal session from your backend and redirect the customer there:
// API route to redirect to Stripe's customer portal
export async function POST(request: Request) {
const { customerId } = await request.json()
const portalSession = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: `${process.env.NEXT_PUBLIC_URL}/dashboard`,
})
return NextResponse.json({ url: portalSession.url })
}
The customer ID (cus_...) comes from the webhook event when they first subscribed. You store it in your database alongside their account so you can reference it later.
Stripe Pricing: What It Actually Costs You
Stripe charges 2.9% + $0.30 per successful card transaction in the US. No monthly fees. No setup fees. If you have zero sales this month, you pay nothing.
Let's work through what that means at different revenue levels:
- $10 sale: Stripe keeps $0.59 (5.9%). You get $9.41.
- $29/month subscription: Stripe keeps $1.14 (3.9%). You get $27.86.
- $99/month subscription: Stripe keeps $3.17 (3.2%). You get $95.83.
- $499 one-time purchase: Stripe keeps $14.77 (3.0%). You get $484.23.
The $0.30 flat fee hurts more on small transactions. A $5 product loses 8.9% to Stripe. A $100 product loses 3.2%. This is why most SaaS products with Stripe don't offer plans under $10/month — it's not just about margin, it's also about the payment processing economics making small amounts less efficient.
International cards cost slightly more — 3.9% + $0.30. If you add currency conversion, there's an additional 1% fee. For a global audience, these add up, but there's no way to avoid them with any payment processor.
What Stripe charges nothing for: creating customers, creating products, creating prices, storing payment methods, handling failed payment retries, the customer portal, webhooks, the Stripe CLI, test mode usage, or looking at analytics in the Dashboard. The only time you pay is when a transaction actually succeeds.
Common Patterns You'll Use in Real Projects
Beyond the basic checkout flow, here are the Stripe patterns that come up on nearly every monetized project.
Freemium with Paid Upgrades
Your app is free up to a limit, then requires a paid subscription. The flow: user signs up for free (no Stripe involved), uses the free tier, hits the limit, sees an upgrade prompt, clicks it, goes through Stripe checkout, comes back with a paid subscription. Your webhook handler updates their plan tier in the database when checkout.session.completed fires.
Per-Seat Pricing
Common for team tools. One account, multiple users, priced per user. Stripe handles this with the quantity field on line items — when they add a user, you update their subscription quantity via the API and Stripe prorates the charge automatically.
Usage-Based Billing
Charge based on API calls, messages sent, or compute used. Stripe has a metered billing feature where you report usage to Stripe during the billing period and they calculate the charge. More complex to set up but powerful for infrastructure or API products.
Free Trials
Offer 14 days free before billing starts. Add one parameter to your checkout session:
subscription_data: {
trial_period_days: 14,
}
Stripe won't charge the customer until the trial ends. The customer.subscription.trial_will_end webhook fires 3 days before the trial ends — use it to send a reminder email.
One-Time Setup Fee + Subscription
Charge a setup fee once, then a recurring subscription. Add both as line items in the same checkout session — one with mode: 'payment' pricing and one with mode: 'subscription' pricing. Stripe handles the mixed billing automatically.
Connecting Stripe to the Rest of Your Stack
Stripe works best when it's connected to everything else your app needs to do. Here's the typical integration pattern for a real SaaS project.
Your database stores Stripe customer IDs, subscription IDs, and plan status. When someone subscribes, you update the record in your database so the rest of your app knows what they have access to. The webhook handler is what makes these database updates.
Your authentication system (Clerk, Auth.js, Supabase) handles who's logged in. When a user completes checkout, you need to connect their Stripe customer ID to their authenticated identity. The usual pattern: pass the user's internal ID as metadata in the checkout session, read it back in the webhook.
// Pass user ID as metadata in checkout session
const session = await stripe.checkout.sessions.create({
// ...
metadata: {
userId: currentUser.id, // your internal user ID
},
})
// Read it back in the webhook
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session
const userId = session.metadata?.userId
// Now update this user's record in your database
await db.user.update({
where: { id: userId },
data: { plan: 'pro', stripeCustomerId: session.customer as string },
})
}
Your email system (Resend, SendGrid) sends transactional emails triggered by webhook events. Payment confirmed? Send a receipt. Subscription renewed? Send a renewal notification. Payment failed? Send a card update request. Connect these in the webhook handler.
For a complete project that brings all of this together, see our guide on building an e-commerce store with AI — it walks through the full stack from product catalog to checkout to order fulfillment.
Mistakes That Will Cost You Time (Learn From Others)
After helping dozens of vibe coders get Stripe working, these are the mistakes that come up most often:
Using live keys in development. You'll test with a real card, get real charges, spend 20 minutes wondering why your analytics look weird, then realize your test data is in production. Always use test keys locally.
Forgetting to verify webhook signatures. Without signature verification, anyone could POST to your webhook endpoint with a fake "payment completed" event and get a free account. Always use stripe.webhooks.constructEvent() to verify. AI-generated code usually includes this, but double-check.
Not using the Stripe CLI for local webhook testing. Webhooks are HTTP requests from Stripe to your server. Stripe can't reach localhost. Install the Stripe CLI (stripe listen --forward-to localhost:3000/api/webhooks/stripe), which creates a tunnel and forwards events to your local server.
Handling webhooks without idempotency. Stripe can send the same webhook event more than once (network issues, retries). If your webhook handler isn't idempotent — meaning it's safe to process the same event twice — you'll double-activate accounts or send duplicate emails. Check if you've already processed an event before acting on it.
Not storing the Stripe customer ID. You'll need it later for subscriptions, portal sessions, refunds, and updating plans. The checkout session webhook gives it to you — save it to your database immediately.
Checking success URL instead of using webhooks. The success URL redirect is unreliable. The browser might close before it loads. Always use webhooks as the authoritative source of truth about payment status, and use the success URL only for user-facing confirmation UI.
What to Learn Next
Now that you understand Stripe, here are the concepts that connect to it:
- What Is a Webhook? — The mechanism Stripe uses to notify your app. Understanding webhooks in general makes the Stripe-specific implementation click much faster.
- What Is an API? — Stripe is an API-first product. Understanding how APIs work gives you more control over the integration beyond what the generated code does.
- Build an E-Commerce Store With AI — A complete project that uses Stripe for checkout, including product catalog, cart management, and order fulfillment.
Frequently Asked Questions
Is Stripe safe to use for my app? Do I have to store credit card numbers?
Stripe is PCI-DSS compliant, which means they've passed the most rigorous payment security audit that exists. You never store credit card numbers in your own database — the card details go directly to Stripe's servers, never touching yours. Stripe gives you a token or session ID representing the payment, and that's what your app works with. This is actually one of the main reasons Stripe is so popular: it takes the most dangerous part of payments completely off your hands.
How much does Stripe cost?
Stripe charges 2.9% plus $0.30 per successful card transaction in the US. There's no monthly fee, no setup fee, and no minimum. If nobody buys, you pay nothing. International cards cost a bit more (3.9% + $0.30). For most early-stage vibe-coded projects, the standard per-transaction pricing is all you need to get started.
What's the difference between Stripe test mode and live mode?
Test mode uses fake API keys (starting with sk_test_) and lets you run through the full payment flow with test card numbers — no real money moves. Live mode uses real API keys (starting with sk_live_) and processes actual charges. The Stripe Dashboard has a toggle at the top to switch between them. Always build and test in test mode first. The test card number 4242 4242 4242 4242 always works in test mode with any future expiry date.
Why does every AI tool suggest Stripe when I ask about payments?
Stripe has the best developer documentation of any payment platform, and there are millions of working Stripe code examples in the training data AI tools learn from. The Stripe API is also designed to be predictable and consistent, which makes AI-generated Stripe code much more likely to be correct on the first try. When you ask Claude or Copilot to "add payments," they reach for Stripe because they've seen it work correctly in more contexts than any other option.
Do I need webhooks for basic Stripe payments?
For a simple one-time payment you can technically check the session status after redirect — but you should still use webhooks. They're essential for subscriptions, renewals, failed payments, refunds, and disputes. If you're building any kind of SaaS with recurring billing, webhooks are required — they're the mechanism Stripe uses to tell your app "someone's subscription just renewed" or "that payment failed." Without webhooks, your app won't know about events that happen outside the checkout flow.