Setting Stripe up
This package ships the payment provider. Nothing here needs a Stripe SDK, a build step or a redeploy — a portal that holds the keys sells, and a portal that does not says so.
1. The three keys
| Key | What it is | Where it comes from |
|---|---|---|
Commerce:Stripe:SecretKey |
the server-side API secret (sk_test_… / sk_live_…) |
Stripe dashboard → Developers → API keys |
Commerce:Stripe:WebhookSecret |
the endpoint signing secret (whsec_…) |
Stripe dashboard → the endpoint you add in step 2 |
Commerce:BaseUrl |
this portal's public base URL | the deployment |
As environment variables they are Commerce__Stripe__SecretKey, Commerce__Stripe__WebhookSecret
and Commerce__BaseUrl.
🚨 The key names are contract. They are read out of environment variables, Key Vault secret names and helm values on every portal that sells, so renaming one is a silent deletion: a renamed key reads as absent, and absent means "this portal does not sell".
🚨 A secret's VALUE never leaves configuration. Nothing logs it, renders it or stores it on a node. The only thing derived from the secret key is its prefix, which names test-vs-live mode.
2. The webhook endpoint
Add an endpoint in the Stripe dashboard pointing at:
{Commerce:BaseUrl}/api/hooks/Store/Payments
Subscribe it to exactly these events (the list PaymentPathAudit measures against — it is
StripePaymentProvider.RequiredEvents, and a test holds it equal to the Store's runbook):
checkout.session.completed— the purchase that payscheckout.session.expired— the buyer who walked awayinvoice.paid— the monthly RENEWAL of a subscriptioncustomer.subscription.deleted— the END of a subscriptioninvoice.payment_failed— a FAILED charge: the plan goes past-due and the subscriber is told
🚨 invoice.paid and customer.subscription.deleted are what keeps a subscription alive. An endpoint not subscribed to
invoice.paid grants each plan exactly one month while Stripe keeps charging the card;
without customer.subscription.deleted a cancelled subscriber keeps access forever.
Then allowlist the target on the portal: WebhookInbox:Targets:0 = Store/Payments. The inbox is
fail-closed — without the allowlist the endpoint answers 404 and stores nothing, however correct
the Stripe side is.
Adding an event to an EXISTING endpoint keeps its signing secret — nothing changes on the portal.
The test-mode endpoint of the public instance (we_…) predates
invoice.payment_failed, so a person adds it, in TEST mode, one of two ways. No agent calls the
Stripe API with an account key.
Dashboard: Developers → Webhooks (test mode:
https://dashboard.stripe.com/test/webhooks/we_1UJssh1RJOWModnTiCCfq7R5) → ⋯ → Update details → Select events → tickinvoice.payment_failed→ Update endpoint. The endpoint's other events stay ticked.Stripe CLI, logged in to the TEST account (
stripe login; the CLI uses a restricted test key):stripe webhook_endpoints update we_1UJssh1RJOWModnTiCCfq7R5 \ -d "enabled_events[]=checkout.session.completed" \ -d "enabled_events[]=checkout.session.expired" \ -d "enabled_events[]=invoice.paid" \ -d "enabled_events[]=customer.subscription.deleted" \ -d "enabled_events[]=invoice.payment_failed"🚨
enabled_eventsis REPLACED, not appended: the call must list all five events above, or the ones it leaves out stop arriving. Read the result'senabled_eventsback and check all five are there.
3. The Customer Portal ("Manage billing")
Manage billing on the plan page opens Stripe's hosted Customer Portal for the subscriber
(POST /v1/billing_portal/sessions with the customer and a return URL — StripeGateway.OpenBillingPortal).
The session is created per click; what the portal offers is the account's portal CONFIGURATION,
which lives in the dashboard, once per mode:
- 🚨 It must be SAVED once in each mode before the first session can be created. Until then
Stripe refuses every portal session in that mode with a message saying so, and the plan page shows
that message. Open Settings → Billing → Customer portal (test mode:
https://dashboard.stripe.com/test/settings/billing/portal) and press Save; repeat in live mode before selling live. - Enable: Payment methods → update (the reason the button exists — a failed card is fixed
here), Invoice history, and Cancel subscriptions with "At the end of billing period". That
last setting is load-bearing: the plan here ends only when
customer.subscription.deletedarrives, which with this setting happens when the paid period runs out — the same rule as the plan page's own Cancel button. "Immediately" would end a paid month early. - Leave off: Switch plans (plans are sold with inline prices, not Stripe Products, so the portal has nothing to switch to) and Update quantities. Customer-information updates are harmless and optional.
- The default business information (name, privacy/terms links) is shown on the portal page; set it under Settings → Business if it is not already.
Nothing else is needed on the portal side: the secret key the checkout already uses opens the
portal, and a portal-side cancellation needs no new event — it arrives as the
customer.subscription.deleted the endpoint already carries.
4. Test and live are separate worlds
Test-mode and live-mode endpoints are configured separately at Stripe, so a live-mode endpoint
never fires for an sk_test_… checkout and vice versa. A key lists only the endpoints of its own
mode — which is what lets Store/Maintenance → PaymentPathAudit tell "no endpoint registered"
apart from "registered in the other mode".
5. Switching the public instance to live mode
Going live is a NEW key, a NEW endpoint and a NEW portal configuration — nothing from test mode carries over. The steps, in order; a person does the Stripe half, and no agent reads, writes or calls anything with a live key.
- Precondition — one payment, one period, on the RUNNING image. A plan's opening month is
granted by
checkout.session.completedalone, keyed by its checkout session;invoice.paidextends only forbilling_reason: subscription_cycle, keyed by its invoice and landing on the invoice line's period end — so any number of deliveries of either event grant exactly one period (see Subscriptions → One payment, one grant). The fix (Plugins #2406 and #2410) ships in the Store and Stripe PACKAGES, which advance independently of the portal image, so a running image proves nothing about it. The precondition is #2405's own closing check, still in TEST mode: one plan checkout on the public instance endsFulfilledwith no error and exactly one new[active]line onAdmin/Subscriptions/<buyer>. Do not replace the test secrets before that has passed — otherwise a live subscriber can be credited twice. - In the Stripe dashboard, LIVE mode (the maintainer):
- create the live secret key (Developers → API keys,
sk_live_…); - add a live webhook endpoint at the SAME URL,
https://portal.example.com/api/hooks/Store/Payments, subscribed to the SAME five events as §2 —checkout.session.completed,checkout.session.expired,invoice.paid,customer.subscription.deletedandinvoice.payment_failed— and copy its signing secret (whsec_…; a live endpoint has its own); - save the live Customer Portal configuration exactly as §3 describes (it is per mode, and until it is saved every Manage billing click is refused).
- create the live secret key (Developers → API keys,
- Enter the two values through the write-only GUI, never by hand. On the control instance
open
Deployments/<deployment>→ Set Key Vault secrets… and write<prefix>-Stripe-SecretKey(the live secret key) andmemexcloud-Stripe-WebhookSecret(the live endpoint's signing secret). The GUI writes without showing the value back; nothing else — noaz keyvault, no chat, no issue — ever carries it. - Make the portal read them. The secrets are read only at pod start, so run the governed
Reconcile and then Restart for the public instance on the control instance
—
Hosting/InstanceActionnodes the operator executes, neverkubectl. - Check the mode, then buy.
Store/Maintenance→PaymentPathAuditmust now report LIVE mode with the live endpoint registered and all five events present. Then work through the real-card checklist on #2440: one real purchase of the smallest plan,validUntilexactly one period ahead, cancel at period end, refund, and the evidence recorded on Payments.
What the module does, and deliberately does not
- Does: create hosted Checkout sessions (one-off and recurring, with inline prices), verify
every delivery's
Stripe-SignatureHMAC inside a five-minute replay window, parse the events, stop a subscription at the END of the period already paid for, and open the hosted Customer Portal for a subscriber. - Does not: decide who is entitled to what, price anything, write an order, or send anyone anywhere. Where a buyer returns to is passed in by the caller, so moving a checkout surface can never leave the payment provider redirecting at a dead URL.
Reading further
- Payments — what the Store does with a verified delivery, and how to read a
non-empty
_Rejected. - Subscriptions — the recurring lane end to end.