DocsSetting up Stripe for hosted billing
▸ Tutorial 14

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

  1. A Stripe account in live mode (Test mode works for staging — same flow, different keys).
  2. Your business verified with Stripe (UK-based businesses need to complete the Bank Account / KYC step before going live).
  3. 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 with lib/plans.ts when you read this, the registry wins; npm run test:pricing pins 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 → CatalogProductsAdd 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 in PRICE_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.ts flags it NOT YET CREATED) — creating it is part of go-live, see docs/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=sovereign with fit_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

  1. DevelopersAPI keys.
  2. Copy the Secret key (starts with sk_live_… or sk_test_…). Set as STRIPE_SECRET_KEY in .env.local.
  3. 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.

  1. DevelopersWebhooksAdd endpoint.
  2. Endpoint URL: https://scarifone.com/api/billing/webhook (replace with your domain).
  3. Events to listen to — add exactly these five:
    • customer.subscription.created
    • customer.subscription.updated
    • customer.subscription.deleted
    • checkout.session.completed
    • invoice.payment_failed
  4. Click Add endpoint.
  5. On the endpoint detail page, click Reveal under “Signing secret”. Copy the whsec_… value. Set as STRIPE_WEBHOOK_SECRET in .env.local.

Step 4 — Configure the Customer Portal

The portal handles plan changes, payment-method updates, invoice downloads, and cancellations. Customers reach it from /billingManage subscription.

  1. SettingsCustomer Portal.
  2. 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).
  3. Branding — upload your logo + set your brand colour (#a31b1b if you’re using TRI’s) so the portal feels cohesive with Scarif One.
  4. 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

  1. Open localhost:3000/pricing in an incognito window.
  2. Click Start 14-day free trial on Helm. You land on /signup with the plan pre-selected.
  3. Fill the signup form. Submit.
  4. You’re redirected to {slug}.scarifone.com/billing/start?plan=helm&cadence=monthly&next=/setup.
  5. That page server-side creates a Stripe Checkout Session and bounces you to Stripe.
  6. On Stripe Checkout, use a test card (e.g. 4242 4242 4242 4242 with any future expiry and any CVC).
  7. After payment, Stripe redirects to /billing/success and the webhook updates tenant.profile.subscription.
  8. Click Continue setup to finish onboarding.

Self-serve plan change

  1. Sign in to a tenant with a subscription.
  2. Open /billingManage subscription.
  3. Stripe Customer Portal opens. Switch from Helm → Agency. Confirm.
  4. The webhook fires customer.subscription.updated. Your subscription.priceId updates. 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:

  1. Buyer books a fit call from /sovereignty or /pricing.
  2. After the call, you send a Stripe invoice against the Sovereign one-time price (£9,999.99) from the Stripe dashboard.
  3. On payment, the webhook fires checkout.session.completed / invoice.paid and the licence-issuance pipeline emails the signed licence file automatically (checks.licenseIssuance on /api/health verifies that pipeline is live).

Step 7 — Going live

  1. Switch your STRIPE_SECRET_KEY from sk_test_… to sk_live_….
  2. Re-create the webhook in live mode (different signing secret) and update STRIPE_WEBHOOK_SECRET.
  3. Re-create products + prices in live mode (test-mode IDs don’t carry over). Update each STRIPE_PRICE_* env var.
  4. Verify in Stripe SettingsBank account that your payouts are routed to the correct UK account.
  5. Open scarifone.com/pricing in 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:

  1. You’re using the test-mode webhook secret in production (or vice versa)
  2. Your reverse proxy is mangling the request body (set body-parser to raw / use Next’s default)
  3. The Stripe CLI is forwarding to the wrong URL — double-check the --forward-to flag

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=true once you’ve enabled it in your Stripe dashboard.
  • Promo codes / discounts — already enabled (allow_promotion_codes: true in checkout). Create codes in Stripe Dashboard → Coupons.
  • Refunds — handled in Stripe Dashboard, not Scarif One. We log incoming invoice.payment_failed for visibility.
  • Dunning — Stripe Smart Retries handle failed-payment retries automatically. Configure under SettingsSubscriptions and emailsDunning.

Need help? Email <a href="mailto:[email protected]">[email protected]</a>.