Setting up Stripe for hosted billing
Reading time: 8 minutes. Hands-on time: ~30 minutes (most of it inside Stripe’s dashboard, not this codebase). What you’ll have at the end: a working hosted-mode billing flow — customers can pick a plan from
/pricing, complete checkout on Stripe, get a 14-day trial, and self-serve plan changes via the Stripe Customer Portal.
This tutorial only matters if you’re running the hosted mode of Scarif One. Self-host installs (Sovereignty Package customers) don’t pay subscriptions and don’t need any of this.
Prerequisites
- A Stripe account in live mode (Test mode works for staging — same flow, different keys).
- Your business verified with Stripe (UK-based businesses need to complete the Bank Account / KYC step before going live).
- Optional but recommended: Stripe Tax enabled if you sell to EU customers. Stripe handles VAT calculation automatically.
Step 1 — Create the products
Prices below match
lib/plans.ts— the registry of record — as of 2026-08-13. If they disagree withlib/plans.tswhen you read this, the registry wins;npm run test:pricingpins the marketing site to it. There are NO gen caps on any tier (Phase 28 removed metering) — don’t copy cap language into the Stripe product descriptions.
In your Stripe Dashboard → Catalog → Products → Add product.
You’ll create five products (four recurring tiers + the one-time Sovereign licence). There is NO maintenance product to create — the live /sovereignty page promises lifetime updates with the licence, and that promise is canonical. (The sovereign_maintenance plan id in lib/plans.ts exists only to honour historically-issued maintenance subscriptions; never sell it to new buyers.)
Product 1: Scarif One — Solo
- Name: Scarif One — Solo
- Description: One brand, one seat. The whole studio, your own AI keys.
- Image: optional — upload your logo if you want it on Checkout pages
Add two prices to this product:
- Price 1 (monthly): £29.00 GBP, recurring, billed monthly. Copy the resulting
price_…ID. - Price 2 (annual): £278.00 GBP, recurring, billed yearly (20% off monthly×12). Copy the resulting
price_…ID.
Product 2: Scarif One — Deputy
- Name: Scarif One — Deputy
- Description: 2 brands, 2 seats. Scarif drafts the week and keeps the queue; you say yes.
- Two prices: £59/mo and £566/year.
- Note: production may already carry the pre-rename
STRIPE_PRICE_STUDIO_*env vars for this rung — those still resolve (they're fallbacks inPRICE_ENV_KEYS), so don't delete them if live subscriptions exist on them.
Product 3: Scarif One — Helm
- Name: Scarif One — Helm
- Description: 3 brands, 3 seats. Scarif acts inside your guardrails; API access included.
- Two prices: £99/mo and £950/year.
- ⚠ As of 2026-08-13 this product does not exist in Stripe yet (
lib/billing.tsflags it NOT YET CREATED) — creating it is part of go-live, seedocs/STRIPE-GO-LIVE.md.
Product 4: Scarif One — Agency
- Name: Scarif One — Agency
- Description: Up to 10 brands, 5 seats, client portals. For agencies.
- Two prices: £149/mo and £1,430/year.
Product 5: Scarif One — Sovereign
- Name: Scarif One — Sovereign
- Description: Self-host licence. One-time, lifetime updates, unlimited brands.
- One price: £9,999.99 GBP, one-time (not recurring). Copy the
price_…ID. - Sovereign is fit-call gated: both checkout surfaces reject
plan=sovereignwithfit_call_required, so this price is only ever used for the post-call invoice you send by hand from Stripe. It must still exist so the invoice references a real price and the webhook can attribute the purchase.
Optional Product 6: Sovereign Maintenance
- Name: Scarif One — Sovereign Maintenance
- Description: Annual updates + priority email support (year 2+).
- One price: £499.00 GBP, recurring, billed yearly. Copy the
price_…ID.
Tip: name the prices something obvious like “Helm · Monthly”, “Helm · Annual” so you can tell them apart in the Stripe dashboard later. Use Stripe’s lookup keys (
helm_monthly,helm_annual, etc.) for an extra layer of safety.
Step 2 — Get your API keys
- Developers → API keys.
- Copy the Secret key (starts with
sk_live_…orsk_test_…). Set asSTRIPE_SECRET_KEYin.env.local. - The Publishable key is not required by Scarif One — we only do server-side checkout creation.
Step 3 — Configure the webhook
Webhooks let Stripe tell us when subscriptions change so we can keep tenant.profile.subscription in sync.
- Developers → Webhooks → Add endpoint.
- Endpoint URL:
https://scarifone.com/api/billing/webhook(replace with your domain). - Events to listen to — add exactly these five:
customer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedcheckout.session.completedinvoice.payment_failed
- Click Add endpoint.
- On the endpoint detail page, click Reveal under “Signing secret”. Copy the
whsec_…value. Set asSTRIPE_WEBHOOK_SECRETin.env.local.
Step 4 — Configure the Customer Portal
The portal handles plan changes, payment-method updates, invoice downloads, and cancellations. Customers reach it from /billing → Manage subscription.
- Settings → Customer Portal.
- Functionality — enable:
- Invoice history
- Update payment method
- Cancel subscription (cancellation takes effect at end of period)
- Switch plan — toggle on, and add all eight of your subscription prices (Solo, Deputy, Helm and Agency — monthly + annual each). This lets customers self-serve upgrades/downgrades. Do NOT add the Sovereign one-time price here (it isn’t a subscription).
- Branding — upload your logo + set your brand colour (
#a31b1bif you’re using TRI’s) so the portal feels cohesive with Scarif One. - Save.
Step 5 — Set environment variables
Drop these into your .env.local:
The names below are the FIRST-preference names in PRICE_ENV_KEYS
(lib/plans.ts) — the single table both checkout and plan-resolution read.
_ANNUAL is accepted as an alias of _YEARLY, and STRIPE_PRICE_STUDIO_*
still resolves as a legacy fallback for Deputy.
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PRICE_SOLO_MONTHLY=price_...
STRIPE_PRICE_SOLO_YEARLY=price_...
STRIPE_PRICE_DEPUTY_MONTHLY=price_...
STRIPE_PRICE_DEPUTY_YEARLY=price_...
STRIPE_PRICE_HELM_MONTHLY=price_...
STRIPE_PRICE_HELM_YEARLY=price_...
STRIPE_PRICE_AGENCY_MONTHLY=price_...
STRIPE_PRICE_AGENCY_YEARLY=price_...
# One-time self-host licence (fit-call invoicing only)
STRIPE_PRICE_SOVEREIGN_ONE_TIME=price_...
# Legacy only — honours historically-issued maintenance subs; do NOT create for new sales
# STRIPE_PRICE_SOVEREIGN_MAINTENANCE_YEARLY=price_...
# Optional
STRIPE_AUTOMATIC_TAX=true
Restart your dev server (or your prod container) so the new env vars are picked up.
Step 6 — Test the flow
Testing locally with Stripe CLI
# Install if you haven't: https://stripe.com/docs/stripe-cli
stripe login
# Forward webhooks to your local dev server
stripe listen --forward-to localhost:3000/api/billing/webhook
The CLI prints a webhook signing secret — use that as STRIPE_WEBHOOK_SECRET while developing locally.
Trial flow
- Open
localhost:3000/pricingin an incognito window. - Click Start 14-day free trial on Helm. You land on
/signupwith the plan pre-selected. - Fill the signup form. Submit.
- You’re redirected to
{slug}.scarifone.com/billing/start?plan=helm&cadence=monthly&next=/setup. - That page server-side creates a Stripe Checkout Session and bounces you to Stripe.
- On Stripe Checkout, use a test card (e.g.
4242 4242 4242 4242with any future expiry and any CVC). - After payment, Stripe redirects to
/billing/successand the webhook updatestenant.profile.subscription. - Click Continue setup to finish onboarding.
Self-serve plan change
- Sign in to a tenant with a subscription.
- Open
/billing→ Manage subscription. - Stripe Customer Portal opens. Switch from Helm → Agency. Confirm.
- The webhook fires
customer.subscription.updated. Yoursubscription.priceIdupdates. The dashboard now shows Agency limits.
One-time Sovereign purchase
Sovereign is not self-serve: /api/billing/checkout and /billing/start
both reject plan=sovereign with fit_call_required and point the buyer at
/contact?topic=sovereign. The real flow is:
- Buyer books a fit call from
/sovereigntyor/pricing. - After the call, you send a Stripe invoice against the Sovereign one-time price (£9,999.99) from the Stripe dashboard.
- On payment, the webhook fires
checkout.session.completed/invoice.paidand the licence-issuance pipeline emails the signed licence file automatically (checks.licenseIssuanceon/api/healthverifies that pipeline is live).
Step 7 — Going live
- Switch your
STRIPE_SECRET_KEYfromsk_test_…tosk_live_…. - Re-create the webhook in live mode (different signing secret) and update
STRIPE_WEBHOOK_SECRET. - Re-create products + prices in live mode (test-mode IDs don’t carry over). Update each
STRIPE_PRICE_*env var. - Verify in Stripe Settings → Bank account that your payouts are routed to the correct UK account.
- Open
scarifone.com/pricingin an incognito window and run through one real-money payment to verify (you can refund yourself afterward via the Stripe dashboard).
Common issues
“No price env var set for helm:monthly” (or any tier)
The env var isn’t set or you forgot to restart the server. The error lists exactly which STRIPE_PRICE_* names it tried (they come from PRICE_ENV_KEYS in lib/plans.ts) — echo $STRIPE_PRICE_HELM_MONTHLY from your shell to verify.
Webhook returns 400 “Webhook verification failed”
The stripe-signature header doesn’t match — usually because:
- You’re using the test-mode webhook secret in production (or vice versa)
- Your reverse proxy is mangling the request body (set
body-parserto raw / use Next’s default) - The Stripe CLI is forwarding to the wrong URL — double-check the
--forward-toflag
Customer didn’t get charged but I see them in Stripe
Trial period is active — they won’t be charged for 14 days. Check subscription.status is trialing. The webhook updates this on transition to active.
Self-host install showing “Billing not enabled”
Correct — that’s by design. Self-host is a one-time purchase, no recurring billing. The /billing page shows a friendly explainer.
What this tutorial doesn’t cover (yet)
- VAT / Sales Tax automation — Stripe Tax handles this; flip
STRIPE_AUTOMATIC_TAX=trueonce you’ve enabled it in your Stripe dashboard. - Promo codes / discounts — already enabled (
allow_promotion_codes: truein checkout). Create codes in Stripe Dashboard → Coupons. - Refunds — handled in Stripe Dashboard, not Scarif One. We log incoming
invoice.payment_failedfor visibility. - Dunning — Stripe Smart Retries handle failed-payment retries automatically. Configure under Settings → Subscriptions and emails → Dunning.
Need help? Email <a href="mailto:hello@scarifone.com">hello@scarifone.com</a>.