Build a Startup Cheatsheet
Payments (Stripe)
Use this Build a Startup reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.
Why Payments Are Non-Negotiable Infrastructure
Payments are the most critical integration in any monetized product. A bug in your payment flow means lost revenue, confused customers, and potential fraud liability. The answer is almost always: use Stripe, implement it carefully, and never roll your own payment processing.
Stripe is the industry standard for a reason. It has the best documentation, the widest feature set, excellent fraud tooling, and a developer experience that is genuinely pleasant.
Stripe Fees
Stripe charges per transaction — no monthly fee.
| Fee type | Cost |
|---|---|
| Card processing (US) | 2.9% + 30¢ per transaction |
| International cards | +1.5% |
| Currency conversion | +1% |
| ACH bank transfer | 0.8%, capped at $5 |
| Radar fraud protection | Included (advanced: +5¢/transaction) |
| Stripe Billing (subscriptions) | +0.5–0.8% of billing volume |
There is no monthly fee until you start earning. Stripe does not charge you until money flows through it.
Stripe Core Concepts
Customer — a Stripe object representing one of your users. Stores their payment methods and billing info. Create one customer per user and link it to your users table via stripe_customer_id.
PaymentMethod — a saved card, bank account, or wallet (Apple Pay, Google Pay).
PaymentIntent — represents one payment attempt. Tracks status: requires_payment_method → requires_confirmation → succeeded or failed.
Checkout Session — a hosted payment page Stripe provides. The simplest integration: you create a session server-side, redirect the user, and Stripe handles everything.
Price — a pricing configuration (e.g., $20/month, $199/year). Created in the Stripe dashboard or API. Reference by price_id in your code.
Subscription — a recurring billing relationship between a Customer and a Price.
Webhook — an HTTP POST Stripe sends to your server when events happen (payment succeeded, subscription canceled, invoice paid, etc.).
Integration Patterns
Pattern 1: Stripe Checkout (Recommended for Most Startups)
Stripe Checkout is a hosted payment page. You redirect the user to Stripe, Stripe handles the form and card data, then redirects back with a result. You never touch card numbers.
Best for: One-time purchases, subscription sign-ups, simplest implementation.
// Server: create a Checkout Session import Stripe from "stripe"; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); const session = await stripe.checkout.sessions.create({ payment_method_types: ["card"], mode: "subscription", line_items: [{ price: "price_xxxxx", quantity: 1 }], success_url: "https://yourapp.com/dashboard?success=true", cancel_url: "https://yourapp.com/pricing", customer_email: user.email, // pre-fill the form metadata: { userId: user.id }, // passed back in the webhook }); // Redirect the browser return redirect(session.url!);
Pattern 2: Stripe Elements / Payment Element
Stripe Elements are pre-built, embeddable UI components that collect card details directly in your page — without a redirect. More control over the UI.
Best for: When you want a seamless in-page payment flow without redirecting to Stripe's hosted page.
// Use @stripe/react-stripe-js import { Elements, PaymentElement } from "@stripe/react-stripe-js"; import { loadStripe } from "@stripe/stripe-js"; const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!); // Wrap your payment form <Elements stripe={stripePromise} options={{ clientSecret }}> <PaymentElement /> <button onClick={handleSubmit}>Pay now</button> </Elements>
Webhooks: The Critical Piece
A webhook is an HTTP POST Stripe sends to your server when something happens. This is how you know when a payment succeeds, a subscription is renewed, or a user cancels.
Never rely on the redirect URL alone to confirm payment. Users can close the browser tab, lose internet connection, or manipulate the URL. Always confirm payment via a webhook.
Setting Up Webhooks
- In the Stripe dashboard → Developers → Webhooks → Add endpoint
- Set the endpoint URL:
https://yourapp.com/api/billing/webhook - Select events to listen for:
checkout.session.completed,customer.subscription.updated,customer.subscription.deleted,invoice.payment_failed - Copy the Webhook Signing Secret — you use it to verify that the request came from Stripe
Verifying and Handling Webhooks
// app/api/billing/webhook/route.ts import Stripe from "stripe"; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); export async function POST(req: Request) { const body = await req.text(); // raw body — critical for signature verification const sig = req.headers.get("stripe-signature")!; let event: Stripe.Event; try { event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!); } catch { return new Response("Webhook signature verification failed", { status: 400 }); } switch (event.type) { case "checkout.session.completed": { const session = event.data.object as Stripe.Checkout.Session; const userId = session.metadata?.userId; // Activate the user's subscription in your DB await db.update(users).set({ isPremium: true }).where(eq(users.id, userId)); break; } case "customer.subscription.deleted": { // Deactivate premium break; } } return new Response("ok"); }
Important: If you are using Next.js and need to read the raw body for Stripe webhook signature verification, you must use
req.text()and NOT parse JSON first. Some frameworks parse JSON automatically; this breaks Stripe's signature check.
Subscriptions
Stripe Billing handles recurring payments. The minimal setup:
1. Create products and prices in the Stripe dashboard (or via API)
- Product: "Hack University Premium"
- Price: $20/month recurring, lookup key: hu_premium_monthly
2. Create a Checkout Session with mode: "subscription" pointing to the price
3. Handle webhooks to sync subscription status to your database
4. Check subscription status from your DB — never call Stripe on every request
// Using a lookup key (more stable than hardcoding price IDs) const prices = await stripe.prices.list({ lookup_keys: ["hu_premium_monthly"], limit: 1, }); const price = prices.data[0]; const session = await stripe.checkout.sessions.create({ mode: "subscription", line_items: [{ price: price.id, quantity: 1 }], // ... });
Store stripe_customer_id and stripe_subscription_id in your users table. Store subscription_status (active, trialing, past_due, canceled) and update it via webhooks.
Customer Portal
Stripe's hosted Customer Portal lets users manage their own subscription — cancel, switch plans, update payment methods — without you building that UI.
const portalSession = await stripe.billingPortal.sessions.create({ customer: user.stripeCustomerId, return_url: "https://yourapp.com/dashboard", }); return redirect(portalSession.url);
Enable the Customer Portal in Stripe Dashboard → Settings → Billing → Customer portal.
One-Time Payments
const session = await stripe.checkout.sessions.create({ payment_method_types: ["card"], mode: "payment", // not "subscription" line_items: [{ price_data: { currency: "usd", product_data: { name: "Resume Review" }, unit_amount: 4900, // $49.00 in cents }, quantity: 1, }], success_url: "https://yourapp.com/thank-you", cancel_url: "https://yourapp.com/services", });
Stripe Connect (Marketplaces)
If you are building a marketplace where third parties get paid (freelancers, creators, service providers), you need Stripe Connect. It lets you split payments between your platform and the seller.
Pricing: Additional 0.25% + 25¢ per payout (on top of standard processing fees).
The setup is substantially more complex than basic Stripe. Sellers must complete KYC (identity verification) via Stripe's onboarding flow. Plan for 1–2 weeks of integration work.
Testing
Stripe provides a test mode with fake card numbers:
| Card number | Behavior |
|---|---|
4242 4242 4242 4242 | Always succeeds |
4000 0000 0000 0002 | Always declines |
4000 0025 0000 3155 | Requires 3D Secure authentication |
4000 0000 0000 9995 | Insufficient funds |
Use any future expiry date and any 3-digit CVC.
To test webhooks locally, use the Stripe CLI:
stripe listen --forward-to localhost:3000/api/billing/webhookCommon Mistakes
- Trusting the
success_urlredirect instead of the webhook to confirm payment - Not verifying webhook signatures — anyone can POST to your endpoint
- Storing price IDs instead of lookup keys — IDs change between test and production; lookup keys stay consistent
- Not handling
invoice.payment_failed— users whose cards decline need to be notified or their access needs to be suspended - Forgetting that
subscription.statuscan bepast_due(grace period) orunpaid— check all active-ish states:active,trialing - Not idempotently handling webhooks — Stripe may send the same event twice; your handler must be safe to run multiple times