Make the connector do something on the page that sells it, and read its mode off the artifact rather than declaring it
Decided
Three things, and the middle one is the load-bearing one.
Affects: lib/integrations.ts (the stripe-billing entry, rewritten), config/site.ts, lib/careers.ts, app/integrations/page.tsx (metadata and a new #billing-sandbox section), app/api/billing/checkout-session/route.ts (new), components/integrations/billing-runner.tsx (new), scripts/check-api-reference.mjs (a third host declaration, plus every path the new route calls).
- 1.
/integrationscreates a real Stripe Checkout Session on request, through a server route holding the sandbox credential, and prints the object it gets back — session id, amount, tax-inclusive total, quote number, expiry, and the hosted URL. - 2.Which Stripe mode the deployment runs in is derived from the session id prefix —
cs_test_orcs_live_— and from nothing else. No environment variable is read, and no sentence on the page declares it. - 3.The hosted URL is released to the browser only when that prefix says test.
liveand anything unrecognised both withhold it.
What produced it. A review of task 6 said: "Stripe is listed as an integration, but no visible test-mode billing flow." It listed three gaps: no visible test-mode flow, no visible invoice or subscription objects, no explicit billing sandbox section.
Two of those three were fair. The site had a field-mapping table, a maturity mark of "Generally available", and a readiness badge fed by GET /api/connectors — all of which are the site talking about itself. The end-to-end flow has existed and worked since task 7; a reader had no way to reach it. That is the fourth instance of the same defect D-069 through D-072 each record, and the fix is the same shape every time: stop describing the artifact, hand it over.
The larger thing the review only brushed. Chasing the finding meant reading backend/src/integrations/stripe/* against what the catalogue entry claimed, and they did not match. The entry advertised Subscription, Payout and Refund objects, "a nightly payout reconciliation", and settlement by "Canadian pre-authorised debit, and ACH where relevant". None of that is built. The adapter calls Customers, InvoiceItems, Invoices, invoice finalize and Checkout Sessions. There is no cron in backend/vercel.json and no backend/src/jobs. And checkout.ts sets payment_method_types[0]=card explicitly, with a comment saying why: an account with acss_debit enabled would otherwise offer pre-authorised debit on a page this code believes settles instantly, and the quote would sit unpaid for days with no error anywhere.
So the page was selling the opposite of what the code deliberately does, and the comment explaining the deliberate choice sat two files away where no buyer would read it. The reviewer went looking for the subscription objects the table promised, found none, and concluded the integration was unfinished. They were wrong about the integration and right about the table.
Corrected in all four places it was stated — the catalogue entry, the marquee tile in config/site.ts, a job description in lib/careers.ts, and the /integrations metadata description. Fixing it only where it was argued and leaving it where it was merely mentioned is how an overclaim survives a correction.
The entry now also says out loud what is *not* built: subscriptions, payouts and refunds, with the sentence "a refund today is issued in the Stripe dashboard and reconciled by hand." Not built is a smaller problem than claimed and absent. The second is the one that costs a deal at diligence.
Why the mode is derived and not published. The obvious alternative was to add a field to GET /api/connectors reporting the mode from STRIPE_SECRET_KEY and render that. It needs a backend change and a redeploy, and it is a second statement of a fact that can drift from the first. The session id prefix is generated by Stripe, travels with the object, and is present in the same response the page is already rendering. It is the same rule as everything else here — derive the claim from the artifact — and here the artifact was already in hand.
It doubles as the safety gate, which is the part worth keeping. The reason to withhold a cs_live_ URL is that a marketing page handing out a live payment page is a way for a stranger to be charged for a demo. Gating on the prefix means the check and the claim are the same expression: the page cannot display "test mode" while offering a live link, because the string that decides one decides the other.
Why the route accepts no input at all. The body is ignored. Not validated — ignored. A return URL, an amount, a quote id or a customer email would each let an anonymous caller steer what gets created inside our tenant, and the panel needs none of them. The demo configuration is fixed and drawn from lib/sandbox-fixtures.ts, so the SKUs it posts cannot drift from the ones the sandbox tenant was seeded with — check:api-reference already asserts that.
Why it does not run on mount, unlike the CPQ runner on `/developers`. That one calculates and writes nothing, so firing it on mount is free and makes the panel prove itself before anyone touches it. This one approves a quote, which is the event that finalises a real invoice, and then creates a real payment page. A page should not do that because somebody scrolled past it, and a crawler should not be able to do it by fetching a URL. So it needs a press.
Two failure modes that were designed for rather than discovered. The route tolerates 409 on approve, because "the quote is already in the state you asked for" is the outcome the call wanted. And it tolerates 409 on checkout-session, which means the quote it found has already been paid — which happens exactly when a visitor followed the link and completed it with a test card. Without a retry on a fresh quote, the sandbox would work until the first person actually used it and then be permanently broken by its own success.
What would make this wrong. If the deployment ever legitimately runs Checkout in live mode for a reason we want visible, the page goes quiet with a warning rather than adapting, and that is deliberate. If subscriptions or refunds get built, the catalogue entry has to change in the same commit as the code — the entry is the thing this decision is about, and it earned its paragraph by being wrong for months.