Lesson 1 · Foundations

How money actually flows
through Stripe

Before you write a line of integration code, you need one mental model: the handful of objects money passes through, and who creates each one. Everything else in this mission hangs off this.

Why this lesson: Your mission is to hand-build payment flows for one-time downloads and SaaS subscriptions. Both are the same object model with one field flipped. Learn it once here.

The one-sentence model

You define a Price on a Product, send a buyer through a Checkout Session, and Stripe tells your server it worked via a webhook Event — which is when you grant access.

The single most important idea in this whole mission: the customer's browser redirect after payment is not proof of payment. The webhook is. Fulfill on the webhook, never on the "success" redirect. We'll hammer this in every lesson.[3]

Walk the flow — one-time download

Tags show browser your server stripe — notice the secret-key work all happens server-side.

1
Define what you sell stripe Create a Product ("UI Kit") with a one-time Price ($29). Do it once in the Dashboard or API; reuse the price ID forever.
2
Buyer clicks "Buy" browser The browser hits your backend — not Stripe directly.
3
Create a Checkout Session your server Using your secret key, call Stripe with the price + mode:'payment'. Stripe returns a session with a hosted URL.
4
Redirect to Stripe's page browser Buyer enters card details on Stripe's hosted page. You never touch raw card data — Stripe handles PCI, 3DS, receipts.
5
Stripe charges the card stripe Behind the scenes a PaymentIntent moves to succeeded.
6
Webhook fires → you fulfill your server Stripe POSTs a checkout.session.completed Event to your webhook. You verify it, then grant the download / email the file. This is fulfillment.

Here's steps 3 and 6 in Node.js — the whole one-time integration is essentially these two handlers:[1]

// STEP 3 — create the session (server-side, secret key)
const session = await stripe.checkout.sessions.create({
  mode: 'payment',              // one-time. 'subscription' for SaaS
  line_items: [{ price: 'price_123', quantity: 1 }],
  success_url: 'https://you.com/thanks?s={CHECKOUT_SESSION_ID}',
  cancel_url:  'https://you.com/pricing',
});
// send session.url to the browser → redirect there

// STEP 6 — the webhook: where you ACTUALLY grant access
const event = stripe.webhooks.constructEvent(rawBody, sig, whSecret); // verify it's Stripe
if (event.type === 'checkout.session.completed') {
  const s = event.data.object;
  grantDownload(s.customer_details.email);   // fulfill here, not on success_url
}
The subscription flow is the same diagram with two changes: the Price has a recurring field, and you create the session with mode:'subscription'. Then instead of one checkout.session.completed, Stripe keeps sending invoice.paid every billing period to tell you "keep access on."[4] That's why learning this model once covers both halves of your mission.

Check yourself

Answer these — instant feedback. This is the feedback loop; getting these right means the model stuck.

1. A buyer pays, gets redirected to your success_url, but the page load fails on their flaky WiFi. Did you fulfill the order?
Exactly. The webhook is the source of truth. The redirect is just UX — it can fail, be closed, or be faked, and the buyer still gets what they paid for.
Reread the key callout: the redirect is unreliable UX. Stripe POSTs the webhook server-to-server regardless of what the browser does — that's why you fulfill there.
2. You want to sell the same "Pro Plan" as both $99/year and $10/month. What do you create?
Right. Product = the thing; Price = how it's billed. One Product can carry many Prices (monthly, yearly, currencies, one-time add-ons).
Not quite. A Price holds a single amount + interval and is immutable. So you keep one Product and attach two recurring Prices to it.
3. What single difference turns a one-time download into a SaaS subscription, structurally?
That's the whole trick. Same Product/Customer/Checkout/webhook machinery — a recurring Price plus subscription mode, and Stripe handles the repeating invoices.
No — Checkout handles both, and webhooks matter more for subscriptions (every renewal fires one). The real switch is a recurring Price + mode:'subscription'.
4. Which key is safe to ship in your frontend JavaScript?
Correct. Publishable keys are meant for the browser. The secret key and signing secret are server-only — leaking the secret key lets anyone charge as you.
Careful — that's a server-only secret. Only the pk_… publishable key belongs in frontend code.

⚡ Your tangible win (≈10 min)

Create your first real (test-mode) money flow — no app needed, just the CLI.

  1. Install the CLI & log in: brew install stripe/stripe-cli/stripe then stripe login.[2]
  2. Create a Product + one-time Price entirely from the terminal:
    stripe products create --name="UI Kit"
    # copy the prod_… id, then:
    stripe prices create --unit-amount=2900 --currency=usd \
      --product=prod_XXX
  3. Watch a real webhook arrive: in one terminal run stripe listen --forward-to localhost:4242/webhook, and in another run stripe trigger checkout.session.completed. You'll see the exact Event your future server will receive.
  4. Open the Test-mode Dashboard and confirm your Product + Price exist.

Done? You've now touched 4 of the core objects (Product, Price, Event, and seen where the webhook lands). That's the foundation for Lesson 2.

I'm your teacher — ask me anything. Stuck on the CLI? Want the equivalent in Python? Curious why Checkout beats building card UI yourself, or how 3DS fits in? Just ask in the chat and we'll dig in before moving on.

Citations

  1. Stripe — Accept a payment with Checkout (quickstart)
  2. Stripe CLI docs (install, login, listen, trigger)
  3. Stripe — Fulfill orders with the Checkout API (fulfill on webhook)
  4. Stripe — Subscriptions overview (recurring invoices & events)