Stripe in production
Two one-time setups take billing from “works locally” to “works for real customers”: a production webhook, and the customer portal’s plan-switching feature.
1. Register the production webhook
In local dev you forward events with stripe listen. In production, Stripe
delivers them directly.
Create the endpoint
In the Stripe Dashboard → Developers → Webhooks → Add endpoint, point it at:
https://your-domain.com/api/billing/webhookSubscribe at least to customer.subscription.created, .updated, and
.deleted.
Set the signing secret
Copy the endpoint’s signing secret (whsec_…) into the STRIPE_WEBHOOK_SECRET
env var on Vercel, and redeploy.
Use live-mode keys and price ids in production, and make sure the webhook
endpoint belongs to the same Stripe account as your STRIPE_SECRET_KEY.
Mismatched accounts are the #1 cause of “payment succeeded but plan didn’t
update”.
2. Enable plan switching in the customer portal
Existing subscribers change plans in place through Stripe’s hosted portal. That requires the portal’s subscription-update feature to be on, with your paid products listed. Otherwise switching fails with:
This subscription cannot be updated because the subscription update feature
in the portal configuration is disabled.(The app catches this and returns a clear message instead of a 500 — but you still need to enable it.)
Dashboard: Settings → Billing → Customer portal → Subscriptions → turn on Customers can switch plans → add your Pro and Team products → Save.
Or via API (fill in your config id and product ids):
set -a; . ./.env.local; set +a
curl -s -X POST "https://api.stripe.com/v1/billing_portal/configurations/<bpc_default_id>" \
-u "$STRIPE_SECRET_KEY:" \
-d "features[subscription_update][enabled]=true" \
-d "features[subscription_update][proration_behavior]=create_prorations" \
-d "features[subscription_update][default_allowed_updates][]=price" \
-d "features[subscription_update][products][0][product]=<prod_pro>" \
-d "features[subscription_update][products][0][prices][]=$STRIPE_PRICE_PRO" \
-d "features[subscription_update][products][1][product]=<prod_team>" \
-d "features[subscription_update][products][1][prices][]=$STRIPE_PRICE_TEAM"Find <bpc_default_id>:
curl -s https://api.stripe.com/v1/billing_portal/configurations \
-u "$STRIPE_SECRET_KEY:"New subscriptions (free → paid via Checkout) work without this. It’s only required for existing subscribers switching between plans.