GoShipped
Product

πŸ’³ Payments

Stripe Checkout, webhooks, and entitlements are already wired. Skip billing correctly β€” skipping keys is not turning billing off.

You don’t need to build Checkout, webhook handling, and entitlements again. Connect Stripe, map your plans, and start charging.

GoShipped already maps plans to Price IDs, starts Checkout, syncs subscriptions from webhooks, and exposes entitlements to the app. You create the Stripe product, paste keys, and decide what each plan is allowed to do.

User chooses a plan
        ↓
Stripe Checkout
        ↓
Payment
        ↓
Webhook
        ↓
GoShipped updates access

What GoShipped already handles

PieceWhere
Checkout sessionPOST /api/v1/billing/checkout β†’ Stripe Checkout (subscription mode)
Customer portalPOST /api/v1/billing/portal when STRIPE_CUSTOMER_PORTAL_ENABLED=true
WebhooksPOST /api/v1/billing/webhooks/stripe
Local subscription rowUpdated from Stripe events (idempotent)
Entitlementsapps/api/app/plans/config.py
Settings UIBilling tab in Settings
Marketing pricesLive GET /api/v1/billing/plans (see Landing Page)

The frontend has no Stripe keys. Only apps/api/.env talks to Stripe.

Checkout returns to {FRONTEND_URL}/settings/billing?checkout=success or canceled.

Skip vs disable

Skipping Stripe β‰  billing off

Billing is on in starter.config.json (features.billing: true).

If you skip Stripe in pnpm run setup and leave that flag true, the app still boots, but doctor requires Stripe keys. Checkout is unavailable until they exist.

Not using Stripe yet? Turn billing off and move on: set features.billing to false.

StateRuntimepnpm run doctor
Skip keys, flag still trueNo Checkout. If the secret is empty, billing is treated as off.Fails β€” keys required
features.billing: falseNo checkout, portal, or billing settings. Webhooks return 200 and ignore.Billing group skipped

When billing is disabled, every user gets DEFAULT_PLAN_WHEN_BILLING_DISABLED (default pro, set in apps/api/.env). That is not a β€œfree” plan. Change the env value if you want free instead.

Plans still exist when billing is off. You are choosing the default entitlement, not deleting the catalog.

What you configure

1. Feature flag

"features": { "billing": true }

Then pnpm config:sync / keep pnpm dev running.

2. Stripe (Test Mode first)

Stay in Test mode until Checkout works locally.

  1. Dashboard β†’ Test mode β†’ Product (for example Pro) β†’ monthly and yearly Prices (price_…)
  2. Paste API keys into apps/api/.env:
STRIPE_SECRET_KEY=sk_test_...
STRIPE_PRICE_PRO_MONTHLY=price_...
STRIPE_PRICE_PRO_YEARLY=price_...
STRIPE_TRIAL_DAYS=14
STRIPE_CUSTOMER_PORTAL_ENABLED=true
STRIPE_WEBHOOK_SECRET=whsec_...
FRONTEND_URL=https://localhost:3000
  1. Customer portal: Dashboard β†’ Settings β†’ Billing β†’ Customer portal β†’ enable and save
  2. Forward webhooks with the Stripe CLI:
pnpm stripe:listen

That is stripe listen --forward-to https://localhost:8000/api/v1/billing/webhooks/stripe --skip-verify. Paste the printed whsec_… into .env and restart the API.

Test card: 4242 4242 4242 4242.

3. Plans vs prices

WhatFile
Sellable Stripe plan + Price IDsapps/api/app/billing/config.py (pro monthly/yearly from env)
Quotas, model allowlist, feature entitlementsapps/api/app/plans/config.py (free / pro)
Marketing bulletsapps/api/app/plans/presentation.py
Trial lengthSTRIPE_TRIAL_DAYS (0 = off)

Dollar amounts are not duplicated in env. Checkout and the pricing UI read Stripe Price objects.

Shipped Pro entitlements include higher chat quotas, Deep Research, MCP, and a wider model list. Edit plans/config.py for your product β€” do not leave demo limits if they are wrong.

Webhooks

Endpoint: https://<api-host>/api/v1/billing/webhooks/stripe

Handled events:

  • checkout.session.completed
  • customer.subscription.created / updated / deleted
  • customer.subscription.trial_will_end
  • invoice.paid / invoice.payment_failed

Unknown types return 200. Signature check uses STRIPE_WEBHOOK_SECRET (no user JWT). Events are stored so retries do not apply twice.

Test vs Live

Never mix sk_test_… with live Price IDs or a Live webhook secret. The deploy wizard does not copy Test keys to Production. Create Live Prices and a Production webhook after the API URL exists. Local stripe listen secrets stay local.

Doctor: when billing is on, secret + both Price IDs + FRONTEND_URL are required. A missing webhook secret is a warning locally (you can still exit 0). Production Doctor treats a missing Production webhook the same way β€” warn + a manual task.

Access after payment

DEV_PLAN_OVERRIDE          (non-production only)
        ↓
billing disabled?          DEFAULT_PLAN_WHEN_BILLING_DISABLED
        ↓
subscription row           active / trialing / past_due β†’ plan_key
        ↓
otherwise                  free

Settings β†’ Billing is where users upgrade, open the portal, and see status.

Production

When features.billing is on, deploy asks for Live keys and can help register the Production webhook. You still confirm the endpoint in the Stripe dashboard.

If you are not selling yet, set features.billing to false before you run doctor or deploy. You can turn it on later without rewriting Checkout.

On this page