Skip to content

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 StripeYou copyWorker secret
Product "Reform Assistant" with a $8.00 per month PricePrice ID price_...STRIPE_PRICE_MONTHLY
A $21.60 every 3 months Price on the same productPrice IDSTRIPE_PRICE_QUARTERLY
A $81.60 every 12 months Price on the same productPrice IDSTRIPE_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 ​

  1. Open the Stripe Dashboard at https://dashboard.stripe.com and choose Sign up (general Stripe knowledge).
  2. Enter an email address and a password, then confirm your email address.
  3. 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 idProductAmountCurrencyRecurring billing period
monthlyReform Assistant$8.00USDevery 1 month
quarterlyReform Assistant$21.60USDevery 3 months
annualReform Assistant$81.60USDevery 12 months
  1. In the Dashboard open the Product catalog and choose Add product (general Stripe knowledge).

  2. Name: Reform Assistant. A description is optional (Checkout shows it).

  3. Add the first Price: pricing model Standard, type Recurring, amount 8.00, currency USD, billing period Monthly (every 1 month).

  4. Add a second Price on the same product: Recurring, 21.60 USD, billing period Custom with an interval of every 3 months (a custom interval is how Stripe expresses "every 3 months"; general Stripe knowledge).

  5. Add a third Price: Recurring, 81.60 USD, every 12 months (Stripe may offer Yearly for this; the code only needs one 12-month recurring Price).

  6. Save the product, open each Price and copy its Price ID (price_...). Keep track of which is which:

    PriceGoes into
    $8.00 / monthSTRIPE_PRICE_MONTHLY
    $21.60 / 3 monthsSTRIPE_PRICE_QUARTERLY
    $81.60 / 12 monthsSTRIPE_PRICE_ANNUAL

Notes from the code:

  • A Price ID is also how the Worker recognises the plan in webhooks. subscriptionPlan in src/stripe_webhook.ts first compares the Price ID of the subscription's first item to the three secrets, then falls back to metadata.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=subscription and Stripe rejects it for a one-time Price (the Worker would return 502 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 ​

  1. In the Dashboard open the developer area and the API keys page (general Stripe knowledge: Developers → API keys).
  2. 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).

  1. In the Dashboard open the billing settings and the Customer portal page (general Stripe knowledge: Settings → Billing → Customer portal).
  2. Enable Customers can switch plans (subscription update) and add the product Reform Assistant with all three Prices: monthly, quarterly, annual (general Stripe knowledge).
  3. 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 stays active until the period ends, and the portal stores cancel_at_period_end = 1 from the customer.subscription.updated event; afterwards the customer.subscription.deleted event makes the status canceled;
    • immediately: the customer.subscription.deleted event makes the status canceled and the connection not_connected.
  4. Enable Update payment method and View invoice history. Both are optional for the Worker, but they are what a family expects behind "Manage billing".
  5. Save the configuration.

What the Worker does with changes made in the portal:

  • A plan switch becomes a customer.subscription.updated event; the Worker re-fetches the subscription, reads the new Price ID from its first item, and sets the stored plan_id accordingly (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):

EventNeeded for
checkout.session.completedStores the customer id and subscription id after payment; clears the stored open Checkout Session id
checkout.session.expiredClears the stored open Checkout Session id
customer.subscription.createdCreates or updates the subscription row, queues the connection
customer.subscription.updatedRenewals, plan switches, past_due, cancel-at-period-end
customer.subscription.deletedMarks 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):

  1. In the Dashboard open Developers → Webhooks and choose Add endpoint (general Stripe knowledge).
  2. Endpoint URL: https://<your-origin>/api/stripe/webhook. The path comes from src/index.ts.
  3. Select the five events above.
  4. Save, then reveal the endpoint's Signing secret (whsec_...) and copy it into STRIPE_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_TOKEN

Each 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 ​

  1. With no Stripe values set, POST /api/billing/checkout answers 503 billing_not_configured and the console shows "Payments are not switched on yet (sandbox setup pending)." That is the expected state before this setup.
  2. With the values set, follow the sandbox flow in Testing and the manual QA checklist.