Taking payments online: one-off orders, subscriptions and the webhooks that keep them honest.
Stripe from zero: how card payments work, then products and prices, Checkout, webhooks, subscriptions, invoices, refunds and disputes, up to the expert work of idempotency, reconciliation, fraud and security. Every module follows one small online bakery that sells one-off orders and a monthly bread subscription.
What you’ll be able to do
Explain how a card payment moves between cardholder, merchant, networks and banks, and why your server never sees a card number.
Build a Checkout flow priced on the server, with success and cancel pages that never grant anything on their own.
Receive webhooks safely: verify signatures, survive retries and duplicates, and keep your database in step with Stripe.
Sell subscriptions with trials, plan changes, a customer portal, invoices and failed-payment recovery.
Handle refunds, disputes, taxes, errors and fraud, reconcile payouts, test with the Stripe CLI and go live with confidence.
Modules
How a card payment works· 8 slides
Payment processors, and why you never touch card numbers· 8 slides
Money in code· 8 slides
Your Stripe account: test mode, the Dashboard and keys· 8 slides
Your first API call with the Node library· 8 slides
Products and prices· 8 slides
Checkout: Stripe’s payment page· 8 slides
Success pages, cancel pages and what to trust· 9 slides
Webhooks from zero· 12 slides
The Stripe CLI and testing· 8 slides
Retries, duplicates and idempotent handlers· 9 slides
Payment Intents and Elements· 9 slides
Customers and subscriptions· 9 slides
Trials, plan changes and the customer portal· 8 slides
Invoices and failed payments· 8 slides
Refunds, disputes and taxes· 8 slides
Errors, retries and idempotency keys· 9 slides
Syncing state and reconciling money· 8 slides
Fraud, security and going live· 8 slides
Mastery challenge· 10 slides
Interactive parts (exercises, code to run, walkthroughs, flashcards, checklists) are drawn still and marked “Interactive · disabled in the PDF”. Do them in the app; the answers are at the end.
Module 1: How a card payment works
Stripe1 / 171
Module 1 · How a card payment works
Selling online means taking money from strangers
Picture a small bakery that wants to sell bread on its website. Some people order a loaf or two for the weekend. Others want a bread subscription: a fresh loaf every month, paid for automatically. Either way, the bakery has to take money from people it has never met, through a web page, safely.
In a shop, a card reader does that. Online, there is no reader: the buyer types their card details into a page, and a chain of companies passes the request along until a bank says yes or no. This course is about Stripe, a company that handles that chain for you, and the code you write to use it. First, the chain itself.
Cardholder
The person paying, whose name is on the card.
Merchant
The business being paid. Here, the bakery.
Payment
Money moving from the cardholder’s account to the merchant’s.
Stripe2 / 171
Module 1 · How a card payment works
Four parties and a card network take part in every card payment
The request follows the arrows to the issuer; the answer comes back the same way.
Issuer
The bank that gave the cardholder their card, and holds their money.
Acquirer
The bank that accepts card payments on the merchant’s behalf.
Card network
The company whose rails connect issuers and acquirers: Visa, Mastercard, American Express and others.
Stripe3 / 171
Module 1 · How a card payment works
Check: who decides?
QuizInteractive · disabled in the PDF
A customer’s card is declined at the bakery’s checkout. Who made that decision?
A customer’s card is declined at the bakery’s checkout. Who made that decision?
Stripe4 / 171
Module 1 · How a card payment works
Authorisation asks; capture takes
A card payment happens in two steps, even when they look like one. Authorisation asks the issuer “can this card pay €12?”. If yes, the issuer puts a hold on that amount: the money is set aside but hasn’t moved. Capture then tells the issuer “take it now”. Most online shops capture straight after authorising, so the buyer only ever notices one step.
Authorisation: the issuer approves and holds the amount.
Capture: the merchant claims the held amount.
Settlement: over the next day or few, the money travels from issuer to acquirer.
Payout: the merchant’s money lands in its bank account.
Note. Why split them? A hotel authorises your card when you book and captures when you leave. A shop that ships later can capture on shipping. A hold that is never captured is released after some days, and the buyer is never charged.
Stripe5 / 171
Module 1 · How a card payment works
Practise: the life of a payment
Put in orderInteractive · disabled in the PDF
Put the steps of a weekend loaf order in order.
1.The bakery captures the payment
2.The money settles and is paid out to the bakery’s bank
3.The issuer approves and holds the amount
4.The request travels through the acquirer and the network to the issuer
5.The buyer types their card details and presses Pay
Stripe6 / 171
Module 1 · How a card payment works
Payments cost a fee, and some fail
Every company in the chain takes a small cut, so the merchant receives a little less than the buyer paid. That difference is the fee. Stripe charges one combined fee per successful payment, usually a percentage plus a fixed amount, depending on the country and the card.
A payment can…
Because…
Succeed
The issuer approved and the merchant captured.
Be declined
Not enough money, a wrong number, the card expired, or the issuer suspects fraud.
Need the buyer to act
The issuer wants proof it’s really them, such as a code in their banking app.
Be reversed later
Refunded by the merchant, or disputed by the cardholder with their bank.
QuizInteractive · disabled in the PDF
A buyer pays €30.00 for a month of bread. What reaches the bakery’s bank account?
A buyer pays €30.00 for a month of bread. What reaches the bakery’s bank account?
Stripe7 / 171
Module 1 · How a card payment works
Cards are one way to pay among many
Buyers also pay with wallets (Apple Pay, Google Pay: a card stored on a phone), bank debits (money pulled straight from a bank account) and local methods in some countries. Each is a payment method. Some answer in seconds; some, like bank debits, take days to confirm, which your code will have to handle later.
Tip. You don’t have to support each method yourself. A payment processor, the subject of the next module, offers many through one integration, and you switch them on and off in its settings.
Stripe8 / 171
Module 1 · How a card payment works
Recap
A card payment involves the cardholder, the merchant, the acquirer, the card network and the issuer.
The issuer, the cardholder’s bank, approves or declines.
Authorisation holds the money; capture takes it; settlement and payout move it over days.
The merchant receives the amount minus fees.
Payments can be declined, need the buyer to act, or be reversed later.
FlashcardsInteractive · disabled in the PDF
Issuer
The cardholder’s bank. It approves or declines the payment.
Acquirer
The bank that accepts card payments for the merchant.
Card network
Visa, Mastercard and others: they connect issuers and acquirers.
Authorisation
Asking the issuer to approve and hold an amount.
Capture
Claiming an authorised amount, so the money actually moves.
Payout
Money transferred from the processor to the merchant’s bank account.
Module 2: Payment processors, and why you never touch card numbers
Stripe9 / 171
Module 2 · Payment processors, and why you never touch card numbers
A processor connects your shop to the card world
Signing a deal with an acquiring bank, connecting to every card network and keeping card data safe is far too much for a bakery. A payment processor does all of it and gives you one simple door. Stripe is a processor: it acts as (or works with) the acquirer, talks to the networks, keeps the money for you, and pays it out to your bank.
Your website and serverwhat you build
Stripepayment pages, the API, fraud checks, payouts
Acquirers and card networksVisa, Mastercard…
Issuersthe buyers’ banks
Payment processor
A company that accepts payments for merchants and handles the banks and networks behind them.
Stripe10 / 171
Module 2 · Payment processors, and why you never touch card numbers
Card numbers are dangerous to hold
A card number is enough to steal money, so the card industry has a security standard for anyone who handles one: PCI DSS (the Payment Card Industry Data Security Standard). In one sentence: if card numbers pass through your servers, you must prove, with audits and a long list of controls, that nobody can steal them.
The easy way to stay out of that is to never let a card number reach your code. With Stripe, the buyer types their card into a page or a form field that Stripe serves. Your server only ever sees Stripe’s ids and results, never the card.
Never build your own card field. A plain <input name="card"> posting to your own server puts you fully in scope of PCI, and puts your customers at risk. Always use Stripe’s hosted page or Stripe’s own form fields.
Stripe11 / 171
Module 2 · Payment processors, and why you never touch card numbers
Check: who sees the card?
QuizInteractive · disabled in the PDF
The bakery uses Stripe’s hosted payment page. Which of these ever receives the buyer’s full card number?
The bakery uses Stripe’s hosted payment page. Which of these ever receives the buyer’s full card number?
Stripe12 / 171
Module 2 · Payment processors, and why you never touch card numbers
Your code talks to Stripe through its API
An API (application programming interface) is a way for one program to ask another to do something. Stripe’s API lives on the web at https://api.stripe.com. Your server (the program that runs your website, on a computer you control) sends it an HTTP request, the same kind of message a browser sends to load a page, and gets an answer back in JSON, a plain text format for data.
A Customer, as Stripe sends it back. Ids shortened with “...”.
API
A set of requests one program can send another, and the answers it gets back.
Server
A program, running on a computer you control, that answers requests from browsers.
JSON
Text that describes data with { } for objects, [ ] for lists, and "key": value pairs.
Stripe13 / 171
Module 2 · Payment processors, and why you never touch card numbers
Everything in Stripe is an object with an id
Stripe models payments as objects: a Customer, a Product, a Price, a Checkout Session, a Payment Intent, a Refund. Each has an id that starts with a short prefix telling you what it is, an object field naming its type, and created, the time it was made, in seconds since 1 January 1970 (a Unix timestamp).
Prefix
Object
For the bakery
cus_
Customer
A person who buys, with their email and saved cards
prod_, price_
Product, Price
A sourdough loaf; what it costs
cs_
Checkout Session
One visit to Stripe’s payment page
pi_
Payment Intent
One payment, from start to finish
sub_
Subscription
A monthly bread plan
re_
Refund
Money given back
evt_
Event
A note that something happened
Stripe14 / 171
Module 2 · Payment processors, and why you never touch card numbers
Practise: read an id
QuizInteractive · disabled in the PDF
A support email mentions sub_.... What should the bakery look for in its Stripe account?
A support email mentions sub_.... What should the bakery look for in its Stripe account?
QuizInteractive · disabled in the PDF
Which of these is safe to store in the bakery’s own database?
Which of these is safe to store in the bakery’s own database?
Stripe15 / 171
Module 2 · Payment processors, and why you never touch card numbers
Two ways to take a payment with Stripe
Stripe Checkout
You send the buyer to a payment page Stripe hosts.
Very little code; Stripe handles cards, wallets and languages.
Where this course starts.
Your own form, with Elements
Stripe’s card fields sit inside your page.
More control over the look; more code.
Covered later, once the basics are solid.
Both keep the card away from your server, and both end the same way: your server learns from Stripe whether the payment succeeded, and only then hands over the bread.
Stripe16 / 171
Module 2 · Payment processors, and why you never touch card numbers
Recap
A payment processor like Stripe connects your shop to banks and card networks.
PCI DSS is the card industry’s security standard; keeping card numbers off your server keeps you out of most of it.
Your server talks to Stripe’s API with HTTP requests and gets JSON back.
Stripe’s objects have ids with prefixes: cus_, price_, cs_, pi_, sub_, evt_.
Start with Stripe Checkout, the hosted page; your own form with Elements comes later.
FlashcardsInteractive · disabled in the PDF
Payment processor
A company that accepts payments for you and deals with the banks and networks.
PCI DSS
The card industry’s security standard for anyone who handles card numbers.
API
A way for one program to send requests to another and get answers.
JSON
A text format for data: objects, lists, keys and values.
Unix timestamp
A time as the number of seconds since 1 January 1970 (UTC).
What is cs_...?
A Checkout Session: one visit to Stripe’s payment page.
Module 3: Money in code
Stripe17 / 171
Module 3 · Money in code
Computers are bad at decimal fractions
Most programming languages store numbers with a fraction, like 8.50, as floating-point numbers: a binary approximation. Approximations are fine for a temperature, but not for money. In JavaScript and TypeScript, adding ten and twenty cents gives a number that is almost thirty.
console.log prints a value. Whole numbers (integers) add exactly.
Integer
A whole number with no fraction: 0, 850, -3.
Floating point
How computers store numbers with fractions: fast, but approximate. Not for money.
Stripe18 / 171
Module 3 · Money in code
Stripe counts money in the smallest unit
So Stripe never takes decimals for money. Every amount is an integer in the currency’s smallest unit: cents for US dollars, cents for euros, pence for pounds. A sourdough loaf at $8.50 is 850. A monthly bread plan at $30.00 is 3000.
You mean
You send
Unit
$8.50
850
cents
€12.00
1200
euro cents
£0.99
99
pence
¥500
500
yen (no smaller unit)
Tip. Keep amounts as integers everywhere in your own code too: in your database, in your cart, in calculations. Only turn them into “$8.50” at the very last moment, to show a person.
Stripe19 / 171
Module 3 · Money in code
Practise: amounts
QuizInteractive · disabled in the PDF
Two rye loaves at $7.25 each. What amount does Stripe expect?
Two rye loaves at $7.25 each. What amount does Stripe expect?
Fill the blanksInteractive · disabled in the PDF
Total a cart in cents: three croissants at 350 each and one loaf at 850.
A currency is named by a three-letter ISO 4217 code, which Stripe writes in lowercase: usd, eur, gbp, jpy. An amount means nothing without its currency: 1200 is $12 in usd but only ¥1,200 (about $8) in jpy.
Most currencies split into 100 smaller units. A few, called zero-decimal currencies, don’t: the Japanese yen and the Korean won are counted whole, so the integer is the amount. Stripe also has a minimum amount per currency (for US dollars, 50 cents), below which a charge is refused.
QuizInteractive · disabled in the PDF
The bakery sells a ¥900 loaf in Japan. What does it send to Stripe?
The bakery sells a ¥900 loaf in Japan. What does it send to Stripe?
Lines 1: A function is a named piece of code you can run again. This one takes an amount in the smallest unit and a currency code.
Lines 2-5:Intl.NumberFormat is built into JavaScript. It knows each currency’s symbol and how many decimals it has.
Lines 6-8: Ask it how many decimals this currency uses (2 for dollars, 0 for yen), and divide only now, just for display.
Lines 11-12: The same function shows dollars and yen correctly.
PlaygroundInteractive · disabled in the PDF
TypeScript
1functionformatPrice(amount:number, currency:string){2const format =new Intl.NumberFormat('en-US',{3style:'currency',4 currency5})6const decimals = format.resolvedOptions()7.maximumFractionDigits ??28return format.format(amount /10** decimals)9}1011console.log(formatPrice(850,'usd'))// $8.5012console.log(formatPrice(900,'jpy'))// ¥90013console.log(formatPrice(1200,'eur'))// €12.0014console.log(0.1+0.2,10+20)// why amounts are integers
Try the locale 'de-DE' or 'ja-JP' instead of 'en-US', or the currency 'gbp'.
Going deeper
Where Stripe and Intl disagree. For a few currencies, Stripe’s smallest unit isn’t what Intl reports. Stripe counts the Icelandic króna (isk), the Ugandan shilling (ugx) and the Hungarian forint (huf) in hundredths, so ISK 5 is 500, but Intl can report no decimals for them, and formatPrice(500, 'isk') would say ISK 500. If you sell in one of them, divide by 100 for it yourself. Stripe’s currencies page lists these special cases.
Stripe22 / 171
Module 3 · Money in code
Round once, on purpose
Sometimes a calculation does produce a fraction: a 15% discount on a 850-cent loaf is 722.5 cents. Decide how to round, round once, and keep the result as an integer from then on. Math.round rounds to the nearest whole number.
Find the bugInteractive · disabled in the PDF
Stripe rejects this request with “Invalid integer”. Which line causes it?
TypeScript
Stripe23 / 171
Module 3 · Money in code
Try it: total a cart in cents
This is the kind of code that runs before you ever call Stripe: your own cart, in your own server, kept in integers from start to finish.
Code challengeInteractive · disabled in the PDF
Write cartTotal(lines, percentOff). Each line has a unitAmount in cents and a quantity. Add them up, then take off the discount: percentOff percent of the subtotal, rounded to the nearest cent with Math.round.
TypeScript
1typeLine={ unitAmount:number; quantity:number}23functioncartTotal(lines:Line[], percentOff =0):number{4// TODO: the subtotal in cents, minus the rounded discount5return06}
Stripe24 / 171
Module 3 · Money in code
Recap
Floating-point numbers are approximate; never do money in them.
Stripe amounts are integers in the smallest unit: 850 is $8.50.
Currencies are lowercase ISO codes; an amount needs its currency.
Zero-decimal currencies like jpy have no smaller unit.
Format for people only at the end, with Intl.NumberFormat; round once, deliberately.
FlashcardsInteractive · disabled in the PDF
How is $8.50 sent to Stripe?
850: an integer number of cents.
Zero-decimal currency
One with no smaller unit, like the yen: the integer is the whole amount.
ISO 4217
The standard three-letter currency codes, like usd and eur.
Why not 0.1 + 0.2?
Floating point gives 0.30000000000000004: approximate, wrong for money.
Stripe’s minimum for usd?
50 cents.
Module 4: Your Stripe account: test mode, the Dashboard and keys
Stripe25 / 171
Module 4 · Your Stripe account: test mode, the Dashboard and keys
An account is free, and you can practise at once
Anyone can make a Stripe account at stripe.com with an email address. You don’t need a company or a bank account to start: those are only asked for when you want to take real money. Until then, everything happens in a practice world.
Try itInteractive · disabled in the PDF
Stripe26 / 171
Module 4 · Your Stripe account: test mode, the Dashboard and keys
Test mode is a copy of Stripe where no money moves
Stripe gives every account a test environment: the same API, the same Dashboard and the same events as the real thing, but with pretend cards and pretend money. Sandboxes are extra test environments, each with its own keys and settings, so you can try things without disturbing anyone. Live mode is the real world, where cards are charged.
Watch out. Test and live never share objects: not products, not prices, not customers. Something you made in test mode doesn’t exist in live mode, and will have to be made again there when you launch. Settings are different: the older test mode shares many Dashboard settings with live mode, so changing one there can change live too. A sandbox keeps its settings to itself.
Test mode
Stripe’s practice environment: real API, fake money.
Sandbox
A separate test environment with its own keys, objects and settings.
Live mode
Where real cards are charged and real money moves.
Stripe27 / 171
Module 4 · Your Stripe account: test mode, the Dashboard and keys
The Dashboard shows everything your code does
The Dashboard is Stripe’s website for your account. Whatever your code creates shows up there, and you can do most things by hand too: refund a payment, look at a customer, read the log of every request your code made.
Section
What you find
Payments (Transactions)
Every payment, its status, its fee, and refunds
Customers
Who bought, with their payments and subscriptions
Product catalogue
What you sell and its prices
Billing → Subscriptions
Recurring plans and their invoices
Developers (Workbench)
API keys, request logs, events and webhooks
Note. Menu names move around as Stripe redesigns the Dashboard. If a section isn’t where you expect, the search bar at the top finds pages, settings and objects by id.
Stripe28 / 171
Module 4 · Your Stripe account: test mode, the Dashboard and keys
Two kinds of API key: publishable and secret
An API key is a long password that tells Stripe which account a request is for and what it may do. Each environment has its own pair. The prefix tells you which is which and which world it belongs to.
Key
Starts with
Can
Lives
Publishable
pk_test_, pk_live_
Only what the browser needs, like loading Stripe’s card fields
In web pages: it’s public
Secret
sk_test_, sk_live_
Everything on the account: charge, refund, read customers
On your server only
Restricted
rk_test_, rk_live_
Only the permissions you choose
On your server only
A secret key is the keys to the till. Anyone with your live secret key can refund every payment and read every customer. Never put it in a web page, a screenshot, a chat message or a file you commit to git.
Stripe29 / 171
Module 4 · Your Stripe account: test mode, the Dashboard and keys
Check: which key goes where?
QuizInteractive · disabled in the PDF
Which key is safe to include in the bakery’s web page, where every visitor can read it?
Which key is safe to include in the bakery’s web page, where every visitor can read it?
QuizInteractive · disabled in the PDF
You made a “Sourdough” price in test mode. Your live server asks Stripe for it by its id. What happens?
You made a “Sourdough” price in test mode. Your live server asks Stripe for it by its id. What happens?
Stripe30 / 171
Module 4 · Your Stripe account: test mode, the Dashboard and keys
Keep keys in environment variables
Code shouldn’t contain keys. Instead, it reads them from environment variables: named values the computer gives a program when it starts. On your own machine, they often sit in a file called .env, which you tell git (the tool that tracks your code’s history) to ignore.
.env
1# Never commit this file. Copy keys from the Dashboard.2STRIPE_SECRET_KEY=sk_test_...3STRIPE_PUBLISHABLE_KEY=pk_test_...
The “...” stands for the rest of the key you copy from the Dashboard.
.gitignore
1.env
Tip. In production, use your host’s secret settings instead of a file. If a key ever leaks, roll it in the Dashboard: Stripe makes a new one and the old one stops working.
Stripe31 / 171
Module 4 · Your Stripe account: test mode, the Dashboard and keys
Test cards make every outcome happen on purpose
In test mode, real cards are refused. Stripe publishes test card numbers instead, each behaving in a known way. Use any future expiry date, any three-digit security code and any postcode.
Number
Behaves like
4242 4242 4242 4242
A Visa card that always succeeds
4000 0000 0000 0002
A card that is always declined
4000 0000 0000 9995
Declined for insufficient funds
4000 0025 0000 3155
Asks the buyer to confirm with their bank (3D Secure)
QuizInteractive · disabled in the PDF
You want to see what the bakery’s checkout does when a card is declined. What do you do?
You want to see what the bakery’s checkout does when a card is declined. What do you do?
Stripe32 / 171
Module 4 · Your Stripe account: test mode, the Dashboard and keys
Recap
Test mode and sandboxes are practice worlds; live mode moves real money. They share no objects (test mode does share some settings with live).
The Dashboard shows everything, and lets you do most things by hand.
Publishable keys (pk_) are public; secret (sk_) and restricted (rk_) keys stay on the server.
Read keys from environment variables; keep .env out of git; roll a key that leaks.
Test cards like 4242 4242 4242 4242 make each outcome happen on demand.
FlashcardsInteractive · disabled in the PDF
Publishable key
A public key (pk_) for the browser; it can do very little alone.
Secret key
A server-only key (sk_) that can do everything on the account.
Restricted key
A server-only key (rk_) limited to the permissions you choose.
Environment variable
A named value given to a program when it starts, like STRIPE_SECRET_KEY.
Rolling a key
Replacing it with a new one, so the old one stops working.
The always-succeeds test card
4242 4242 4242 4242, any future date, any code.
Module 5: Your first API call with the Node library
Stripe33 / 171
Module 5 · Your first API call with the Node library
Three tools: a terminal, Node.js and npm
Server code for this course is written in TypeScript, JavaScript with types that catch mistakes before the code runs. To run it you need a few tools, all free.
Terminal
A window where you type commands instead of clicking. On macOS it’s Terminal; on Windows, PowerShell or Windows Terminal.
Node.js
A program that runs JavaScript (and, in recent versions, TypeScript) outside a browser.
npm
Node’s package manager: it downloads code other people published, called packages.
Package
A bundle of reusable code, like Stripe’s library, installed into a project.
Note. Stripe’s Node library 23 needs Node 20 or newer. Node 22.18 and later can run .ts files directly, which is what this module uses.
Stripe34 / 171
Module 5 · Your first API call with the Node library
Make a project and install stripe
Terminal
1mkdirbakery&&cdbakery# a new folder, and go into it2npminit-y# make package.json3npmpkgsettype=module# use modern import syntax4npminstallstripe# download Stripe's library
Type each line in the terminal and press Enter. Text after # is a comment.
bakery/
node_modules/# downloaded packages; never edit
.env# your test keys (module 4)
.gitignore# lists .env, so git skips it
package.json# the project's name and packages
first.ts# the script you'll write next
Stripe35 / 171
Module 5 · Your first API call with the Node library
Lines 1:import brings Stripe’s library into this file. Stripe is a class: a template for making objects.
Lines 3-4:process.env holds the environment variables. If the key is missing, stop at once with a clear message rather than a confusing failure later.
Lines 6:new Stripe(key) makes a client: an object whose methods send requests to Stripe with your key.
Lines 8-11: Ask Stripe to create a Customer. await waits for Stripe’s answer, which takes a moment because it travels over the internet.
Lines 13: Print the new customer’s id, something like cus_....
Terminal
1node--env-file=.envfirst.ts
--env-file loads .env into process.env before the script runs.
Stripe36 / 171
Module 5 · Your first API call with the Node library
Check your work in the Dashboard
Open Customers in the Dashboard (in test mode): Sam Baker is there, made by your script. Run the script again and you get a second Sam Baker with a different id. Stripe doesn’t guess that you meant the same person; avoiding duplicates is your job, and a later module shows how.
Try itInteractive · disabled in the PDF
Stripe37 / 171
Module 5 · Your first API call with the Node library
The library mirrors the API
Resources follow the web address: /v1/customers is stripe.customers, /v1/checkout/sessions is stripe.checkout.sessions.
Methods are verbs: create, retrieve, update, list, del.
Parameters keep Stripe’s snake_case names, like unit_amount, not unitAmount.
Every method returns a Promise, a value that arrives later, so you await it.
Fill the blanksInteractive · disabled in the PDF
Read back a customer you made earlier, by its id.
TypeScript
1const sam = stripe..(customerId)
QuizInteractive · disabled in the PDF
Which call changes Sam’s email?
Which call changes Sam’s email?
Stripe38 / 171
Module 5 · Your first API call with the Node library
Read the error: it usually says what’s wrong
When Stripe refuses a request, the library throws an error: it stops your code and reports what happened. The error has a type, a message written for developers, and often a param naming the field at fault.
Error
Usually means
StripeAuthenticationError
The key is wrong, revoked, or missing
StripeInvalidRequestError
A parameter is wrong or missing, or no object has that id
StripeCardError
A card was declined
StripeConnectionError
Your server couldn’t reach Stripe
Find the bugInteractive · disabled in the PDF
Stripe answers “Received unknown parameter: emailAddress”. Which line is wrong?
TypeScript
Stripe39 / 171
Module 5 · Your first API call with the Node library
Stripe keeps a log of every request
The Dashboard’s developer section lists every request your keys made: what was sent, what came back, and how long it took. When something fails, the log is often the quickest way to see exactly what Stripe received.
API versions. Stripe changes its API in dated versions with names, like 2026-10-28.endive. Each release of the library is fixed to one version, sends it with every request, and its types describe that version. Upgrading the library can change the shape of some objects, so read the changelog when you upgrade.
Stripe40 / 171
Module 5 · Your first API call with the Node library
Recap
Install with npm install stripe; make a client with new Stripe(key), the key read from the environment.
Methods follow the API: stripe.customers.create, retrieve, update, list.
Parameters are snake_case; every call returns a Promise you await.
Errors have a type and a message; param points at the field at fault.
The Dashboard logs every request, which is the first place to look when debugging.
FlashcardsInteractive · disabled in the PDF
Client
The object made by new Stripe(key); its methods send requests.
Promise
A value that arrives later. await waits for it.
What does retrieve do?
Reads one object by its id.
StripeInvalidRequestError
A bad or missing parameter, or no object with that id.
Throwing an error
Stopping the code and reporting a problem, for something else to catch.
API version
A dated name for the API’s shape; each library release is fixed to one.
Module 6: Products and prices
Stripe41 / 171
Module 6 · Products and prices
A product says what; a price says how much
Stripe’s catalogue has two objects. A Product is the thing sold: “Sourdough loaf”, with a name, a description and maybe a picture. A Price belongs to one product and says what it costs: a currency, an amount, and whether it’s paid once or again and again. One product can have many prices.
The bakery’s catalogue
Sourdough loafproduct
$8.50, onceprice
€8.00, onceprice
Bread clubproduct
$30.00 a monthprice
$300.00 a yearprice
Product
Something you sell, described once.
Price
One way to pay for a product: currency, amount, once or recurring.
Catalogue
All your products and their prices.
Stripe42 / 171
Module 6 · Products and prices
Make them in the Dashboard or in code
The Dashboard’s product catalogue lets you add products and prices by hand, which is perfect for a small shop. Code does the same through the API, which helps when you have many products or want test and live to match exactly.
WalkthroughInteractive · disabled in the PDF
TypeScript
1const loaf =await stripe.products.create({2name:'Sourdough loaf',3description:'Baked this morning, 800 g',4default_price_data:{5currency:'usd',6unit_amount:8507}8})910console.log(loaf.id, loaf.default_price)
Lines 1-3: Create the product with a name (required) and a description.
Lines 4-7:default_price_data creates a first price at the same time: $8.50, in cents. It becomes the product’s default_price.
Lines 10: Print both ids: prod_... for the product, price_... for its price.
A price with recurring is billed again and again. interval is day, week, month or year, and interval_count stretches it: { interval: 'week', interval_count: 2 } is every two weeks. Without recurring, a price is one-time: paid once, like a loaf.
QuizInteractive · disabled in the PDF
The bakery adds a box delivered every three months for $80. Which price matches?
The bakery adds a box delivered every three months for $80. Which price matches?
Stripe44 / 171
Module 6 · Products and prices
A price’s amount never changes: make a new one
Once a price exists, its amount and currency are fixed, because subscriptions and past orders point at it. When flour costs more and a loaf goes to $9.00, you create a new price and archive the old one (active: false): it stays on old orders but can’t be used for new ones.
Fill the blanksInteractive · disabled in the PDF
Raise the loaf to $9.00: make a new price, then archive the old one.
Price ids differ between test and live, and change when you reprice. Hard-coding price_... in your code breaks both times. A lookup key is your own name for a price, like loaf_sourdough, which you can ask for by name in any environment.
TypeScript
1await stripe.prices.create({2product: loafId,3currency:'usd',4unit_amount:900,5lookup_key:'loaf_sourdough',6transfer_lookup_key:true7})89const{ data }=await stripe.prices.list({10lookup_keys:['loaf_sourdough'],11expand:['data.product']12})
transfer_lookup_key moves the name from the old price to the new one.
Note.expand asks Stripe to return a related object in full instead of just its id: here, each price’s product. A list returns its results in data.
Stripe46 / 171
Module 6 · Products and prices
Or describe the price inside each checkout
Catalogue prices
Made once, used by id or lookup key.
Shown and editable in the Dashboard.
Needed for most subscription features.
Inline `price_data`
The amount is written into each checkout.
Good when prices live in your own database.
Nothing to keep in step with Stripe.
QuizInteractive · disabled in the PDF
The bakery’s cake orders are priced by size and decoration, worked out on its server. Which suits them?
The bakery’s cake orders are priced by size and decoration, worked out on its server. Which suits them?
Stripe47 / 171
Module 6 · Products and prices
Metadata: your own labels on Stripe’s objects
Most objects take metadata: up to 50 string keys and values of your own. Keys can be up to 40 characters, values up to 500. Use it for your ids and labels, like which oven batch a product comes from or your own order number. Never put secrets or card details in it.
Products say what; prices say how much, in which currency, once or recurring.
Recurring prices have an interval and an optional interval_count.
Prices don’t change: create a new one and archive the old.
Lookup keys let code find prices by name in any environment.
Inline price_data suits amounts worked out per order.
Metadata holds your own string labels: 50 keys, 500 characters each.
FlashcardsInteractive · disabled in the PDF
One-time price
A price paid once, with no recurring.
How do you change a price’s amount?
Create a new price and set the old one to active: false.
Lookup key
Your own name for a price, to find it with prices.list({ lookup_keys }).
expand
Ask for a related object in full instead of its id.
price_data
A price described inline in a checkout, with no catalogue object.
Metadata limits
50 keys; keys up to 40 characters; string values up to 500.
Module 7: Checkout: Stripe’s payment page
Stripe49 / 171
Module 7 · Checkout: Stripe’s payment page
A Checkout Session is one visit to a payment page
A Checkout Session is a payment page Stripe builds for one purchase. Your server creates it, saying what’s being bought. Stripe answers with a url. You send the buyer there; they pay on Stripe’s page, then Stripe sends them back to a page of yours.
Stripe50 / 171
Module 7 · Checkout: Stripe’s payment page
A small server to hang it on
The examples use Express, a popular package for writing Node servers (npm install express). A server listens for requests; each route pairs a method and a path, like POST /checkout, with a function that gets the request (what the browser sent) and builds the response (what goes back).
server.ts
1import express from'express'2import Stripe from'stripe'34const stripe =newStripe(process.env.STRIPE_SECRET_KEY!)5const app =express()6app.use(express.urlencoded({extended:false}))78app.post('/checkout',async(req, res)=>{9// create a session here (next slide)10})1112app.listen(3000)
! tells TypeScript the variable is set; module 5 showed a friendlier check.
Route
A method and path (POST /checkout) and the code that answers it.
Request
What the browser sent: the path, headers, and a body such as form fields.
Response
What the server sends back: a status code, headers and a body.
Lines 2: Your site’s address; the return pages are built from it.
Lines 4:mode: 'payment' is a one-off purchase. A subscription is 'subscription' (module 13).
Lines 5-7: What’s bought: a list of line items, each a price and a quantity. The price id comes from configuration, not from the browser.
Lines 8-10: Where Stripe sends the buyer afterwards. {CHECKOUT_SESSION_ID} is filled in by Stripe (next module).
Lines 12: Send the browser to Stripe’s page. 303 means “go there, with a GET”.
Stripe52 / 171
Module 7 · Checkout: Stripe’s payment page
The browser says what; the server says how much
Anything the browser sends can be changed by the person using it, with a few clicks in the browser’s developer tools. So a form may say which loaves and how many, but the price must come from your server’s own records.
Find the bugInteractive · disabled in the PDF
Someone bought ten loaves for one cent each. Which line let them?
TypeScript
Stripe53 / 171
Module 7 · Checkout: Stripe’s payment page
Line items: a catalogue price or an inline one
TypeScript
1line_items:[2// a price from the catalogue, by id3{price: sourdoughPriceId,quantity:2},4// a price described here, for a custom cake5{6quantity:1,7price_data:{8currency:'usd',9unit_amount:4500,10product_data:{name:'Birthday cake, 8 inch'}11}12}13]
A redirect is a response that says “go to this other address instead”. Status 303 See Other tells the browser to follow it with a plain GET, which is right after a form POST. Stripe’s page lives at the session’s url, which is only valid for that one session.
QuizInteractive · disabled in the PDF
Could the bakery create one session at start-up and send every buyer to its url?
Could the bakery create one session at start-up and send every buyer to its url?
Stripe55 / 171
Module 7 · Checkout: Stripe’s payment page
Options worth knowing
Parameter
Does
customer_email
Fills in the email when you already know it
allow_promotion_codes: true
Shows a box for discount codes made in the Dashboard
shipping_address_collection
Asks for a delivery address, in the countries you list
expires_at
Ends the session sooner than the default 24 hours (at least 30 minutes ahead)
locale
The page’s language; by default it follows the buyer’s browser
Payment methods come from the Dashboard. Checkout offers the payment methods switched on in the Dashboard’s settings that suit the currency and the buyer. In library version 23 the old payment_method_types parameter is gone; to leave one out for a session, use excluded_payment_method_types.
QuizInteractive · disabled in the PDF
The bakery delivers only in the US and Canada. Which option collects a delivery address?
The bakery delivers only in the US and Canada. Which option collects a delivery address?
Stripe56 / 171
Module 7 · Checkout: Stripe’s payment page
Recap
A Checkout Session is one purchase on Stripe’s page, created by your server.
mode is payment for one-off orders and subscription for recurring ones.
Line items are a catalogue price or inline price_data, with a quantity.
Prices come from your server, never the browser.
Redirect to session.url with a 303; make a new session for each purchase.
FlashcardsInteractive · disabled in the PDF
Checkout Session
A Stripe-hosted payment page for one purchase (cs_...).
Line item
One row of an order: a price and a quantity.
Why a 303 redirect?
It sends the browser to a new address with a GET, right after a form POST.
How long does an unused session last?
24 hours by default; expires_at can shorten it.
Who decides the amount?
Your server, from its own records. Never a form field.
Where are payment methods chosen?
In the Dashboard’s payment method settings.
Module 8: Success pages, cancel pages and what to trust
Stripe57 / 171
Module 8 · Success pages, cancel pages and what to trust
Every checkout ends at one of two addresses
Address
When
Show
success_url
The buyer completed the payment page
“Thank you”, and the order
cancel_url
The buyer pressed the back link on Stripe’s page
Their cart, ready to try again
The buyer may also simply close the tab, lose their connection, or wander off. Then neither page is ever visited. Keep that in mind: it is why the success page can’t be where the order is fulfilled.
Stripe58 / 171
Module 8 · Success pages, cancel pages and what to trust
{CHECKOUT_SESSION_ID} is filled in by Stripe
The success page has to know which session just finished. Put the exact text {CHECKOUT_SESSION_ID} in success_url, and when Stripe sends the buyer back it replaces it with the real session id.
In TypeScript, the success_url is written as a template literal. Why doesn’t this break it?
Stripe59 / 171
Module 8 · Success pages, cancel pages and what to trust
The success page asks Stripe what happened
WalkthroughInteractive · disabled in the PDF
TypeScript
1app.get('/thanks',async(req, res)=>{2const id =String(req.query.session_id ??'')3if(!/^cs_(test|live)_\w+$/.test(id)){4return res.status(400).send('No order here')5}6const session =await stripe.checkout.sessions.retrieve(id)7if(session.payment_status ==='paid'){8return res.send('Thank you, your bread is on its way')9}10 res.send('We’re waiting for your payment to clear')11})
Lines 2: Read session_id from the address. req.query holds what came after the ?.
Lines 3-5: Check it looks like a session id before calling Stripe. This regular expression (a text pattern) means cs_test_ or cs_live_, then letters and digits.
Lines 6: Fetch the session from Stripe. Its fields are Stripe’s word, not the browser’s.
Lines 7-10: Only say it’s paid when Stripe says so. Some payment methods take days to clear, so “complete” isn’t always “paid”.
Stripe60 / 171
Module 8 · Success pages, cancel pages and what to trust
Visiting the success page must grant nothing
Anyone can type /thanks?session_id=... into their browser, and many paying buyers never arrive at all. So the success page only shows what Stripe reports. Recording the order, emailing the receipt and telling the kitchen to bake happen elsewhere: in a webhook, the subject of the next module.
QuizInteractive · disabled in the PDF
Where should the bakery mark an order as paid and send it to the kitchen?
Where should the bakery mark an order as paid and send it to the kitchen?
Stripe61 / 171
Module 8 · Success pages, cancel pages and what to trust
Two fields tell you where a session stands
Field
Values
Means
status
open
The page is still usable; nothing has been completed
complete
The buyer finished the page
expired
Nobody finished it in time; it can’t be used again
payment_status
paid
The money is collected
unpaid
Not paid yet, for example a bank payment still clearing
no_payment_required
Nothing to pay now, such as a free trial
QuizInteractive · disabled in the PDF
A session is complete but unpaid. What should the bakery do?
A session is complete but unpaid. What should the bakery do?
Stripe62 / 171
Module 8 · Success pages, cancel pages and what to trust
Tie the session to your own order
Your server usually makes its own order record before sending the buyer to Stripe. Put that order’s id on the session so you can find it later. client_reference_id holds one id; metadata holds more. The session also makes a Payment Intent, a separate object with its own metadata, which later refunds and disputes refer to.
Fill the blanksInteractive · disabled in the PDF
Label the session and its payment with the bakery’s order id.
Module 8 · Success pages, cancel pages and what to trust
Line items come only when you ask
A session you retrieve doesn’t include its line items unless you ask: they can be long. Expand them, or list them separately.
TypeScript
1const session =await stripe.checkout.sessions.retrieve(id,{2expand:['line_items']3})4for(const item of session.line_items?.data ??[]){5 console.log(item.quantity, item.description, item.amount_total)6}78// or, page by page:9const items =await stripe.checkout.sessions.listLineItems(id)
?. and ?? [] cope with line_items being absent if it wasn’t expanded.
Tip. Also on a completed session: amount_total (in the smallest unit), currency, and customer_details with the buyer’s email, name and address.
Stripe64 / 171
Module 8 · Success pages, cancel pages and what to trust
The cancel page keeps the cart
A buyer who goes back from Stripe’s page has changed their mind or wants to edit the cart. Show the cart again. If they edit it, make a new session; to make sure the old one can’t be paid, you can end it with stripe.checkout.sessions.expire(id).
Find the bugInteractive · disabled in the PDF
Some buyers who paid have their orders marked “cancelled”. Which line causes it?
TypeScript
Stripe65 / 171
Module 8 · Success pages, cancel pages and what to trust
Recap
success_url and cancel_url bring the buyer back, when they come back at all.
{CHECKOUT_SESSION_ID} in success_url becomes the session’s id.
The success page retrieves the session and shows what Stripe says; it grants nothing.
status says if the page was finished; payment_status says if the money arrived.
Tie sessions to your orders with client_reference_id and metadata, on the payment too.
FlashcardsInteractive · disabled in the PDF
{CHECKOUT_SESSION_ID}
Text Stripe replaces with the session id in success_url.
complete but unpaid
The buyer finished the page; the money hasn’t arrived yet.
client_reference_id
One id of your own on a Checkout Session, like your order id.
payment_intent_data.metadata
Labels on the session’s payment, which refunds and disputes point at.
Ending an unused session
stripe.checkout.sessions.expire(id).
Regular expression
A pattern for matching text, like /^cs_(test|live)_\w+$/.
Module 9: Webhooks from zero
Stripe66 / 171
Module 9 · Webhooks from zero
A webhook is Stripe calling you
Until now, your server asked Stripe things. A webhook turns that around: when something happens on your account, Stripe sends an HTTP POST to an address on your server, saying what happened. That address is your webhook endpoint, and each message carries an event.
A buyer pays, then closes the tab before reaching the success page.
A bank payment clears two days later, with nobody on your site.
A monthly bread club payment renews, or fails, in the middle of the night.
Someone refunds an order from the Dashboard.
In each case only the webhook tells your server. That is why payments are fulfilled from webhooks.
Webhook
An HTTP request a service sends to your server when something happens.
Endpoint
The address on your server that receives webhooks, like /webhooks/stripe.
Event
Stripe’s record of something that happened (evt_...), with a type and the object involved.
Stripe67 / 171
Module 9 · Webhooks from zero
An event names what happened and carries the object
Trimmed. data.object is a copy of the object at the moment the event happened.
Event type
Means
checkout.session.completed
A buyer finished a Checkout page
checkout.session.async_payment_succeeded
A delayed payment from a Checkout cleared
payment_intent.succeeded
A payment succeeded
charge.refunded
Some or all of a payment was refunded
customer.subscription.deleted
A subscription ended
Stripe68 / 171
Module 9 · Webhooks from zero
Check: who tells the bakery?
QuizInteractive · disabled in the PDF
A buyer pays for a weekend order and their phone dies before the success page loads. How does the bakery find out?
A buyer pays for a weekend order and their phone dies before the success page loads. How does the bakery find out?
Stripe69 / 171
Module 9 · Webhooks from zero
Signatures prove an event really came from Stripe
Your endpoint is a public address: anyone could post a fake “paid” event to it. So Stripe signs each delivery with a secret only you and Stripe know, the endpoint’s signing secret (whsec_...). The Stripe-Signature header carries a timestamp and the signature: a code computed from the secret, the timestamp and the exact body. Change one byte and it no longer matches.
Note. The timestamp is part of what’s signed, and the library refuses events older than five minutes by default. That stops someone who recorded a real delivery from replaying it later.
Lines 3:express.raw keeps the body as the exact bytes Stripe sent. The signature is over those bytes.
Lines 5-11:constructEvent checks the signature and the timestamp, then parses the body into a typed Stripe.Event. If anything is off, it throws.
Lines 12-14: A failed check gets a 400 and nothing else is done.
Lines 15-16: Act on the event, waiting until it’s done, then answer 200 to say “received”. If handle throws, Express 5 answers 500 instead, and Stripe tries again later.
Stripe71 / 171
Module 9 · Webhooks from zero
Verify the exact bytes Stripe sent
Find the bugInteractive · disabled in the PDF
Every real event is rejected with “Bad signature”. Which line is the cause?
TypeScript
Stripe72 / 171
Module 9 · Webhooks from zero
Try it: compute a Stripe signature
constructEvent is the right tool in an app. Writing its check once by hand shows exactly what it proves. Stripe’s v1 signature is an HMAC-SHA256 (a code made from a secret key and a message; without the key, nobody can make a matching one) of the text timestamp.body, keyed by the signing secret and written in hexadecimal. The web’s built-in crypto.subtle can compute it: importKey turns the secret into a key, sign makes the code.
TypeScript
1const key =await crypto.subtle.importKey(2'raw',// the key is plain bytes3newTextEncoder().encode('a secret'),4{name:'HMAC',hash:'SHA-256'},5false,// can't be exported again6['sign']// what it may be used for7)8const mac =await crypto.subtle.sign('HMAC', key, bytesOfMessage)
Code challengeInteractive · disabled in the PDF
Finish sign(secret, t, payload): import secret as an HMAC SHA-256 key, sign the text made of t, a dot and payload (1767225600.{…}) with it, and return the result as hex with the hex helper.
TypeScript
1constbytes=(text:string)=>newTextEncoder().encode(text)2consthex=(buffer:ArrayBuffer)=>3[...newUint8Array(buffer)]4.map((byte)=> byte.toString(16).padStart(2,'0'))5.join('')67asyncfunctionsign(secret:string, t:number, payload:string){8// TODO: HMAC-SHA256 of `${t}.${payload}`, keyed by the secret9return''10}
Stripe73 / 171
Module 9 · Webhooks from zero
Try it: verify a delivery
The Stripe-Signature header looks like t=1767225600,v1=91e6…: comma-separated key=value pairs. There can be more than one v1 (while you roll a secret over, Stripe signs with both), and a v0 you ignore. A delivery is genuine when one v1 matches the signature you compute, and recent when its t is at most 300 seconds old.
Code challengeInteractive · disabled in the PDF
Finish verify(payload, header, secret, now) (times in seconds). Read t and every v1 from the header. Return false if t is missing or more than 300 seconds before now; otherwise true when any v1 equals sign(secret, t, payload).
TypeScript
1asyncfunctionsign(secret:string, t:number, payload:string){2constbytes=(text:string)=>newTextEncoder().encode(text)3const hmac ={name:'HMAC',hash:'SHA-256'}4const key =await crypto.subtle5.importKey('raw',bytes(secret), hmac,false,['sign'])6const mac =await crypto.subtle7.sign('HMAC', key,bytes(`${t}.${payload}`))8return[...newUint8Array(mac)]9.map((byte)=> byte.toString(16).padStart(2,'0'))10.join('')11}1213asyncfunctionverify(14 payload:string, header:string, secret:string, now:number15):Promise<boolean>{16// TODO: read t and each v1; check the age; compare signatures17returnfalse18}
Tries again later, for up to three days in live mode
Failing for days
Emails you, and may disable the endpoint
QuizInteractive · disabled in the PDF
An event type the bakery doesn’t act on arrives. What should the endpoint answer?
An event type the bakery doesn’t act on arrives. What should the endpoint answer?
Stripe76 / 171
Module 9 · Webhooks from zero
Register the endpoint and keep its secret
In the Dashboard’s developer section, open Webhooks and add an endpoint.
Enter its public address, such as https://bakery.example/webhooks/stripe.
Choose the events it should receive, only the ones you handle.
Reveal its signing secret (whsec_...) and store it as STRIPE_WEBHOOK_SECRET.
Tip. Your laptop has no public address, so local development uses the Stripe CLI to forward events to it, with a secret of its own. That’s the next module.
On Cloudflare Workers and other web runtimes. Where crypto is only asynchronous (Workers, Deno, browsers), use await stripe.webhooks.constructEventAsync(body, signature, secret, undefined, Stripe.createSubtleCryptoProvider()), reading the body with await request.text().
Stripe77 / 171
Module 9 · Webhooks from zero
Recap
Webhooks are Stripe telling your server what happened, even when nobody is on your site.
An event has a type and a snapshot of the object in data.object.
Verify every delivery with constructEvent over the raw body and the signing secret.
Switch on event.type; handle delayed payments too.
Answer 2xx once handled or ignored; errors make Stripe retry for days.
FlashcardsInteractive · disabled in the PDF
Webhook endpoint
The address on your server that receives Stripe’s events.
Signing secret
The whsec_... secret used to check that a delivery came from Stripe.
Why the raw body?
The signature covers the exact bytes; a parsed and rebuilt body won’t match.
Default timestamp tolerance
Five minutes (300 seconds).
How long does Stripe retry?
Up to three days in live mode.
Async version for Workers
constructEventAsync, with Stripe.createSubtleCryptoProvider().
Module 10: The Stripe CLI and testing
Stripe78 / 171
Module 10 · The Stripe CLI and testing
The Stripe CLI brings Stripe to your terminal
A CLI (command-line interface) is a program you use by typing commands. The Stripe CLI talks to your account in test mode: it can forward webhooks to your own computer, make events happen, and show your API requests as they arrive. Install it from Stripe’s docs (on macOS, brew install stripe/stripe-cli/stripe), then connect it to your account.
Terminal
1stripelogin# opens the browser to link your account2stripe--version# check it's installed
CLI
A command-line interface: a program you drive by typing commands in a terminal.
Stripe79 / 171
Module 10 · The Stripe CLI and testing
stripe listen forwards events to your laptop
Stripe can’t reach localhost, the name your computer uses for itself. stripe listen opens a connection to Stripe, receives your test account’s events, and sends each one to the local address you give it, signed with a secret it prints when it starts.
Terminal
1stripelisten--forward-tolocalhost:3000/webhooks/stripe2# > Ready! Your webhook signing secret is whsec_...
Keep it running in its own terminal window while you work.
QuizInteractive · disabled in the PDF
Locally, every forwarded event fails with “Bad signature”. What’s the likely cause?
Locally, every forwarded event fails with “Bad signature”. What’s the likely cause?
trigger creates real test objects in your account (a product, a customer, a payment) so the event really happens, and listen forwards it. The objects are made up, so their metadata won’t match your orders: good for checking your endpoint receives and verifies events, less for testing a full order.
Tip.stripe logs tail streams the API requests your keys make, as Stripe sees them. Run it beside your server to watch a checkout from Stripe’s side.
Stripe81 / 171
Module 10 · The Stripe CLI and testing
Practise: a whole purchase on your laptop
Put in orderInteractive · disabled in the PDF
Put a local test purchase in order.
1.Pay on Stripe’s test page with 4242 4242 4242 4242
2.See checkout.session.completed arrive and the order marked paid
3.Add a loaf to the cart and press Checkout
4.Put the printed whsec_... in .env and restart the server
5.Start your server and stripe listen --forward-to its webhook route
Try itInteractive · disabled in the PDF
Stripe82 / 171
Module 10 · The Stripe CLI and testing
Automated tests can sign their own events
An automated test is code that runs your code and checks the result, so you can rerun every check in seconds. To test a webhook route without Stripe, build an event yourself and sign it with a test secret using generateTestHeaderString.
Every event has a page in the Dashboard’s developer section: what it contained, which endpoints it was sent to, each attempt, and what your server answered. A failed delivery can be resent from there once you’ve fixed the bug. Events are kept for 30 days.
QuizInteractive · disabled in the PDF
A test order stays “pending” though the payment succeeded. Where do you look first?
A test order stays “pending” though the payment succeeded. Where do you look first?
Stripe85 / 171
Module 10 · The Stripe CLI and testing
Recap
stripe login links the CLI to your test account.
stripe listen --forward-to sends events to your laptop, with its own whsec_....
stripe trigger makes events happen with made-up objects; stripe logs tail shows requests.
Unit tests can sign their own payloads with generateTestHeaderString.
Test forged, duplicate and unknown events, not just the happy path.
The Dashboard’s event pages show every delivery and let you resend.
FlashcardsInteractive · disabled in the PDF
stripe listen
Forwards your test events to a local address, signed with a secret it prints.
stripe trigger <event>
Creates test objects so that event really happens.
stripe logs tail
Streams your API requests as Stripe receives them.
The name a computer uses for itself; Stripe can’t reach it directly.
Module 11: Retries, duplicates and idempotent handlers
Stripe86 / 171
Module 11 · Retries, duplicates and idempotent handlers
Events arrive at least once, and in any order
Stripe promises to deliver each event at least once, not exactly once. If your server’s 200 gets lost on the way back, Stripe retries, and your handler sees the same event twice. Retries also mean events can arrive late and out of order: a payment_intent.succeeded may land before or after the checkout.session.completed from the same purchase.
The same event, more than once.
Events from one purchase, in any order.
An event hours after it happened, after your server was down.
Idempotent
Safe to repeat: doing it twice has the same effect as doing it once. Pressing a lift’s call button again doesn’t call a second lift.
Stripe87 / 171
Module 11 · Retries, duplicates and idempotent handlers
Let the database decide whether this is the first time
WalkthroughInteractive · disabled in the PDF
TypeScript
1asyncfunctionmarkPaid(orderId:string){2const result =await db.query(3`UPDATE orders SET status = 'paid', paid_at = now()4 WHERE id = $1 AND status = 'pending'`,5[orderId]6)7if(result.rowCount ===0)return89await kitchen.addToBakeList(orderId)10await email.sendReceipt(orderId)11}
Lines 2-6: One SQL statement (the language of databases) changes the order to paid, but only if it is still pending. The database runs it as one step, so two deliveries can’t both pass.
Lines 7:rowCount is how many rows changed. Zero means the order was already paid (a duplicate) or doesn’t exist: nothing more to do.
Lines 9-10: The side effects run only for the delivery that actually changed the order, so the kitchen bakes one loaf and the buyer gets one email.
Stripe88 / 171
Module 11 · Retries, duplicates and idempotent handlers
Spot the race
Find the bugInteractive · disabled in the PDF
Now and then a buyer gets two receipts for one order. Which line opens the gap?
TypeScript
Stripe89 / 171
Module 11 · Retries, duplicates and idempotent handlers
Try it: a handler that is safe to repeat
Here a Map stands in for the orders table and an array for the emails sent. The events have Stripe’s shape, trimmed to what the handler reads. In markPaid, the check and the change run one after the other with no await between them, so nothing can slip in; in a real database, that is the single conditional UPDATE from two slides back.
Code challengeInteractive · disabled in the PDF
Make markPaid idempotent: only an order that is pending becomes paid and gets a receipt. Then add a case for checkout.session.async_payment_succeeded that marks its order paid too.
TypeScript
1typeSession={ client_reference_id:string; payment_status:string}2typeEvent={ id:string; type:string; data:{ object:Session}}34const orders =newMap<string,'pending'|'paid'>()5const receipts:string[]=[]67functionmarkPaid(orderId:string){8// TODO: only a pending order becomes paid and gets a receipt9 orders.set(orderId,'paid')10 receipts.push(orderId)11}1213functionhandle(event:Event){14const session = event.data.object15switch(event.type){16case'checkout.session.completed':17if(session.payment_status ==='paid'){18markPaid(session.client_reference_id)19}20break21// TODO: a delayed payment that cleared22}23}
Stripe90 / 171
Module 11 · Retries, duplicates and idempotent handlers
Or remember which events you’ve handled
A second approach is a table of handled event ids, with the id as its primary key (a column the database won’t allow twice). Insert the event’s id first; if the insert fails because it already exists, you’ve seen this event and can stop.
Idempotent by state
Each change says what it expects (“only if pending”).
Works even when different events mean the same thing.
Needs thought for each handler.
A log of event ids
One rule for every handler.
Two different events for the same purchase both pass.
Do the insert and the change in one transaction, or a crash between them loses work.
Expert note. Strong handlers often use both: the event log stops exact repeats cheaply, and state checks stop different events (say checkout.session.completed and async_payment_succeeded) from fulfilling twice.
Stripe91 / 171
Module 11 · Retries, duplicates and idempotent handlers
Record first, answer fast, do slow work later
Stripe waits only a short time for your answer before counting it as failed and retrying. So save what matters before answering (if saving fails, answer 500 and Stripe brings the event back), but hand slow jobs, like generating a PDF invoice or calling a delivery service, to a queue: a list of jobs that another process works through.
Put in orderInteractive · disabled in the PDF
Put the webhook handler’s steps in order.
1.Check the event is about one of your orders
2.Read the raw body and the Stripe-Signature header
3.Answer 200
4.Update the order in one idempotent statement
5.Verify with constructEvent; answer 400 if it fails
6.Queue the slow work, keyed by the order id
Stripe92 / 171
Module 11 · Retries, duplicates and idempotent handlers
Delayed payments: two events, one order
With a delayed method, checkout.session.completed arrives with payment_status: 'unpaid', and later checkout.session.async_payment_succeeded (or _failed) arrives with the outcome. With a card, completed arrives already paid and no async event follows. One markPaid called from both places handles every case.
QuizInteractive · disabled in the PDF
A checkout.session.completed event arrives with payment_status: 'unpaid'. What should the handler do?
A checkout.session.completed event arrives with payment_status: 'unpaid'. What should the handler do?
Stripe93 / 171
Module 11 · Retries, duplicates and idempotent handlers
One fulfilment function, called from two places
TypeScript
1asyncfunctionfulfil(sessionId:string){2const session =await stripe.checkout.sessions.retrieve(sessionId)3if(session.payment_status !=='paid')return4const orderId = session.client_reference_id5if(!orderId)return6awaitmarkPaid(orderId)7}89// in the webhook: await fulfil(event.data.object.id)10// on /thanks: await fulfil(sessionId)
Whichever runs first does the work; the other finds it done.
QuizInteractive · disabled in the PDF
Why does fulfil retrieve the session instead of trusting what it was given?
Why does fulfil retrieve the session instead of trusting what it was given?
Stripe94 / 171
Module 11 · Retries, duplicates and idempotent handlers
Recap
Events arrive at least once, possibly late, in any order.
Make handlers idempotent: change state with one conditional statement and act only if it changed.
“Check, then change” in two steps is a race; let the database decide.
A log of handled event ids stops exact repeats.
Save before answering; queue slow work; answer quickly.
Handle delayed payments; one fulfil can serve the webhook and the success page.
FlashcardsInteractive · disabled in the PDF
At-least-once delivery
Every event arrives one or more times; duplicates are normal.
Idempotent
Safe to repeat: twice has the same effect as once.
Race condition
A bug where the outcome depends on which of two simultaneous things goes first.
Primary key
A column whose value the database won’t allow twice, like an event id.
Queue
A list of jobs worked through by another process, so the webhook can answer fast.
When to answer 500?
When saving failed and trying again could help.
Module 12: Payment Intents and Elements
Stripe95 / 171
Module 12 · Payment Intents and Elements
A Payment Intent follows one payment to the end
Behind every payment is a Payment Intent (pi_...): Stripe’s record of trying to collect one amount, from “we’d like $19.00” through any check with the bank to “paid”. Each actual attempt to move money is a Charge (ch_...); a Payment Intent may have several attempts if a first card is declined. In payment mode, Checkout creates the Payment Intent for you.
Checkout Sessioncs_...
Payment Intentpi_..., one amount
Chargedeclined
Chargesucceeded
Refundre_...
Stripe96 / 171
Module 12 · Payment Intents and Elements
A Payment Intent moves through a few statuses
status
Means
requires_payment_method
Waiting for a card or other method (or the last one failed)
requires_confirmation
A method is attached; waiting to be confirmed
requires_action
The buyer must do something, such as confirm in their banking app
processing
Stripe and the bank are working on it: instant for cards, days for bank debits
requires_capture
Authorised and held; waiting for you to capture
succeeded
Paid
canceled
Abandoned; no money will move
QuizInteractive · disabled in the PDF
A buyer’s card is declined. What status is the Payment Intent in afterwards?
A buyer’s card is declined. What status is the Payment Intent in afterwards?
Stripe97 / 171
Module 12 · Payment Intents and Elements
Your own form: the server makes the Payment Intent
With Elements, Stripe’s card fields sit inside your page instead of on Stripe’s. Your server creates a Payment Intent for the amount and sends its client secret to the browser, which uses it to finish that one payment.
Lines 1-3: Load Stripe.js in the browser with your publishable key, the public one.
Lines 4-8: Ask your server for a Payment Intent and get its client secret.
Lines 10-11: Create the Payment Element, one block that shows cards, wallets and other methods, and place it in an empty <div id="payment-element">.
Lines 13-19:confirmPayment sends the details straight to Stripe, handles any bank check, then sends the buyer to return_url. If something fails at once, error says why.
Stripe99 / 171
Module 12 · Payment Intents and Elements
Guard the client secret
Whoever holds a client secret can confirm that payment and read its status. Send it only to the buyer making the payment, over HTTPS. Don’t log it, don’t put it in a URL you’ll share, and don’t store it longer than the payment needs.
QuizInteractive · disabled in the PDF
confirmPayment reported no error and the buyer reached /thanks. Is the order paid?
confirmPayment reported no error and the buyer reached /thanks. Is the order paid?
Stripe100 / 171
Module 12 · Payment Intents and Elements
Some banks ask the buyer to prove it’s them
In Europe and elsewhere, rules called Strong Customer Authentication (SCA) make many online payments ask for a second check, usually through 3D Secure: a code or a tap in the buyer’s banking app. The Payment Intent goes to requires_action, Stripe.js shows the check, and the payment continues. Checkout and the Payment Element handle this for you.
QuizInteractive · disabled in the PDF
How do you see the 3D Secure step in test mode?
How do you see the 3D Secure step in test mode?
Stripe101 / 171
Module 12 · Payment Intents and Elements
Authorise now, capture later
Module 1 split a payment into authorisation and capture. By default Stripe does both at once. For a custom cake that the bakery confirms the next morning, it can authorise now and capture once it accepts the order: with capture_method: 'manual', the Payment Intent waits in requires_capture. For most cards, a hold not captured within about seven days is released.
Fill the blanksInteractive · disabled in the PDF
Hold the cake’s price now, and take it once the order is accepted.
TypeScript
1const intent =await stripe.paymentIntents.create({2amount:4500,3currency:'usd',4capture_method:''5})6// later, when the bakery accepts the order:7await stripe.paymentIntents.(intent.id)
Stripe102 / 171
Module 12 · Payment Intents and Elements
Checkout or Elements?
Checkout
Least code; Stripe builds and updates the page.
Subscriptions, discounts, tax and addresses built in.
Fulfil from checkout.session.completed.
Payment Intents with Elements
The form is part of your page and your design.
You build the order summary, errors and states.
Fulfil from payment_intent.succeeded.
Tip. Start with Checkout. Move to Elements when there is a real reason, such as a checkout that must sit inside a single-page flow. Checkout can also be embedded in your page (its ui_mode setting), a middle way.
Stripe103 / 171
Module 12 · Payment Intents and Elements
Recap
A Payment Intent tracks one payment; each attempt to move money is a Charge.
Statuses run from requires_payment_method to succeeded or canceled.
With Elements, the server creates the intent and sends the browser its client secret.
confirmPayment finishes the payment in the browser, including 3D Secure.
Never trust the browser’s word; fulfil from payment_intent.succeeded.
capture_method: 'manual' holds now and captures later.
FlashcardsInteractive · disabled in the PDF
Payment Intent
Stripe’s record of collecting one amount (pi_...).
Charge
One attempt to move money (ch_...); refunds and disputes hang off it.
requires_action
The buyer must act, such as a 3D Secure check.
Client secret
Lets the browser complete one Payment Intent; share only with that buyer.
SCA and 3D Secure
Rules and a protocol for asking the buyer to prove it’s them.
requires_capture
Authorised and held, waiting for paymentIntents.capture.
Module 13: Customers and subscriptions
Stripe104 / 171
Module 13 · Customers and subscriptions
A Customer remembers a buyer across payments
A Customer (cus_...) holds a buyer’s email, name, saved payment methods, invoices and subscriptions. In one-off payment mode, Checkout treats the buyer as a guest unless you ask for a customer with customer_creation: 'always', or pass an existing one with customer: 'cus_...'. In subscription mode, there is always a customer: someone has to be billed next month.
QuizInteractive · disabled in the PDF
Which bakery buyers need a Stripe Customer?
Which bakery buyers need a Stripe Customer?
Stripe105 / 171
Module 13 · Customers and subscriptions
One Stripe customer per person in your system
If your site has accounts, store each account’s Stripe customer id the first time you create one, and pass it to every later checkout. Otherwise each checkout makes a new customer, and one person ends up as five, each with part of their history.
Find the bugInteractive · disabled in the PDF
A regular buyer has twelve customers in Stripe. Which line is to blame?
TypeScript
Stripe106 / 171
Module 13 · Customers and subscriptions
A subscription bills a customer every period
A Subscription (sub_...) says “bill this customer this recurring price, every period, until told to stop”. Each billing period (a month, for the bread club), Stripe makes an invoice, a bill listing what’s owed, and tries to pay it with the customer’s saved method.
Subscription
An agreement to bill a customer a recurring price each period.
Billing period
The stretch of time one payment covers, such as a month.
Invoice
A bill: what’s owed, for what, and whether it’s paid.
The first payment never succeeded (after about 23 hours)
No
trialing
In a free trial (next module)
Yes
active
Paid up
Yes
past_due
A renewal failed; Stripe is retrying
Your choice, for a grace period
unpaid
Retries ran out; kept open but not billed
No
paused
Paused by you, or a trial ended with no payment method
No
canceled
Over. It can’t be restarted
No
QuizInteractive · disabled in the PDF
A subscriber’s card expired and this month’s renewal failed. What status is the subscription in while Stripe retries?
A subscriber’s card expired and this month’s renewal failed. What status is the subscription in while Stripe retries?
Stripe109 / 171
Module 13 · Customers and subscriptions
Try it: decide access from the status
Your app stores each subscription’s status from its events, and one function decides what that status means for the customer. Keeping the decision in one place means one place to change when the rules do.
Code challengeInteractive · disabled in the PDF
Write hasAccess(status, daysPastDue). active and trialing get access. past_due gets access only during a 7-day grace period (daysPastDue of 7 or less). Every other status gets none.
TypeScript
1typeStatus='incomplete'|'incomplete_expired'|'trialing'2|'active'|'past_due'|'unpaid'|'paused'|'canceled'34functionhasAccess(status:Status, daysPastDue =0):boolean{5// TODO: decide from the status (this first guess is too generous)6return status !=='canceled'7}
Which event best tells the bakery that February’s bread is paid for?
Which event best tells the bakery that February’s bread is paid for?
Stripe111 / 171
Module 13 · Customers and subscriptions
Where “paid until” lives
To know how long a subscriber is covered, read the end of the current period. Since Stripe’s 2025 API versions, period dates live on each subscription item (each price in the subscription), not on the subscription itself. With one price, read the first item.
Stripe’s times are in seconds; JavaScript’s Date wants milliseconds, hence * 1000.
Older guides. Many tutorials read subscription.current_period_end. That field was removed from the subscription in the 2025-03-31 API version; on the version this course teaches it’s on items.data[].current_period_end.
Stripe112 / 171
Module 13 · Customers and subscriptions
Recap
A Customer holds a buyer’s details, saved methods, invoices and subscriptions.
Keep one Stripe customer per person; store its id and reuse it.
A Subscription bills a recurring price each period through invoices.
Sell one with Checkout in subscription mode; put metadata in subscription_data.
Listen for customer.subscription.* and invoice.paid / invoice.payment_failed.
FlashcardsInteractive · disabled in the PDF
customer_creation: 'always'
Makes Checkout create a Customer in payment mode.
past_due
A renewal failed and Stripe is retrying.
incomplete
The first payment of a new subscription hasn’t succeeded.
The renewal event
invoice.paid, every period.
Where is the period end?
On each subscription item: items.data[].current_period_end.
Can a canceled subscription restart?
No. Make a new subscription.
Module 14: Trials, plan changes and the customer portal
Stripe113 / 171
Module 14 · Trials, plan changes and the customer portal
A free trial delays the first payment
The bakery wants new members to try the bread club free for 14 days. A trial starts the subscription in trialing status; the first invoice comes when the trial ends. Checkout still collects a card by default, so the first payment can happen without the member doing anything.
A member signs up for the trial. What is the completed session’s payment_status?
A member signs up for the trial. What is the completed session’s payment_status?
Stripe114 / 171
Module 14 · Trials, plan changes and the customer portal
Plan for the end of the trial
Three days before a trial ends, Stripe sends customer.subscription.trial_will_end: a good moment to remind the member. If you let people start a trial without a card (payment_method_collection: 'if_required' on the session), choose what happens when it ends with none on file.
Lines 1-2: A subscription holds items, one per price. Find the item to change.
Lines 4-5: Replace that item’s price by giving the item’s id with the new price. Without the id, Stripe would add a second item, and the member would pay for both.
Lines 6: Decide how to charge for the part of the month already gone (next slides).
Stripe116 / 171
Module 14 · Trials, plan changes and the customer portal
Proration: paying fairly for part of a period
Proration adjusts the bill when a plan changes in the middle of a period. The member gets credit for the unused time on the old price and pays for the remaining time on the new one. Stripe works it out to the second.
Halfway through the month
Amount
Unused half of the $30.00 plan (credit)
−$15.00
Remaining half of the $45.00 plan
+$22.50
Added to the next invoice
+$7.50
QuizInteractive · disabled in the PDF
A member on the $45 plan moves to the $30 plan exactly halfway through the month. With prorations, what happens?
A member on the $45 plan moves to the $30 plan exactly halfway through the month. With prorations, what happens?
Stripe117 / 171
Module 14 · Trials, plan changes and the customer portal
Choose when prorations are billed
proration_behavior
Effect
create_prorations
The default: prorations are added to the next regular invoice
always_invoice
Prorations are invoiced and charged straight away
none
No adjustment: the new price simply applies from the next period
Show the cost before the change.stripe.invoices.createPreview({ subscription: sub.id, subscription_details: { items: [{ id: item.id, price: newPriceId }] } }) returns the invoice the change would produce, so the member sees the amount before agreeing.
Stripe118 / 171
Module 14 · Trials, plan changes and the customer portal
At period end, the subscription stays active until the paid month runs out, and can be un-cancelled by setting cancel_at_period_end back to false. Then customer.subscription.deleted arrives. Cancelling now ends it at once; it can’t be restarted.
QuizInteractive · disabled in the PDF
A member cancels on the 10th of a month already paid. When should their deliveries stop?
A member cancels on the 10th of a month already paid. When should their deliveries stop?
Stripe119 / 171
Module 14 · Trials, plan changes and the customer portal
The customer portal lets members manage themselves
Stripe’s customer portal is a hosted page where a customer can update their card, see and download invoices, switch plans and cancel, with the options you allow in the Dashboard’s portal settings. You build none of it: you create a portal session and redirect.
Find the bugInteractive · disabled in the PDF
Anyone can open any member’s billing page. Which line allows it?
TypeScript
Note. Whatever members do in the portal arrives as the usual events (customer.subscription.updated, deleted, invoice.paid), so a webhook that already handles them needs no portal-specific code.
Stripe120 / 171
Module 14 · Trials, plan changes and the customer portal
Recap
trial_period_days starts a subscription trialing; the session is no_payment_required.
trial_will_end comes three days before; decide what a trial with no card does at the end.
Change a plan by updating the subscription item’s price, giving the item’s id.
Proration credits unused time and charges the rest; proration_behavior decides when.
Preview a change with invoices.createPreview.
Cancel at period end or now; the customer portal handles cards, invoices, plans and cancelling.
FlashcardsInteractive · disabled in the PDF
Proration
Credit for unused time on the old price plus a charge for the rest on the new.
always_invoice
Bill prorations now, instead of on the next invoice.
cancel_at_period_end: true
Ends the subscription when the paid period runs out.
When does trial_will_end arrive?
Three days before the trial ends.
Customer portal
A Stripe-hosted page where customers manage cards, invoices and plans.
Previewing a change
stripe.invoices.createPreview with subscription_details.
Module 15: Invoices and failed payments
Stripe121 / 171
Module 15 · Invoices and failed payments
An invoice goes from draft to paid
A subscription makes a draft invoice at each renewal and, about an hour later, finalises it: it becomes open, gets a number, and can no longer be edited. Stripe then tries to pay it. A finalised invoice is never deleted; a mistaken one is voided, and one you give up on is marked uncollectible.
Stripe122 / 171
Module 15 · Invoices and failed payments
Check: invoice statuses
QuizInteractive · disabled in the PDF
An open invoice went out with the wrong amount. What do you do?
An open invoice went out with the wrong amount. What do you do?
Stripe123 / 171
Module 15 · Invoices and failed payments
What an invoice tells you
TypeScript
1const invoice = event.data.object // from invoice.paid2const subscriptionId =3 invoice.parent?.subscription_details?.subscription45console.log(6 invoice.number,// "A1B2C3-0004"7 invoice.amount_due,// 3000, in cents8 invoice.amount_paid,// 30009 invoice.hosted_invoice_url,// a page the customer can open10 invoice.invoice_pdf // a PDF to download11)
Changed in 2025. Older code reads invoice.subscription. Since the 2025-03-31 API version, an invoice says what made it in parent: for a subscription, parent.subscription_details.subscription. The library’s types for this version only have the new shape.
Fill the blanksInteractive · disabled in the PDF
Find which subscription an invoice belongs to.
TypeScript
1const id = invoice.?.?.subscription
Stripe124 / 171
Module 15 · Invoices and failed payments
Renewals fail, and most can be saved
A card that worked in January may fail in June: it expired, was replaced after a loss, hit its limit, or the bank was cautious. This involuntary churn, members lost without meaning to leave, is often a big share of a subscription business’s losses. Stripe has tools to recover it, set in the Dashboard’s billing settings.
Tool
Does
Smart Retries
Retries a failed payment at times it’s likely to succeed, over a period you choose
Failed payment emails
Tells the customer and links to a page to update their card
Card updates from networks
Many issuers send new card details when a card is replaced, so saved cards keep working
What to do when retries end
Cancel the subscription, mark it unpaid, or leave it past_due
Churn
Subscribers leaving. Involuntary churn is leaving because a payment failed.
Dunning
The process of chasing failed payments: retries and reminders.
Lines 1-7: When an attempt fails, tell the member, with a link to the hosted invoice page where they can pay with another card. Skip this if Stripe’s own emails are switched on, or they’ll get two.
Lines 8-12: The subscription’s status moves to past_due, and back to active if a retry works. Mirror the status; let your rules decide what each status allows.
QuizInteractive · disabled in the PDF
The bakery gives past_due members a week of bread while Stripe retries. Where does that rule belong?
The bakery gives past_due members a week of bread while Stripe retries. Where does that rule belong?
Stripe126 / 171
Module 15 · Invoices and failed payments
When retries run out
QuizInteractive · disabled in the PDF
The Dashboard is set to cancel subscriptions when all retries fail. Which event marks that moment?
The Dashboard is set to cancel subscriptions when all retries fail. Which event marks that moment?
Tip. Each failed attempt also says when the next one is planned in next_payment_attempt (a Unix timestamp, or null when no more are planned). Use it to tell members how long they have.
Stripe127 / 171
Module 15 · Invoices and failed payments
Bill a business by invoice
A café orders 40 loaves a week and pays by bank transfer within 14 days. Instead of charging a card, send an invoice: collection_method: 'send_invoice' emails it with a link to pay, and the invoice waits open until paid.
Invoices go draft → open → paid, or void or uncollectible; final ones can’t be edited.
Find an invoice’s subscription at parent.subscription_details.subscription.
Failed renewals are common; Smart Retries, emails and card updates recover many.
Mirror the subscription status and keep grace periods in your own rules.
customer.subscription.deleted marks the end when retries run out (if set to cancel).
send_invoice bills a business with an emailed invoice and a due date.
FlashcardsInteractive · disabled in the PDF
Finalising an invoice
Turning a draft into an open, numbered, uneditable invoice.
Voiding
Cancelling a finalised invoice while keeping its record.
Involuntary churn
Subscribers lost because a payment failed, not by choice.
Smart Retries
Stripe retrying failed payments at times likely to succeed.
hosted_invoice_url
A Stripe page where the customer can view and pay the invoice.
next_payment_attempt
When Stripe will retry, or null if it won’t.
Module 16: Refunds, disputes and taxes
Stripe129 / 171
Module 16 · Refunds, disputes and taxes
A refund gives back all or part of a payment
A refund (re_...) returns money from a successful payment to the buyer’s card or account. Refund the whole payment, or pass an amount for part of it, such as one squashed loaf out of three. Refunds can be made in the Dashboard or with the API, and usually take several business days to appear on the buyer’s statement.
TypeScript
1// the whole payment2await stripe.refunds.create({payment_intent: paymentIntentId })34// one loaf of three, with a reason5await stripe.refunds.create({6payment_intent: paymentIntentId,7amount:850,8reason:'requested_by_customer'9})
reason is duplicate, fraudulent or requested_by_customer.
Note. In most countries Stripe doesn’t give back its processing fee when you refund, so a refunded order still costs you the fee. Check the pricing page for yours.
Stripe130 / 171
Module 16 · Refunds, disputes and taxes
Refunds arrive as events, whoever made them
Support staff may refund from the Dashboard, so don’t only update your order where your code calls refunds.create. Listen for charge.refunded, which carries the Charge with amount_refunded and refunded (true once it’s fully refunded). The Charge’s payment_intent leads back to your order through its metadata.
Find the bugInteractive · disabled in the PDF
A customer refunded for one squashed loaf has their whole order cancelled. Which line is wrong?
TypeScript
Stripe131 / 171
Module 16 · Refunds, disputes and taxes
A dispute is the buyer’s bank taking money back
A cardholder can ask their bank to reverse a payment: they don’t recognise it, the bread never came, or it was stolen card use. This is a dispute, also called a chargeback. The money and a dispute fee are taken from your Stripe balance at once, and you can respond with evidence before a deadline.
Dispute (chargeback)
A cardholder’s bank reversing a payment on their request.
Evidence
What you send to show the payment was fair: receipts, delivery proof, messages.
Stripe132 / 171
Module 16 · Refunds, disputes and taxes
Respond with evidence, or accept it
Respond in the Dashboard, or with stripe.disputes.update(id, { evidence: { ... }, submit: true }). If you can’t win (the buyer is right), accept it with stripe.disputes.close(id). charge.dispute.closed tells you how it ended, in status: won or lost.
QuizInteractive · disabled in the PDF
Which steps make disputes less likely? Pick all that apply.
Which steps make disputes less likely? Pick all that apply.
Watch out. Don’t refund a payment that is already disputed: the bank has taken the money back, and a refund would pay the buyer twice. Respond to the dispute or accept it instead.
Stripe133 / 171
Module 16 · Refunds, disputes and taxes
Taxes, in general terms
Many places charge a tax on sales: sales tax in the US, VAT (value-added tax) in the UK and EU, GST in Australia and elsewhere. The seller usually collects it from the buyer and pays it to the government. Whether you must depends on where you and your buyers are, what you sell, and how much, so it’s a question for an accountant, not code. Your code’s job is to charge what the rules require.
Tax-exclusive
Tax is added at checkout: a $8.50 loaf with 10% tax costs $9.35. Common in the US.
Tax-inclusive
Tax is inside the price: an €8.50 loaf already includes its VAT. Usual for consumers in Europe.
VAT
Value-added tax: a tax on sales, collected by the seller, common in Europe.
Tax registration
Signing up with a tax authority to collect tax there. Required before collecting.
Stripe134 / 171
Module 16 · Refunds, disputes and taxes
Stripe Tax works out the amount at checkout
Stripe Tax is an optional, paid feature that calculates the right tax from the buyer’s address, what you sell (its tax code) and where you’re registered. You add your registrations in the Dashboard, give each price a tax_behavior, and turn it on per checkout.
A price of €12.00 is inclusive of 20% VAT. How much of it is VAT?
A price of €12.00 is inclusive of 20% VAT. How much of it is VAT?
Stripe135 / 171
Module 16 · Refunds, disputes and taxes
Read what was charged
A completed session reports the tax in total_details.amount_tax, beside amount_subtotal and amount_total. Store all three with the order: your accounts need the tax apart from the sale. Stripe Tax also has reports in the Dashboard to help with filing.
Refund all or part with refunds.create({ payment_intent, amount }); the fee usually isn’t returned.
Act on charge.refunded, and check refunded to tell full from partial.
A dispute takes the money and a fee; respond before due_by, or accept with disputes.close.
Prevent disputes with a clear descriptor, receipts, easy refunds and reminders.
Taxes depend on where you and buyers are; prices are tax-inclusive or exclusive.
Stripe Tax calculates at checkout with automatic_tax: { enabled: true }.
FlashcardsInteractive · disabled in the PDF
Partial refund
refunds.create with an amount less than the payment.
Chargeback
Another name for a dispute: the buyer’s bank reversing a payment.
Accepting a dispute
stripe.disputes.close(id).
Statement descriptor
The name buyers see on their card statement.
tax_behavior: 'inclusive'
The price already contains the tax.
Where is the tax on a session?
total_details.amount_tax.
Module 17: Errors, retries and idempotency keys
Stripe137 / 171
Module 17 · Errors, retries and idempotency keys
A request can fail in three different places
The third case is the dangerous one. The refund happened, but your server saw a timeout. Retrying blindly makes a second refund. Everything in this module is about knowing which case you’re in, and making retries harmless when you can’t know.
Stripe138 / 171
Module 17 · Errors, retries and idempotency keys
Every Stripe error has a type and a status
Stripe.errors.…
HTTP status
Retry?
StripeCardError
402
No: ask the buyer for another card
StripeInvalidRequestError
400, 404
No: fix the request
StripeAuthenticationError
401
No: fix the key
StripePermissionError
403
No: a restricted key lacks a permission
StripeIdempotencyError
400
No: a key was reused with different parameters
StripeRateLimitError
429
Yes, after waiting
StripeConnectionError
none
Yes, with an idempotency key
StripeAPIError
500 and up
Yes, with an idempotency key
Each error also has a message, and often a code (like resource_missing), a param, and a requestId that Stripe’s support and your request logs can look up.
Stripe139 / 171
Module 17 · Errors, retries and idempotency keys
Catch the errors you mean
Find the bugInteractive · disabled in the PDF
During a Stripe outage, buyers are told “That order doesn’t exist”. Which line causes it?
TypeScript
Tip.instanceof checks which class an error was made from. All Stripe errors extend Stripe.errors.StripeError, so you can catch broadly in one place (to log) and narrowly in another (to recover).
Stripe140 / 171
Module 17 · Errors, retries and idempotency keys
Card errors are for the buyer, gently
With your own payment form, a decline arrives as a StripeCardError (or in the Payment Intent’s last_payment_error) with a code like card_declined and sometimes a decline_code like insufficient_funds. Show the buyer a short, kind message. Keep the codes for your logs.
QuizInteractive · disabled in the PDF
A payment is declined with decline_code: 'fraudulent'. What should the buyer see?
A payment is declined with decline_code: 'fraudulent'. What should the buyer see?
Stripe141 / 171
Module 17 · Errors, retries and idempotency keys
An idempotency key makes a retry safe
An idempotency key is a unique string you send with a request that changes something. Stripe remembers the result for that key, and any repeat with the same key gets the same result back, without doing the work again. Keys are kept for at least 24 hours.
The key goes in the second argument, the request options.
Idempotency key
A string that makes Stripe treat repeats of a request as the same request, returning the first result.
Stripe142 / 171
Module 17 · Errors, retries and idempotency keys
Derive the key from what the request is for
Find the bugInteractive · disabled in the PDF
A retried request still created two customers. Which line makes the key useless?
TypeScript
QuizInteractive · disabled in the PDF
The same idempotency key is sent again, but with a different amount. What does Stripe do?
The same idempotency key is sent again, but with a different amount. What does Stripe do?
Stripe143 / 171
Module 17 · Errors, retries and idempotency keys
Try it: retry with one key
Here your own code repeats a job, as a queue would. The Stripe call is passed in as create, with the same shape as stripe.refunds.create(params, options), so the tests can hand in a mock that fails when they want it to.
Code challengeInteractive · disabled in the PDF
Fix refundComplaint. Its idempotency key must be refund- followed by the complaint’s id, the same on every attempt. If create throws, try again, up to 3 attempts in all; after the third failure, let the error through.
TypeScript
1typeRefund={ id:string; amount:number}2typeCreateRefund=(3 params:{ payment_intent:string; amount:number},4 options:{ idempotencyKey:string}5)=>Promise<Refund>6typeComplaint={ id:string; paymentIntent:string; amount:number}78asyncfunctionrefundComplaint(9 create:CreateRefund, complaint:Complaint10):Promise<Refund>{11const params ={12payment_intent: complaint.paymentIntent,13amount: complaint.amount14}15// TODO: a key from the complaint, and up to 3 attempts16returncreate(params,{idempotencyKey:`refund-${Date.now()}`})17}
Stripe144 / 171
Module 17 · Errors, retries and idempotency keys
Let the library retry for you
TypeScript
1const stripe =newStripe(process.env.STRIPE_SECRET_KEY!,{2maxNetworkRetries:2,// retry connection and server errors3timeout:20_000// milliseconds to wait for an answer4})
The library already retries failures that may pass (connection errors, 409 conflicts, most 5xx responses) on its own, with growing pauses: maxNetworkRetries sets how many times, and 0 turns it off. It also gives every POST an idempotency key of its own, so its retries are safe. Your own key is still worth passing when your code may repeat the job, say a queue retrying a task tomorrow.
QuizInteractive · disabled in the PDF
A nightly job calls Stripe in a tight loop and starts getting 429s. What helps most?
A nightly job calls Stripe in a tight loop and starts getting 429s. What helps most?
Stripe145 / 171
Module 17 · Errors, retries and idempotency keys
Recap
A request can fail before Stripe, at Stripe, or after Stripe succeeded with the answer lost.
Error classes say whether to fix the request, ask the buyer, or retry.
Catch narrowly: a 404 is “not found”; an outage isn’t.
Show buyers a generic decline message; log the codes.
Idempotency keys make repeats return the first result; derive them from the job, never the time.
The library retries safely on its own (maxNetworkRetries); back off on 429.
FlashcardsInteractive · disabled in the PDF
Where does an idempotency key go?
In the request options: create(params, { idempotencyKey }).
How long are keys remembered?
At least 24 hours.
StripeIdempotencyError
A key was reused with different parameters.
Which errors are worth retrying?
Connection errors, 429, and 5xx, with an idempotency key.
requestId on an error
The id to find the request in Stripe’s logs.
Exponential backoff
Waiting longer after each failure before trying again.
Module 18: Syncing state and reconciling money
Stripe146 / 171
Module 18 · Syncing state and reconciling money
Your database mirrors Stripe; it doesn’t compete with it
For billing facts (is this subscription active, was this payment refunded) Stripe is the source of truth: the one place whose answer wins. Your database keeps a copy so pages load fast and your rules can run without calling Stripe each time. The job is to keep that copy right.
Your pages and rulesread only your database
Your databaseorders, members: a mirror
Webhooks, plus a regular checkkeep the mirror current
Stripethe source of truth for billing
Source of truth
The system whose answer is correct when two copies disagree.
Stripe147 / 171
Module 18 · Syncing state and reconciling money
Events don’t arrive in order
Stripe doesn’t guarantee the order of deliveries, and retries shuffle them further. A handler that blindly copies each event’s snapshot can let an old event overwrite a newer state.
Find the bugInteractive · disabled in the PDF
A member who cancelled is shown as “active” again the next morning. Which line lets an older event win?
TypeScript
Stripe148 / 171
Module 18 · Syncing state and reconciling money
Treat an event as a nudge, and fetch the truth
WalkthroughInteractive · disabled in the PDF
TypeScript
1asyncfunctionsyncSubscription(id:string){2const sub =await stripe.subscriptions.retrieve(id)3const item = sub.items.data[0]4await db.query(5`INSERT INTO members (subscription_id, user_id,6 status, paid_until)7 VALUES ($1, $2, $3, $4)8 ON CONFLICT (subscription_id) DO UPDATE9 SET status = $3, paid_until = $4`,10[sub.id, sub.metadata.userId, sub.status,11newDate(item.current_period_end *1000)]12)13}
Lines 1-2: Whatever the event said, ask Stripe for the subscription as it is now. Old events and new ones all lead to the same, current answer.
Lines 4-12: Write the whole state in one upsert: insert the row, or update it if it already exists. Running it twice, or in any order, gives the same result: idempotent by design.
Expert note. Fetching costs one API call per event, which is fine for most shops. At high volume, compare event.created with a stored “last applied” time instead, and fetch only when in doubt.
Stripe149 / 171
Module 18 · Syncing state and reconciling money
Catch up on events you missed
If your endpoint was down longer than Stripe’s retries, or a bug answered 200 without doing the work, events are lost to your system but not to Stripe. Stripe keeps events for 30 days, and you can list them.
Fill the blanksInteractive · disabled in the PDF
Replay every subscription change since an outage began.
TypeScript
1const events = stripe..list({2types:['customer.subscription.updated',3'customer.subscription.deleted'],4created:{: outageStart }5})6for(const event of events){7const sub = event.data.object asStripe.Subscription8awaitsyncSubscription(sub.id)9}
Stripe150 / 171
Module 18 · Syncing state and reconciling money
A regular check catches what events miss
Even with good handlers, mirrors drift: a manual database edit, a bug fixed last week. A reconciliation job runs on a schedule (say nightly), lists what Stripe holds, compares it with your copy, and fixes or reports the differences.
QuizInteractive · disabled in the PDF
What should the bakery’s nightly check compare?
What should the bakery’s nightly check compare?
Stripe151 / 171
Module 18 · Syncing state and reconciling money
Follow the money: balance, transactions, payouts
Money you take goes into your Stripe balance. Every movement in or out of it is a balance transaction with an amount, a fee and a net (amount minus fee). Regularly, Stripe sends the available balance to your bank in a payout (po_...).
Balance transaction type
Example
Net
charge / payment
A $30.00 bread club payment
about +$28.80 after fees
refund
One loaf refunded
−$8.50
adjustment
A dispute: money and fee taken
negative
payout
The transfer to your bank
negative: it leaves the balance
Balance transaction
One movement of money in your Stripe balance, with its fee and net.
Payout
A transfer from your Stripe balance to your bank account.
Reconciliation
Checking that two records of the same money agree, line by line.
Stripe152 / 171
Module 18 · Syncing state and reconciling money
Reconcile a payout line by line
TypeScript
1const lines = stripe.balanceTransactions.list({2payout: payoutId,3expand:['data.source']4})5let total =06forawait(const line of lines){7if(line.type ==='payout')continue8 total += line.net9await ledger.match(line.id, line.type, line.net, line.source)10}11console.log('payout covers', total)
Lists what one payout paid out. source is the charge, refund or dispute behind each line.
QuizInteractive · disabled in the PDF
This week’s orders total $1,000, but the payout is $940. Which could explain it? Pick all that apply.
This week’s orders total $1,000, but the payout is $940. Which could explain it? Pick all that apply.
Stripe153 / 171
Module 18 · Syncing state and reconciling money
Recap
Stripe is the source of truth for billing; your database is a mirror.
Events can arrive in any order: fetch current state, or ignore events older than the last applied.
Write whole states with upserts, so handlers are idempotent.
Replay missed events with events.list (30 days) and for await.
A scheduled reconciliation job catches drift.
Payouts are the net of balance transactions; list them by payout to reconcile.
FlashcardsInteractive · disabled in the PDF
Upsert
Insert a row, or update it if it already exists, in one statement.
Event ordering
Not guaranteed. Don’t let an older snapshot overwrite newer state.
balanceTransactions.list({ payout }), then match each line.
for await over a list
Walks every page of results automatically.
Module 19: Fraud, security and going live
Stripe154 / 171
Module 19 · Fraud, security and going live
Fraud costs the merchant
When someone pays with a stolen card, the real cardholder disputes the charge, and the merchant loses the money, the goods and a dispute fee. A common attack is card testing: a criminal with a list of stolen numbers uses a small shop’s checkout to find which still work, making hundreds of tiny payments.
Card testing
Trying many stolen card numbers with small payments to find working ones.
Radar
Stripe’s fraud system, which scores every card payment for risk.
Stripe155 / 171
Module 19 · Fraud, security and going live
Radar scores every payment
Radar compares each card payment with what it has seen across all of Stripe’s merchants, and records the result on the Charge in outcome: a risk_level (normal, elevated or highest) and, with some plans, a risk_score from 0 to 100. By default, Radar blocks payments it rates highest risk.
A $300 cake order comes through with risk_level: 'elevated'. What’s a sensible response?
A $300 cake order comes through with risk_level: 'elevated'. What’s a sensible response?
Stripe156 / 171
Module 19 · Fraud, security and going live
Rules and reviews tune Radar to your shop
With Radar for Fraud Teams (a paid add-on), you write rules in the Dashboard that block, allow, or send payments to review, a queue where a person decides. Rules read like Block if :risk_score: > 85 or Review if :card_country: != :ip_country:. A payment sent to review fires review.opened.
QuizInteractive · disabled in the PDF
Why not block every payment with a risk score above 30?
Why not block every payment with a risk score above 30?
Stripe157 / 171
Module 19 · Fraud, security and going live
Early fraud warnings: refund before the dispute
Card issuers sometimes report a payment as fraudulent before the cardholder disputes it. Stripe passes this on as radar.early_fraud_warning.created. A dispute usually follows, with its fee, so a common response is to refund at once and cancel any subscription on that card.
Fill the blanksInteractive · disabled in the PDF
Refund a payment flagged by an early fraud warning.
Limit the rate at which one visitor or address can create checkouts.
Ask for a login or a CAPTCHA (a check that a person is present) before payment on risky flows.
Watch for many small, failing payments: a sign of card testing in progress.
Keep 3D Secure on where it’s offered; for authenticated payments, fraud liability usually moves to the issuer.
QuizInteractive · disabled in the PDF
Overnight, the bakery sees 400 declined $1 payments from new visitors. What is most likely happening?
Overnight, the bakery sees 400 declined $1 payments from new visitors. What is most likely happening?
Stripe159 / 171
Module 19 · Fraud, security and going live
A security review of a payment flow
QuizInteractive · disabled in the PDF
Which belong in a security review of the bakery’s Stripe code? Pick all that apply.
Which belong in a security review of the bakery’s Stripe code? Pick all that apply.
Restricted keys. Make a restricted key (rk_...) for each server with just the access it needs: say Checkout Sessions write, Customers write, Subscriptions read. If it leaks, the damage is limited, and a missing permission shows up at once as a StripePermissionError.
Stripe160 / 171
Module 19 · Fraud, security and going live
Going live
To take real money, activate the account: give Stripe your business details and a bank account for payouts. Then remember that test and live share no objects: every product, price and webhook endpoint you made in test mode has to be made again in live mode.
Put in orderInteractive · disabled in the PDF
Put the launch steps in order.
1.Make one small real purchase, check it end to end, and refund it
2.Activate the account with business and bank details
3.Recreate products, prices (with the same lookup keys), tax settings and portal settings in live mode
4.Add the live webhook endpoint and store its new signing secret
5.Swap the server’s keys and webhook secret for live ones
In the first weeksInteractive · disabled in the PDF
Stripe161 / 171
Module 19 · Fraud, security and going live
Recap
Fraud ends in disputes the merchant pays for; card testing targets small checkouts.
Radar scores each payment in charge.outcome; it blocks the highest risk by default.
Rules and reviews tune Radar; weigh each rule against honest buyers turned away.
Refund on radar.early_fraud_warning.created to avoid a dispute.
Review: keys, webhook signatures, server-side prices, nothing secret in logs.
Going live means activating and setting everything up again in live mode.
FlashcardsInteractive · disabled in the PDF
Card testing
Many small payments with stolen numbers to find which work.
Where is Radar’s verdict?
On the Charge, in outcome.risk_level and outcome.risk_score.
Review (Radar)
A queue where a person decides whether to keep a flagged payment.
Early fraud warning
An issuer’s report of fraud, often before a dispute.
Activating an account
Giving Stripe business and bank details so live payments can start.
What carries over from test to live?
No objects: products, prices and webhook endpoints are made again in live mode.
Module 20: Mastery challenge
Stripe162 / 171
Module 20 · Mastery challenge
The brief: review the bakery’s payments
The bakery has grown. It sells weekend orders, a monthly and a yearly bread club with a 14-day trial, and invoices cafés. You’ve been asked to review its Stripe code before a busy season. Each slide is one finding to make. Take your time: most have a tempting wrong answer.
Stripe163 / 171
Module 20 · Mastery challenge
Challenge: the whole one-off flow
Put in orderInteractive · disabled in the PDF
Order the life of a weekend order, from button to bank.
1.The money becomes available and is paid out to the bakery’s bank
2.The buyer pays on Stripe’s page and the issuer authorises
3.The server prices the cart and creates an order marked pending
4.The server creates a Checkout Session with the order id and redirects with a 303
5.A signed checkout.session.completed arrives and the order is marked paid, once
Stripe164 / 171
Module 20 · Mastery challenge
Challenge: the handler that emails twice
Find the bugInteractive · disabled in the PDF
Under retries, some buyers get two receipts though only one order is paid. Which line is in the wrong place?
TypeScript
Stripe165 / 171
Module 20 · Mastery challenge
Challenge: nobody can join the trial
Predict the outputInteractive · disabled in the PDF
Since the 14-day trial launched, no new member gets a welcome email. The handler is below. Why?
The refund call times out with a StripeConnectionError. What should the job do?
The refund call times out with a StripeConnectionError. What should the job do?
Stripe168 / 171
Module 20 · Mastery challenge
Challenge: the member who came back from the dead
QuizInteractive · disabled in the PDF
After a cancellation, an old customer.subscription.updated (status active) arrives last. Which handlers stay correct? Pick all that apply.
After a cancellation, an old customer.subscription.updated (status active) arrives last. Which handlers stay correct? Pick all that apply.
Stripe169 / 171
Module 20 · Mastery challenge
Challenge: launch day
QuizInteractive · disabled in the PDF
On launch day, every checkout fails with “No such price”. Keys are live and correct. What was missed?
On launch day, every checkout fails with “No such price”. Keys are live and correct. What was missed?
QuizInteractive · disabled in the PDF
A week after launch, the payout is lower than the sum of paid orders. What do you do first?
A week after launch, the payout is lower than the sum of paid orders. What do you do first?
Stripe170 / 171
Module 20 · Mastery challenge
Challenge: one last route
Find the bugInteractive · disabled in the PDF
This success page has a security hole. Which line?
TypeScript
Stripe171 / 171
Module 20 · Mastery challenge
Recap: what an expert checks
Prices from the server; ids from configuration or lookup keys that exist in both modes.
Every webhook verified, idempotent, order-proof, with side effects after the state change.
Subscription access from subscription status, not a session’s payment status.
Idempotency keys derived from the job; retries with the same key after timeouts.
Pages show; webhooks grant.
Payouts reconciled from balance transactions; drift caught by a scheduled check.
FlashcardsInteractive · disabled in the PDF
Trial sign-up payment_status
no_payment_required.
Upgrading a subscription item
Pass the item’s id with the new price, or you add a second item.
After a timeout on a create
Retry with the same idempotency key.
Old events after new ones
Fetch current state, or skip events older than the last applied.
“No such price” on launch day
Test-mode ids in live configuration.
Pages versus webhooks
Pages show what Stripe says; webhooks change what people get.
Answer key
1. How a card payment works · Check: who decides? · QuizA customer’s card is declined at the bakery’s checkout. Who made that decision?C. The issuer, the customer’s own bankThe issuer holds the cardholder’s money, so it decides: enough funds, card not reported stolen, nothing suspicious. The network only carries the question and the answer between the banks. The bakery never sees why in detail, which is why a declined buyer is usually told to try another card or call their bank.
1. How a card payment works · Practise: the life of a payment · Put in orderPut the steps of a weekend loaf order in order.1. The buyer types their card details and presses Pay 2. The request travels through the acquirer and the network to the issuer 3. The issuer approves and holds the amount 4. The bakery captures the payment 5. The money settles and is paid out to the bakery’s bankNothing moves until the issuer approves. Capture comes after authorisation, never before, and the money reaches the bakery last: authorisation takes a second or two, settlement and payout take days.
1. How a card payment works · Payments cost a fee, and some fail · QuizA buyer pays €30.00 for a month of bread. What reaches the bakery’s bank account?B. A little less than €30.00, after feesThe processing fee is taken from each payment, so the payout is the amount minus fees. Later modules show where to read the exact fee for each payment, which matters when the bakery’s accounts have to add up.
2. Payment processors, and why you never touch card numbers · Check: who sees the card? · QuizThe bakery uses Stripe’s hosted payment page. Which of these ever receives the buyer’s full card number?C. Only Stripe (and the banks behind it)The buyer types the card on a page Stripe serves, so the number goes straight from their browser to Stripe. The bakery’s server learns that a payment happened and gets ids to refer to it, which keeps its PCI work to a short self-assessment form instead of audits.
2. Payment processors, and why you never touch card numbers · Practise: read an id · QuizA support email mentions sub_.... What should the bakery look for in its Stripe account?B. A subscriptionsub_ is a Subscription. Learning the prefixes pays off quickly: logs, emails and error messages are full of ids, and the prefix tells you where to look before you open anything. A customer is cus_, a price price_, a refund re_.
2. Payment processors, and why you never touch card numbers · Practise: read an id · QuizWhich of these is safe to store in the bakery’s own database?C. The Stripe customer id cus_...Stripe ids are references, not secrets about the card: storing cus_... lets you find the customer in Stripe later. Card numbers and security codes must never be stored by you; the security code may not be stored by anyone after authorisation.
3. Money in code · Practise: amounts · QuizTwo rye loaves at $7.25 each. What amount does Stripe expect?C. 1450$7.25 is 725 cents, and two of them are 1,450 cents, so 1450. 14.5 is a decimal, which Stripe rejects for amounts; 145 would charge $1.45.
3. Money in code · Practise: amounts · Fill the blanksTotal a cart in cents: three croissants at 350 each and one loaf at 850.1: 350, 2: 850, 3: 1900Everything stays in cents: 3 × 350 is 1,050, plus 850 is 1,900, which is $19.00. No decimals appear until you show the total.
3. Money in code · Currencies have codes, and some have no cents · QuizThe bakery sells a ¥900 loaf in Japan. What does it send to Stripe?B. 900The yen is zero-decimal: there’s no smaller unit, so ¥900 is 900. Multiplying by 100 out of habit would charge ¥90,000. Code that converts amounts should look up how many decimals a currency has, not assume two.
3. Money in code · Round once, on purpose · Find the bugStripe rejects this request with “Invalid integer”. Which line causes it?Line 3: const price = Math.round(loaf * (1 - discount))850 × 0.85 is 722.5, a fraction of a cent, and unit_amount must be a whole number. Rounding once where the fraction appears, with Math.round, gives 723. Don’t worry about the stripe.prices.create call yet: it’s how a price is made, in module 6.
3. Money in code · Try it: total a cart in cents · Code challengeWrite cartTotal(lines, percentOff). Each line has a unitAmount in cents and a quantity. Add them up, then take off the discount: percentOff percent of the subtotal, rounded to the nearest cent with Math.round.One solution:
Prices times quantities stay whole numbers, so the subtotal is exact. Only the percentage can make a fraction (127.5 cents), so that is the one place to round, once. Rounding in the middle of a sum, or converting to dollars and back, is how carts end up a cent off from the receipt.
4. Your Stripe account: test mode, the Dashboard and keys · Check: which key goes where? · QuizWhich key is safe to include in the bakery’s web page, where every visitor can read it?B. pk_live_...Only publishable keys (pk_) are meant to be public: they can do very little on their own. Secret (sk_) and restricted (rk_) keys act on the account and must stay on the server.
4. Your Stripe account: test mode, the Dashboard and keys · Check: which key goes where? · QuizYou made a “Sourdough” price in test mode. Your live server asks Stripe for it by its id. What happens?B. Stripe says there is no such priceA live key can’t see test objects at all, so the request fails as if the id didn’t exist. That is a common surprise on launch day; a later module shows how to avoid hard-coding ids that differ between environments.
4. Your Stripe account: test mode, the Dashboard and keys · Test cards make every outcome happen on purpose · QuizYou want to see what the bakery’s checkout does when a card is declined. What do you do?B. Pay in test mode with 4000 0000 0000 0002Test cards make each outcome repeatable without risk. Real cards don’t work in test mode at all, and testing in live mode costs real fees and confuses your accounts.
5. Your first API call with the Node library · The library mirrors the API · Fill the blanksRead back a customer you made earlier, by its id.1: await, 2: customers, 3: retrieveReading one object by id is retrieve. Without await, sam would be a Promise rather than the customer, and sam.email would be undefined.
5. Your first API call with the Node library · The library mirrors the API · QuizWhich call changes Sam’s email?B. stripe.customers.update(customerId, { email: 'new@example.com' })update takes the id of the object to change and the fields to set. create would make a new customer instead; setting a property on the client changes nothing at Stripe, because only requests do.
5. Your first API call with the Node library · Read the error: it usually says what’s wrong · Find the bugStripe answers “Received unknown parameter: emailAddress”. Which line is wrong?Line 3: email: 'sam@example.com'The field is called email. Stripe rejects names it doesn’t know rather than ignoring them, and the message names the culprit. In a TypeScript editor, the library’s types underline this before you even run it.
6. Products and prices · A recurring price bills again every period · QuizThe bakery adds a box delivered every three months for $80. Which price matches?B. unit_amount: 8000, recurring: { interval: 'month', interval_count: 3 }$80 is 8000 cents, and “every three months” is a month interval with a count of 3. There is no quarter interval. Charging a third each month is a different product: the buyer would pay monthly, and the total wouldn’t come to exactly $80.
6. Products and prices · A price’s amount never changes: make a new one · Fill the blanksRaise the loaf to $9.00: make a new price, then archive the old one.1: create, 2: 900, 3: activeNew amount, new price: create with 900 cents. Archiving with active: false keeps history intact. Existing subscribers stay on their old price until you move them, which a later module covers.
6. Products and prices · Or describe the price inside each checkout · QuizThe bakery’s cake orders are priced by size and decoration, worked out on its server. Which suits them?B. Inline price_data, with the amount the server worked outWhen the amount is computed per order, describe it inline in the checkout. Making a catalogue price for every combination would fill the Dashboard with prices used once. And the amount must come from the server, never the browser, as the next module explains.
6. Products and prices · Metadata: your own labels on Stripe’s objects · QuizWhich value can go in metadata as it is?B. { weight: '800' } (a string)Metadata values are strings of up to 500 characters. (The Node library also takes a number, but Stripe stores it as text and gives back '800'.) Keep objects, lists and long notes in your own database, storing only their id in metadata.
7. Checkout: Stripe’s payment page · The browser says what; the server says how much · Find the bugSomeone bought ten loaves for one cent each. Which line let them?Line 9: unit_amount: loaf.unitAmount,The amount came from a form field, which the buyer edited to 1. Look the item up on the server (line 1 does) and take its amount from there. Check the quantity too: a whole number between 1 and a sensible limit.
7. Checkout: Stripe’s payment page · Line items: a catalogue price or an inline one · Fill the blanksSell three croissants from the catalogue.1: payment, 2: price, 3: quantityA one-off purchase is payment mode, and a catalogue line item is a price id with a quantity. Stripe multiplies and shows the total on its page.
7. Checkout: Stripe’s payment page · Send the buyer with a 303 redirect · QuizCould the bakery create one session at start-up and send every buyer to its url?B. No: a session is for one purchase, and once paid or expired it can’t be used againEach session is one purchase by one buyer: once completed, it’s done, and an unused session expires after 24 hours by default. Create a new one every time someone presses the checkout button.
7. Checkout: Stripe’s payment page · Options worth knowing · QuizThe bakery delivers only in the US and Canada. Which option collects a delivery address?A. shipping_address_collection: { allowed_countries: ['US', 'CA'] }shipping_address_collection adds an address form limited to the countries listed (two-letter country codes, in capitals). The address then appears on the completed session for the bakery to deliver to.
8. Success pages, cancel pages and what to trust · {CHECKOUT_SESSION_ID} is filled in by Stripe · QuizIn TypeScript, the success_url is written as a template literal. Why doesn’t this break it?B. Only ${…} is code in a template literal; plain {…} is ordinary textA template literal (text in backticks) runs code only inside ${…}. {CHECKOUT_SESSION_ID} without a $ stays as written and reaches Stripe, which swaps it for the id. Writing ${CHECKOUT_SESSION_ID} by mistake would look for a variable of that name.
8. Success pages, cancel pages and what to trust · Visiting the success page must grant nothing · QuizWhere should the bakery mark an order as paid and send it to the kitchen?C. When Stripe notifies the server that the payment succeededCreating a session is not a payment, and the success page may never be visited (or be visited by anyone). Stripe’s notification, a webhook, arrives whether or not the buyer comes back. The success page can also check and record, as long as running both is harmless, a pattern for a later module.
8. Success pages, cancel pages and what to trust · Two fields tell you where a session stands · QuizA session is complete but unpaid. What should the bakery do?B. Wait: the payment is still clearing, and Stripe will say when it succeeds or failsWith delayed methods, like some bank debits, the buyer finishes the page before the money arrives. Stripe sends a later event when it succeeds or fails. Fulfilling on complete alone would hand out bread that might never be paid for.
8. Success pages, cancel pages and what to trust · Tie the session to your own order · Fill the blanksLabel the session and its payment with the bakery’s order id.1: client_reference_id, 2: metadataclient_reference_id is a single string meant exactly for your own id, and metadata can hold several labels. Copying the label to payment_intent_data.metadata means a refund or dispute, which arrives about the payment and not the session, can still be traced to the order.
8. Success pages, cancel pages and what to trust · The cancel page keeps the cart · Find the bugSome buyers who paid have their orders marked “cancelled”. Which line causes it?Line 3: // show the cart; leave the order as it isVisiting the cart says nothing about payment: a buyer can pay in one tab and open the cart in another, or come back with the browser’s back button after paying. Let Stripe’s events decide an order’s fate; a page visit only shows things.
9. Webhooks from zero · Check: who tells the bakery? · QuizA buyer pays for a weekend order and their phone dies before the success page loads. How does the bakery find out?B. The checkout.session.completed webhookStripe sends the event whatever happens to the browser, and keeps trying until your server confirms it received it. The success page is a courtesy for the buyer; the webhook is how your system learns the truth.
9. Webhooks from zero · Verify the exact bytes Stripe sent · Find the bugEvery real event is rejected with “Bad signature”. Which line is the cause?Line 2: app.use('/api', express.json())express.json() runs first for every route, reads the body and turns it into an object. By the time the webhook route runs, the raw bytes are gone, and re-creating them never matches the signature. Apply the JSON parser only to routes that need it, and keep the webhook raw.
9. Webhooks from zero · Try it: compute a Stripe signature · Code challengeFinish sign(secret, t, payload): import secret as an HMAC SHA-256 key, sign the text made of t, a dot and payload (1767225600.{…}) with it, and return the result as hex with the hex helper.One solution:
The timestamp is inside the signed text, so it cannot be changed without breaking the signature: that is what makes the five-minute window enforceable. The key is the whole secret string, whsec_ prefix included. Signing the parsed-and-rebuilt JSON instead of the exact text would give a different code, which is the raw-body bug from the last slide.
9. Webhooks from zero · Try it: verify a delivery · Code challengeFinish verify(payload, header, secret, now) (times in seconds). Read t and every v1 from the header. Return false if t is missing or more than 300 seconds before now; otherwise true when any v1 equals sign(secret, t, payload).One solution:
Solution
1asyncfunctionsign(secret:string, t:number, payload:string){2constbytes=(text:string)=>newTextEncoder().encode(text)3const hmac ={name:'HMAC',hash:'SHA-256'}4const key =await crypto.subtle5.importKey('raw',bytes(secret), hmac,false,['sign'])6const mac =await crypto.subtle7.sign('HMAC', key,bytes(`${t}.${payload}`))8return[...newUint8Array(mac)]9.map((byte)=> byte.toString(16).padStart(2,'0'))10.join('')11}1213asyncfunctionverify(14 payload:string, header:string, secret:string, now:number15):Promise<boolean>{16const pairs = header.split(',').map((part)=> part.split('='))17const t =Number(pairs.find(([key])=> key ==='t')?.[1])18if(!Number.isInteger(t)|| now - t >300)returnfalse19const expected =awaitsign(secret, t, payload)20return pairs.some(([k, v])=> k ==='v1'&& v === expected)21}
Splitting on , then = gives the pairs; some accepts the delivery if any v1 matches, which keeps working during a secret rollover. The age check uses the signed t, so an attacker who replays a recorded delivery an hour later is refused even though its signature is real. One thing the library does better: it compares signatures in constant time, so the comparison’s speed leaks nothing.
9. Webhooks from zero · Handle each event type you care about · Fill the blanksAdd a case for a delayed payment that failed, so the order is released.1: async_payment_failed, 2: data, 3: objectDelayed payments end with async_payment_succeeded or async_payment_failed. The session is always at event.data.object. Without this case, a failed bank payment would leave the order waiting forever.
9. Webhooks from zero · Answer quickly, with the right status · QuizAn event type the bakery doesn’t act on arrives. What should the endpoint answer?A. 200, and do nothingThe delivery worked; there is just nothing to do. An error would make Stripe retry an event you will never handle. Better still, only subscribe the endpoint to events you handle.
10. The Stripe CLI and testing · stripe listen forwards events to your laptop · QuizLocally, every forwarded event fails with “Bad signature”. What’s the likely cause?B. The server uses the Dashboard endpoint’s secret, not the one stripe listen printedEach endpoint has its own signing secret, and stripe listen acts as a separate endpoint with its own. Put the whsec_... it prints into your local .env. A wrong port would show as a connection error in the CLI, not a bad signature.
10. The Stripe CLI and testing · Practise: a whole purchase on your laptop · Put in orderPut a local test purchase in order.1. Start your server and stripe listen --forward-to its webhook route 2. Put the printed whsec_... in .env and restart the server 3. Add a loaf to the cart and press Checkout 4. Pay on Stripe’s test page with 4242 4242 4242 4242 5. See checkout.session.completed arrive and the order marked paidThe listener must be running before the payment: it only forwards events that happen while it runs. The server needs the listener’s secret loaded to verify what it forwards.
10. The Stripe CLI and testing · Test the edges, not only the happy path · QuizWhich tests are worth writing for the bakery’s webhook route? Pick all that apply.A. A correctly signed paid event marks the order paid; B. An unsigned or wrongly signed request gets a 400 and changes nothing; C. The same event delivered twice gives one paid order, not two; D. An event for an order the bakery doesn’t know is ignored with a 200All four. The happy path matters, but payments fail at the edges: forged requests, duplicates (which Stripe does send) and events for things you didn’t make. The next module is about making duplicates harmless.
10. The Stripe CLI and testing · Test the edges, not only the happy path · Fill the blanksOn a runtime with only asynchronous crypto, the helper has an async twin.1: await, 2: generateTestHeaderStringAsyncLike constructEventAsync, generateTestHeaderStringAsync returns a Promise, so it needs await. Use it in tests that run in Workers or other web runtimes.
10. The Stripe CLI and testing · The Dashboard shows every delivery · QuizA test order stays “pending” though the payment succeeded. Where do you look first?B. The event’s page in the Dashboard: was it delivered, and what did your server answer?The event page tells you at once whether Stripe sent it, to which address, and whether your server answered 200, 400 (often a secret mix-up) or 500 (a bug in your handler). It turns guessing into reading.
11. Retries, duplicates and idempotent handlers · Spot the race · Find the bugNow and then a buyer gets two receipts for one order. Which line opens the gap?Line 3: // update only if pending, in one statement, and stop if nothing changedTwo deliveries arriving together both read pending at line 2, both pass the check at line 3, and both send a receipt. This is a race condition: the result depends on timing. Checking and changing in one conditional statement, as on the previous slide, closes the gap.
11. Retries, duplicates and idempotent handlers · Try it: a handler that is safe to repeat · Code challengeMake markPaid idempotent: only an order that is pending becomes paid and gets a receipt. Then add a case for checkout.session.async_payment_succeeded that marks its order paid too.One solution:
The guard asks the order, not the event, whether this is the first time. That one check covers both repeats of one event and two different events about the same purchase. The starter’s version sent a receipt every time it was called, which is exactly what a retried delivery does to a handler that trusts it is seeing an event for the first time.
11. Retries, duplicates and idempotent handlers · Record first, answer fast, do slow work later · Put in orderPut the webhook handler’s steps in order.1. Read the raw body and the Stripe-Signature header 2. Verify with constructEvent; answer 400 if it fails 3. Check the event is about one of your orders 4. Update the order in one idempotent statement 5. Queue the slow work, keyed by the order id 6. Answer 200Nothing is trusted before the signature check, nothing changes before you know the order is yours, and the answer comes last, so a failure before it makes Stripe try again. Keying queued jobs by the order means a duplicate can’t queue the same work twice.
11. Retries, duplicates and idempotent handlers · Delayed payments: two events, one order · QuizA checkout.session.completed event arrives with payment_status: 'unpaid'. What should the handler do?B. Mark it “awaiting payment”, and wait for the async eventThe buyer finished, but the money hasn’t cleared. Note it so you can tell the buyer, and let async_payment_succeeded or async_payment_failed decide. Answering 500 would only make Stripe retry an event that will never change: events are snapshots.
11. Retries, duplicates and idempotent handlers · One fulfilment function, called from two places · QuizWhy does fulfil retrieve the session instead of trusting what it was given?B. On the success page, the only input is an id from the address bar; Stripe’s copy is the truthAn id from a URL could be anything, so asking Stripe for the session means only a really paid session counts. It also gives the latest state rather than an event’s older snapshot. Because markPaid is idempotent, calling fulfil from both places is safe.
12. Payment Intents and Elements · A Payment Intent moves through a few statuses · QuizA buyer’s card is declined. What status is the Payment Intent in afterwards?B. requires_payment_methodA decline doesn’t end the payment: it goes back to waiting for a method, so the buyer can try another card. The decline is recorded in last_payment_error. Only an explicit cancel, or abandonment in some flows, makes it canceled.
12. Payment Intents and Elements · Guard the client secret · QuizconfirmPayment reported no error and the buyer reached /thanks. Is the order paid?B. Not necessarily: check the Payment Intent on the server, and fulfil from the payment_intent.succeeded webhookThe browser is under the buyer’s control, and some methods are still processing when the buyer returns. As with Checkout, fulfil from a verified webhook (payment_intent.succeeded) or a server-side read that says succeeded.
12. Payment Intents and Elements · Some banks ask the buyer to prove it’s them · QuizHow do you see the 3D Secure step in test mode?B. Pay with the test card 4000 0025 0000 3155That test card always asks for authentication, and Stripe shows a test page to approve or fail it. Testing both answers matters: a buyer who fails the check should be able to try again.
12. Payment Intents and Elements · Authorise now, capture later · Fill the blanksHold the cake’s price now, and take it once the order is accepted.1: manual, 2: capturemanual capture authorises only. paymentIntents.capture(id) takes the money; you can also capture less than was held with amount_to_capture. To release the hold instead, paymentIntents.cancel(id).
13. Customers and subscriptions · A Customer remembers a buyer across payments · QuizWhich bakery buyers need a Stripe Customer?C. Subscribers always; one-off buyers if you want their history or saved cardsA subscription can’t exist without a customer to bill. For one-off orders it’s a choice: a customer lets returning buyers see past orders and reuse a card, at the cost of keeping one more record in step.
13. Customers and subscriptions · One Stripe customer per person in your system · Find the bugA regular buyer has twelve customers in Stripe. Which line is to blame?Line 2: if (user.stripeCustomerId) return user.stripeCustomerIdThe function creates a new customer every time. Return the stored id when there is one, and create only when there isn’t. (Two requests at once could still both create one; keying the create with an idempotency key, in module 17, closes that too.)
13. Customers and subscriptions · Sell the bread club with Checkout · Fill the blanksFinish the subscription checkout.1: subscription, 2: subscription_dataA recurring price needs subscription mode; payment mode would refuse it. Metadata in subscription_data lands on the Subscription itself, which later events are about, rather than only on the session.
13. Customers and subscriptions · A subscription’s life, in statuses · QuizA subscriber’s card expired and this month’s renewal failed. What status is the subscription in while Stripe retries?B. past_duepast_due means a renewal failed but the subscription is still alive and Stripe is retrying. incomplete is only about the very first payment. What happens when retries run out (cancel, mark unpaid, or leave past_due) is a setting in the Dashboard, covered in module 15.
13. Customers and subscriptions · Try it: decide access from the status · Code challengeWrite hasAccess(status, daysPastDue). active and trialing get access. past_due gets access only during a 7-day grace period (daysPastDue of 7 or less). Every other status gets none.One solution:
List the statuses that grant access and refuse everything else. The starter did the opposite, refusing only canceled, so incomplete (never paid) and unpaid (retries ran out) got bread for free. An allowlist also fails safe if Stripe adds a status one day: an unknown one gets no access until you decide.
13. Customers and subscriptions · Follow subscriptions through their events · QuizWhich event best tells the bakery that February’s bread is paid for?B. invoice.paidEvery period’s payment is an invoice, so invoice.paid arrives each month, including the first. checkout.session.completed only happens once, at sign-up, and subscription.created says nothing about money.
14. Trials, plan changes and the customer portal · A free trial delays the first payment · QuizA member signs up for the trial. What is the completed session’s payment_status?C. no_payment_requiredNothing is owed yet, so there is nothing to pay: no_payment_required. trialing is the subscription’s status, not the session’s. A handler that only accepts paid would wrongly refuse every trial sign-up.
14. Trials, plan changes and the customer portal · Plan for the end of the trial · QuizWhich event should send the “your trial ends soon” email?B. customer.subscription.trial_will_endtrial_will_end arrives three days before the trial ends (or at once, if the trial is shorter). Reminding people before they’re charged is kind, and in many places it’s also required, and it heads off disputes from surprised members.
14. Trials, plan changes and the customer portal · Proration: paying fairly for part of a period · QuizA member on the $45 plan moves to the $30 plan exactly halfway through the month. With prorations, what happens?B. They get a $7.50 credit towards their next invoiceCredit for half of $45 (−$22.50) plus half of $30 (+$15.00) is −$7.50. Prorations become lines on an invoice, so a credit reduces what they pay next time; it isn’t sent back to the card unless you refund it.
14. Trials, plan changes and the customer portal · Choose when prorations are billed · Fill the blanksCharge the upgrade difference immediately.1: update, 2: id, 3: always_invoicealways_invoice creates an invoice for the prorations at once and tries to pay it. The item’s id makes it a replacement rather than an extra item.
14. Trials, plan changes and the customer portal · Cancel now, or at the end of the period · QuizA member cancels on the 10th of a month already paid. When should their deliveries stop?B. When customer.subscription.deleted arrives at the end of the paid periodWith cancel_at_period_end, the updated event tells you a cancellation is scheduled (show “ends on the 31st”), but the member has paid for the month. deleted marks the actual end. Acting on updated would cut off bread they paid for.
14. Trials, plan changes and the customer portal · The customer portal lets members manage themselves · Find the bugAnyone can open any member’s billing page. Which line allows it?Line 3: customer: req.user.stripeCustomerId,The customer id came from the request, so anyone logged in can send someone else’s cus_... and manage their subscription. Take the id from the signed-in user’s own record on the server.
15. Invoices and failed payments · Check: invoice statuses · QuizAn open invoice went out with the wrong amount. What do you do?C. Void it, and issue a correct oneOnce finalised, an invoice is a legal record and can’t be edited or deleted. Voiding keeps the record and shows it was cancelled; a new invoice carries the right amount. Only drafts can be changed or deleted.
15. Invoices and failed payments · What an invoice tells you · Fill the blanksFind which subscription an invoice belongs to.1: parent, 2: subscription_detailsInvoices can come from subscriptions, quotes or be made by hand, so the origin lives under parent. Optional chaining (?.) copes with invoices that have no subscription behind them.
15. Invoices and failed payments · Handle a failed renewal in your webhook · QuizThe bakery gives past_due members a week of bread while Stripe retries. Where does that rule belong?B. In the bakery’s own code, reading the subscription status it storedStripe says what state the billing is in; what a member gets in each state is your business rule. Keeping it in your code, from the status you mirrored, means you can change the grace period without touching billing.
15. Invoices and failed payments · When retries run out · QuizThe Dashboard is set to cancel subscriptions when all retries fail. Which event marks that moment?B. customer.subscription.deletedA cancelled subscription ends with customer.subscription.deleted, which is where deliveries stop. invoice.payment_failed comes after each failed attempt, so acting on it alone would stop bread at the first hiccup.
15. Invoices and failed payments · Bill a business by invoice · Put in orderPut a hand-made invoice’s life in order.1. Create a draft invoice for the customer 2. Add the invoice items 3. Finalise it, so it becomes open 4. Send it to the customer 5. The customer pays, and invoice.paid arrivesItems can only be added while the invoice is a draft; finalising fixes them. Sending only makes sense once it’s final, and invoice.paid closes the loop for your system, as with subscriptions.
16. Refunds, disputes and taxes · Refunds arrive as events, whoever made them · Find the bugA customer refunded for one squashed loaf has their whole order cancelled. Which line is wrong?Line 4: if (charge.refunded) await orders.markRefunded(orderId)charge.refunded fires for partial refunds too. Only charge.refunded === true (or amount_refunded === amount) means everything was given back. For a partial refund, record the amount and keep the order.
16. Refunds, disputes and taxes · Respond with evidence, or accept it · QuizWhich steps make disputes less likely? Pick all that apply.A. A recognisable name on the buyer’s statement (the statement descriptor); B. Clear receipts, and an easy way to ask for a refund; D. Reminding members before a trial turns into a chargeMany disputes are confusion: an unfamiliar name on a statement, a forgotten trial. Make the charge recognisable and the bakery easy to reach, and people ask you instead of their bank. Ignoring requests pushes them to the bank, where each dispute costs a fee and too many put your account at risk.
16. Refunds, disputes and taxes · Stripe Tax works out the amount at checkout · QuizA price of €12.00 is inclusive of 20% VAT. How much of it is VAT?B. €2.00Inclusive means €12.00 is price plus tax: 12.00 ÷ 1.20 = €10.00 before tax, so €2.00 is VAT. €2.40 is 20% of €12.00, the exclusive calculation, and €14.40 would be the total if tax were added on top.
16. Refunds, disputes and taxes · Read what was charged · Fill the blanksSave an order’s totals from a completed session.1: total_details, 2: amount_tax, 3: amount_totalAmounts are integers in the smallest unit, as always. total_details also holds amount_discount and amount_shipping, so the four numbers explain how the total was reached.
17. Errors, retries and idempotency keys · Catch the errors you mean · Find the bugDuring a Stripe outage, buyers are told “That order doesn’t exist”. Which line causes it?Line 5: if (e instanceof Stripe.errors.StripeInvalidRequestError && e.statusCode === 404) return nullEvery Stripe error, an outage, a rate limit, a bad key, turns into “not found”. Only a 404 means the session doesn’t exist. Let the rest be thrown, so the page can say “try again in a moment” and your monitoring sees a real failure.
17. Errors, retries and idempotency keys · Card errors are for the buyer, gently · QuizA payment is declined with decline_code: 'fraudulent'. What should the buyer see?B. “Your card was declined. Please try another card or contact your bank.”Telling a fraudster which check caught them helps them try again smarter, and telling an honest buyer they look like a fraudster is unkind. A generic decline message works for both. Stripe’s own pages follow the same rule.
17. Errors, retries and idempotency keys · Derive the key from what the request is for · Find the bugA retried request still created two customers. Which line makes the key useless?Line 4: { idempotencyKey: customer-${user.id} }A key based on the time is different on every attempt, so Stripe sees two unrelated requests. Build the key from the thing the request is about (the user, the order, the complaint) so every attempt at the same job shares it.
17. Errors, retries and idempotency keys · Derive the key from what the request is for · QuizThe same idempotency key is sent again, but with a different amount. What does Stripe do?C. Refuses with an idempotency errorA key promises “this is the same request”. Different parameters break that promise, so Stripe refuses with a StripeIdempotencyError, which usually points to a bug: two different jobs sharing a key.
17. Errors, retries and idempotency keys · Try it: retry with one key · Code challengeFix refundComplaint. Its idempotency key must be refund- followed by the complaint’s id, the same on every attempt. If create throws, try again, up to 3 attempts in all; after the third failure, let the error through.One solution:
The key is built once, from the complaint, outside the loop, so every attempt carries the same one. If the first attempt actually reached Stripe and only the answer was lost, the second gets back that same refund instead of making another. A key from Date.now() changes between attempts, which turns a safe retry into a second refund. The return await inside try matters too: without await, a failure would escape the catch.
17. Errors, retries and idempotency keys · Let the library retry for you · QuizA nightly job calls Stripe in a tight loop and starts getting 429s. What helps most?B. Slow down: fewer requests in parallel, pauses that grow after each 429, and list endpoints that return up to 100 objects a pageA 429 means “too many requests for now”. Waiting longer after each one (exponential backoff) and making fewer, bigger requests fixes the cause. Retrying faster makes it worse, and test mode has lower limits than live, so a job can fail there first.
18. Syncing state and reconciling money · Events don’t arrive in order · Find the bugA member who cancelled is shown as “active” again the next morning. Which line lets an older event win?Line 4: await syncSubscription(sub.id) // fetch the current stateA delayed updated event, still saying active, arrived after deleted and overwrote canceled. Either fetch the subscription’s current state from Stripe before writing (next slide), or store event.created and ignore events older than the last one applied.
18. Syncing state and reconciling money · Catch up on events you missed · Fill the blanksReplay every subscription change since an outage began.1: events, 2: gte, 3: awaitcreated: { gte } means “at or after” a Unix timestamp. for await walks through every page of results for you, so you don’t handle starting_after yourself. types filters the list but not TypeScript’s view of it, hence the as Stripe.Subscription. Because syncSubscription is idempotent, replaying events you already handled is harmless.
18. Syncing state and reconciling money · A regular check catches what events miss · QuizWhat should the bakery’s nightly check compare?B. Members marked active in its database against subscriptions Stripe says are active or trialingWhat matters is the facts that decide who gets bread. Listing subscriptions by status from Stripe and comparing with the members table finds both kinds of drift: people getting bread without paying, and paying without getting bread. Report the second kind loudly.
18. Syncing state and reconciling money · Reconcile a payout line by line · QuizThis week’s orders total $1,000, but the payout is $940. Which could explain it? Pick all that apply.A. Stripe’s processing fees; B. A refund issued this week; C. A dispute that took money and a fee; D. Some payments still waiting to become availableAll of these. A payout is the net of everything that became available in its window, not a sum of orders. Listing its balance transactions shows exactly which charges, fees, refunds and adjustments it includes, so the difference becomes a list instead of a mystery.
19. Fraud, security and going live · Radar scores every payment · QuizA $300 cake order comes through with risk_level: 'elevated'. What’s a sensible response?B. Look before baking: check the details, maybe contact the buyer, and refund if it seems wrongElevated isn’t proof of fraud; many such payments are fine. For something costly and slow to make, a quick human look is worth it. Refunding a suspicious order before baking is far cheaper than a dispute later.
19. Fraud, security and going live · Rules and reviews tune Radar to your shop · QuizWhy not block every payment with a risk score above 30?B. Too strict a rule turns away many honest buyers, which costs more than the fraud it stopsEvery rule trades fraud against lost sales. Radar shows how many past payments a rule would have caught before you turn it on; use that to set thresholds that stop the bad without blocking good customers.
19. Fraud, security and going live · Early fraud warnings: refund before the dispute · Fill the blanksRefund a payment flagged by an early fraud warning.1: early_fraud_warning, 2: fraudulentRefunding with reason: 'fraudulent' also tells Radar the payment was fraud, which helps it learn. The idempotency key, from the warning’s id, keeps a duplicate event from trying a second refund.
19. Fraud, security and going live · Make your checkout a poor target · QuizOvernight, the bakery sees 400 declined $1 payments from new visitors. What is most likely happening?B. Card testingMany tiny, mostly failing payments from strangers is the signature of card testing. Rate-limit or temporarily close the checkout route, and look at Radar’s rules. Even the declines matter: banks notice merchants with many failed attempts.
19. Fraud, security and going live · A security review of a payment flow · QuizWhich belong in a security review of the bakery’s Stripe code? Pick all that apply.A. Secret keys only in server environment variables, never in pages or git; B. Every webhook verified with its signing secret, over the raw body; C. Prices and quantities decided on the server; D. Client secrets, keys and card details kept out of logs and URLsEach one closes a real attack: a leaked key, forged “paid” events, a one-cent cart, and secrets lifted from logs. Add a restricted key with only the permissions the server uses, HTTPS everywhere, and pages that grant nothing on a visit alone.
19. Fraud, security and going live · Going live · Put in orderPut the launch steps in order.1. Activate the account with business and bank details 2. Recreate products, prices (with the same lookup keys), tax settings and portal settings in live mode 3. Add the live webhook endpoint and store its new signing secret 4. Swap the server’s keys and webhook secret for live ones 5. Make one small real purchase, check it end to end, and refund itThe account must be active before live keys can take payments. Catalogue and endpoint come next, so that the switch of keys finds everything in place. A real purchase is the only test that proves live mode works, and refunding it costs little.
20. Mastery challenge · Challenge: the whole one-off flow · Put in orderOrder the life of a weekend order, from button to bank.1. The server prices the cart and creates an order marked pending 2. The server creates a Checkout Session with the order id and redirects with a 303 3. The buyer pays on Stripe’s page and the issuer authorises 4. A signed checkout.session.completed arrives and the order is marked paid, once 5. The money becomes available and is paid out to the bakery’s bankYour record exists before Stripe’s, so the session can point at it. Fulfilment waits for a verified event, and duplicates change nothing. The payout comes days later and is reconciled from balance transactions.
20. Mastery challenge · Challenge: the handler that emails twice · Find the bugUnder retries, some buyers get two receipts though only one order is paid. Which line is in the wrong place?Line 2: // send the receipt after the update, only if a row changedThe update is idempotent, but the email runs before it, on every delivery. Side effects belong after the conditional change and only when it changed a row, like the bake list here. Order matters as much as the SQL.
20. Mastery challenge · Challenge: nobody can join the trial · QuizSince the 14-day trial launched, no new member gets a welcome email. The handler is below. Why?B. A trial sign-up’s session is no_payment_required, so the check skips itNothing is owed at a trial’s start, so payment_status is no_payment_required. For subscriptions, decide access from the subscription’s own status (trialing or active), fetched or mirrored, rather than from the session’s payment status.
20. Mastery challenge · Challenge: the upgrade that doubled the bill · Find the bugMembers who upgrade to the large box are billed for both boxes. Which line causes it?Line 4: items: [{ id: item.id, price: largeBoxMonthlyId }],An item without an id is a new item, so the subscription now has two prices. Passing the existing item’s id replaces its price. The unused item variable on line 2 was the clue.
20. Mastery challenge · Challenge: a safe refund from a queue · Fill the blanksA queue job refunds one loaf for a complaint, and may run more than once. Make it safe.1: 850, 2: idempotencyKey, 3: complaint.idA loaf is 850 cents. The key comes from the complaint, the job’s own identity, so a retried job gets the first refund back instead of making another. A key from Date.now() or a random id would protect nothing.
20. Mastery challenge · Challenge: a safe refund from a queue · QuizThe refund call times out with a StripeConnectionError. What should the job do?B. Retry with the same idempotency keyA timeout means you don’t know whether Stripe made the refund. Retrying with the same key either makes it (if the first never arrived) or returns the first result (if it did). A new key risks two refunds; assuming success risks giving no refund at all.
20. Mastery challenge · Challenge: the member who came back from the dead · QuizAfter a cancellation, an old customer.subscription.updated (status active) arrives last. Which handlers stay correct? Pick all that apply.B. One that retrieves the subscription from Stripe and writes its current status; C. One that ignores events older than the last event.created it applied for that subscriptionFetching the current state makes order irrelevant, and comparing created times refuses stale snapshots. Copying the snapshot revives the member. Answering 500 only makes Stripe resend the same old event for days.
20. Mastery challenge · Challenge: launch day · QuizOn launch day, every checkout fails with “No such price”. Keys are live and correct. What was missed?B. The price ids in the configuration are test-mode idsTest and live share no objects, so a test price_... doesn’t exist for a live key. Looking prices up by lookup key, with the same keys in both modes, avoids the problem entirely. A wrong webhook secret would break fulfilment, not checkout.
20. Mastery challenge · Challenge: launch day · QuizA week after launch, the payout is lower than the sum of paid orders. What do you do first?B. List the payout’s balance transactions and match fees, refunds and adjustmentsA payout is a net amount. Its balance transactions show every charge, fee, refund and dispute adjustment included, which almost always explains the difference. Only an unexplained gap after that is worth a support ticket.
20. Mastery challenge · Challenge: one last route · Find the bugThis success page has a security hole. Which line?Line 4: // show the welcome; membership is granted by the webhookAny logged-in user can open this page with any session id, even someone else’s, and be made a member. The page should only show what Stripe reports; membership comes from the verified subscription events (or a check that this session is this user’s, paid or trialing, done idempotently).