π³ 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 accessWhat GoShipped already handles
| Piece | Where |
|---|---|
| Checkout session | POST /api/v1/billing/checkout β Stripe Checkout (subscription mode) |
| Customer portal | POST /api/v1/billing/portal when STRIPE_CUSTOMER_PORTAL_ENABLED=true |
| Webhooks | POST /api/v1/billing/webhooks/stripe |
| Local subscription row | Updated from Stripe events (idempotent) |
| Entitlements | apps/api/app/plans/config.py |
| Settings UI | Billing tab in Settings |
| Marketing prices | Live 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.
| State | Runtime | pnpm run doctor |
|---|---|---|
Skip keys, flag still true | No Checkout. If the secret is empty, billing is treated as off. | Fails β keys required |
features.billing: false | No 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.
- Dashboard β Test mode β Product (for example
Pro) β monthly and yearly Prices (price_β¦) - 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- Customer portal: Dashboard β Settings β Billing β Customer portal β enable and save
- Forward webhooks with the Stripe CLI:
pnpm stripe:listenThat 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
| What | File |
|---|---|
| Sellable Stripe plan + Price IDs | apps/api/app/billing/config.py (pro monthly/yearly from env) |
| Quotas, model allowlist, feature entitlements | apps/api/app/plans/config.py (free / pro) |
| Marketing bullets | apps/api/app/plans/presentation.py |
| Trial length | STRIPE_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.completedcustomer.subscription.created/updated/deletedcustomer.subscription.trial_will_endinvoice.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 freeSettings β 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.