Appearance
Setup
Step-by-step for an owner who has no Stripe account yet. Everything is done in Stripe's sandbox / test mode, so no real money moves. Going live repeats the relevant steps with live-mode objects.
Not yet done
At the time of writing the sandbox account has not been created and nothing in this page has been executed against Stripe. Facts about the Worker (secret names, event names, required prices) were read from the code. Dashboard menu names and button labels are (general Stripe knowledge) and can differ from what Stripe shows today.
What you will collect
| You create in Stripe | You copy | Worker secret |
|---|---|---|
| Product "Reform Assistant" with a $8.00 per month Price | Price ID price_... | STRIPE_PRICE_MONTHLY |
| A $21.60 every 3 months Price on the same product | Price ID | STRIPE_PRICE_QUARTERLY |
| A $81.60 every 12 months Price on the same product | Price ID | STRIPE_PRICE_ANNUAL |
| (account) | Secret key sk_test_... | STRIPE_SECRET_KEY |
Webhook endpoint (or stripe listen) | Signing secret whsec_... | STRIPE_WEBHOOK_SECRET |
Step 1: Create a Stripe account
- Open the Stripe Dashboard at
https://dashboard.stripe.comand choose Sign up (general Stripe knowledge). - Enter an email address and a password, then confirm your email address.
- Stripe may ask you to activate your account (business profile, bank account, identity). You do not need to finish activation to use test mode / a sandbox (general Stripe knowledge); skip it for now.
Step 2: Stay in test mode (sandbox)
Use the test-mode switch in the Dashboard, or a Sandbox if your account offers them, so that the Dashboard shows its test/sandbox indicator (general Stripe knowledge). Create everything below in this mode.
- Test-mode and live-mode objects are completely separate: keys, Products and Prices, webhook endpoints and customers do not exist in the other mode.
- The secret key, all three Price IDs and the webhook signing secret you give the Worker must all come from the same mode. Mixing them makes Stripe answer with an error, which the Worker returns as
502 stripe_error.
Business details
Business details (legal name, address, support email, statement descriptor) are entered under the account/business settings (general Stripe knowledge). Fill in at least the business name and a support email now: Stripe Checkout and receipt emails display them, and the portal's own legal pages (/terms/, /privacy/) use support@reformassistant.example as a placeholder that has to be replaced with a real address before launch (see Going live). Full activation (bank account, identity verification) is only required to go live.
Step 3: Create the product and three prices
The Prices must match the plan catalog in src/plans.ts exactly. The Worker never reads amounts from Stripe or sends an amount to it: it sends only the Price ID. What the family is charged is whatever the Price in Stripe says, so a wrong amount here is a wrong charge.
| Plan id | Product | Amount | Currency | Recurring billing period |
|---|---|---|---|---|
monthly | Reform Assistant | $8.00 | USD | every 1 month |
quarterly | Reform Assistant | $21.60 | USD | every 3 months |
annual | Reform Assistant | $81.60 | USD | every 12 months |
In the Dashboard open the Product catalog and choose Add product (general Stripe knowledge).
Name:
Reform Assistant. A description is optional (Checkout shows it).Add the first Price: pricing model Standard, type Recurring, amount
8.00, currencyUSD, billing period Monthly (every 1 month).Add a second Price on the same product: Recurring,
21.60USD, billing period Custom with an interval of every3months (a custom interval is how Stripe expresses "every 3 months"; general Stripe knowledge).Add a third Price: Recurring,
81.60USD, every12months (Stripe may offer Yearly for this; the code only needs one 12-month recurring Price).Save the product, open each Price and copy its Price ID (
price_...). Keep track of which is which:Price Goes into $8.00 / month STRIPE_PRICE_MONTHLY$21.60 / 3 months STRIPE_PRICE_QUARTERLY$81.60 / 12 months STRIPE_PRICE_ANNUAL
Notes from the code:
- A Price ID is also how the Worker recognises the plan in webhooks.
subscriptionPlaninsrc/stripe_webhook.tsfirst compares the Price ID of the subscription's first item to the three secrets, then falls back tometadata.plan_id. If a Price ID is wrong or unset, plan changes made in the Billing Portal are not followed reliably, so set all three even if you only plan to sell one. - Each Price must be recurring: the Checkout Session is created with
mode=subscriptionand Stripe rejects it for a one-time Price (the Worker would return502 stripe_error). - Keep one item per subscription: the Worker sends exactly one line item with quantity 1 and reads only the first subscription item (plan and period) from Stripe.
Step 4: Copy the secret key
- In the Dashboard open the developer area and the API keys page (general Stripe knowledge: Developers → API keys).
- In test mode, reveal and copy the Secret key (
sk_test_...). Do not use the publishable key; the Worker never needs it.
Never put the secret key in public/, in a committed file or in wrangler.jsonc. It belongs in .dev.vars (local) and wrangler secret put (deployed).
STRIPE_SECRET_KEY is used for: creating and expiring Checkout Sessions, creating Billing Portal sessions, re-fetching the current subscription while handling webhooks, and cancelling duplicate subscriptions. See Webhooks.
Step 5: Configure the Customer portal
The console's Manage billing button calls POST /api/billing/portal, which creates a Stripe Billing Portal session (see Checkout). Stripe needs a portal configuration saved in the same mode, or it refuses to create portal sessions, and the Worker returns 502 stripe_error (general Stripe knowledge: the portal must be configured and saved once).
- In the Dashboard open the billing settings and the Customer portal page (general Stripe knowledge: Settings → Billing → Customer portal).
- Enable Customers can switch plans (subscription update) and add the product
Reform Assistantwith all three Prices: monthly, quarterly, annual (general Stripe knowledge). - Enable Customers can cancel subscriptions. Choose whether cancellation is immediate or at the end of the billing period; the Worker supports both:
- at the end of the period: Stripe sets
cancel_at_period_end, the subscription staysactiveuntil the period ends, and the portal storescancel_at_period_end = 1from thecustomer.subscription.updatedevent; afterwards thecustomer.subscription.deletedevent makes the statuscanceled; - immediately: the
customer.subscription.deletedevent makes the statuscanceledand the connectionnot_connected.
- at the end of the period: Stripe sets
- Enable Update payment method and View invoice history. Both are optional for the Worker, but they are what a family expects behind "Manage billing".
- Save the configuration.
What the Worker does with changes made in the portal:
- A plan switch becomes a
customer.subscription.updatedevent; the Worker re-fetches the subscription, reads the new Price ID from its first item, and sets the storedplan_idaccordingly (even when the subscription metadata still names the old plan). - Cancellation, failed renewals and recoveries are handled as described in Webhooks.
Step 6: Create the webhook endpoint
The Worker handles exactly these five event types (everything else is archived and acknowledged without a state change):
| Event | Needed for |
|---|---|
checkout.session.completed | Stores the customer id and subscription id after payment; clears the stored open Checkout Session id |
checkout.session.expired | Clears the stored open Checkout Session id |
customer.subscription.created | Creates or updates the subscription row, queues the connection |
customer.subscription.updated | Renewals, plan switches, past_due, cancel-at-period-end |
customer.subscription.deleted | Marks the subscription canceled, ends the connection |
Leave out the others: sending extra events does no harm (they are archived) but adds noise.
Deployed Worker (a public HTTPS origin exists):
- In the Dashboard open Developers → Webhooks and choose Add endpoint (general Stripe knowledge).
- Endpoint URL:
https://<your-origin>/api/stripe/webhook. The path comes fromsrc/index.ts. - Select the five events above.
- Save, then reveal the endpoint's Signing secret (
whsec_...) and copy it intoSTRIPE_WEBHOOK_SECRET.
Local development (wrangler dev, nothing public): Stripe cannot reach localhost. Use the Stripe CLI forwarder instead; its signing secret differs from any Dashboard endpoint's. See Testing.
Step 7: Put the secrets in place
Local: .dev.vars
Copy .dev.vars.example to .dev.vars (the file is git-ignored) and fill in every value:
text
APP_URL=http://localhost:8787
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PRICE_MONTHLY=price_...
STRIPE_PRICE_QUARTERLY=price_...
STRIPE_PRICE_ANNUAL=price_...
HERMES_LINK_TOKEN=<any long random string>Restart npm run dev after any change to .dev.vars.
Deployed: wrangler secret put
APP_URL is a plain var in wrangler.jsonc (set it to the real origin for the deployed Worker; see Cloudflare deployment). The other values are secrets and must never be written to wrangler.jsonc:
sh
npx wrangler secret put STRIPE_SECRET_KEY
npx wrangler secret put STRIPE_WEBHOOK_SECRET
npx wrangler secret put STRIPE_PRICE_MONTHLY
npx wrangler secret put STRIPE_PRICE_QUARTERLY
npx wrangler secret put STRIPE_PRICE_ANNUAL
npx wrangler secret put HERMES_LINK_TOKENEach command prompts for the value. Set STRIPE_SECRET_KEY in every deployed environment that receives webhooks: without it the handler cannot re-fetch the current subscription, so out-of-order events could write stale state, and duplicate subscriptions are recorded but not cancelled.
Verify the wiring
- With no Stripe values set,
POST /api/billing/checkoutanswers503 billing_not_configuredand the console shows "Payments are not switched on yet (sandbox setup pending)." That is the expected state before this setup. - With the values set, follow the sandbox flow in Testing and the manual QA checklist.