Managing your subscription
How the Billing page connects to Stripe — checkout, customer portal, and the button labels on each plan card.
Subscription management happens through two endpoints that proxy to Stripe: /app/billing/checkout and /app/billing/portal. The buttons on the plan cards and the top-right Manage subscription button are wired to these.
Plan-card buttons (verified from PlanCard in Plan/Index.jsx)
Each plan card renders a button whose label depends on state:
- If it's your current plan: a disabled-looking pill labeled Current plan (no action).
-
With an existing subscription, clicking a non-current card:
- Free → Downgrade
- Any paid plan → Switch to {plan.name}
-
Without a subscription:
-
Plan has
trial_period_days > 0→ Start {N}-day trial - Free → Stay on Free
- Any paid plan → Start free trial
-
Plan has
All clickable buttons fire:
router.post("/app/billing/checkout", { price_id: plan.stripe_price_id })
Variant: Free uses an outline button, everything else uses the filled default variant.
Checkout endpoint
Verified from CheckoutController.create/2:
-
Requires a
price_idparam. -
Calls
Billing.create_checkout_session(account, price_id, success_url, cancel_url)where:-
success_url =
{endpoint}/app/billing?status=success -
cancel_url =
{endpoint}/app/billing?status=canceled
-
success_url =
-
On success: external redirect to
session.url(Stripe-hosted checkout page). -
On error: flash "Could not create checkout session.", redirect to
/app/billing.
After Stripe redirects back, the billing page receives ?status=success or ?status=canceled in the URL — the JSX doesn't special-case these today beyond letting Stripe's side handle the flow.
Manage subscription button
Top-right of /app/billing. Verified from Plan/Index.jsx: shown only when subscription is present. Clicking fires:
router.post("/app/billing/portal")
Portal endpoint
Verified from PortalController.create/2:
-
Calls
Billing.create_portal_session(account, return_url)with return_url ={endpoint}/app/billing. -
On success: external redirect to
session.url(Stripe-hosted customer portal). -
On error: flash "Could not open billing portal.", redirect to
/app/billing.
Inside Stripe's portal you can update payment methods, download invoices, change plan, and cancel — whatever your Stripe account exposes.
Current-plan strip
Above the plan grid, verified from CurrentPlanStrip:
-
No
subscription→ neutral strip with "You're on Free. Upgrade anytime for more chatbots, messages, and AI tools." -
With
subscription:- "Current plan: {currentPlanName}" (plan name in primary color)
-
Amber Free trial pill if
subscription.status === "trialing" -
"Renews {Mon D, YYYY}" if
subscription.current_period_endis set (formatted viatoLocaleDateString)
currentPlanName resolves as subscription?.plan_name || plan?.name || "Free".
Trial copy
The top of the page displays (verbatim):
Every paid plan starts with a 7-day free trial. Cancel anytime, no questions asked.
In the trust row further down, the same trial length is reinforced:
7-day free trial — Every paid plan includes a full week of access — no charge until day 8.
The actual button label uses plan.trial_period_days, not a hardcoded 7:
-
If
plan.trial_period_days > 0: Start {N}-day trial - Otherwise: Start free trial (no number in the label)
So the 7-day marketing copy is static; the specific number shown on the button comes from the plan's trial_period_days field at runtime.
What this page doesn't do
- There's no in-dashboard cancel button — use Manage subscription and cancel in the Stripe customer portal.
- There's no direct way to change payment method here — same portal flow.
- There's no resend-invoice or receipts section here — invoices live in Stripe.
Related
- Plans & pricing — per-plan numbers and features
- Usage dashboard — see how close you are to your limits