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.
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 browseryour serverstripe — notice the secret-key work all happens server-side.
1
Define what you sell stripeCreate 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" browserThe browser hits your backend — not Stripe directly.
3
Create a Checkout Session your serverUsing your secret key, call Stripe with the price + mode:'payment'. Stripe returns a session with a hosted URL.
4
Redirect to Stripe's page browserBuyer enters card details on Stripe's hosted page. You never touch raw card data — Stripe handles PCI, 3DS, receipts.
5
Stripe charges the card stripeBehind the scenes a PaymentIntent moves to succeeded.
6
Webhook fires → you fulfill your serverStripe 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 accessconst event = stripe.webhooks.constructEvent(rawBody, sig, whSecret); // verify it's Stripeif (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.
Install the CLI & log in: brew install stripe/stripe-cli/stripe then stripe login.[2]
Create a Product + one-time Price entirely from the terminal:
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.
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.