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 typeCost
Card processing (US)2.9% + 30¢ per transaction
International cards+1.5%
Currency conversion+1%
ACH bank transfer0.8%, capped at $5
Radar fraud protectionIncluded (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_methodrequires_confirmationsucceeded 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

  1. In the Stripe dashboard → Developers → Webhooks → Add endpoint
  2. Set the endpoint URL: https://yourapp.com/api/billing/webhook
  3. Select events to listen for: checkout.session.completed, customer.subscription.updated, customer.subscription.deleted, invoice.payment_failed
  4. 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 numberBehavior
4242 4242 4242 4242Always succeeds
4000 0000 0000 0002Always declines
4000 0025 0000 3155Requires 3D Secure authentication
4000 0000 0000 9995Insufficient 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/webhook

Common Mistakes

  • Trusting the success_url redirect 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.status can be past_due (grace period) or unpaid — 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