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 > 0Start {N}-day trial
    • Free → Stay on Free
    • Any paid plan → Start free trial

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:

  1. Requires a price_id param.
  2. 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
  3. On success: external redirect to session.url (Stripe-hosted checkout page).
  4. 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:

  1. Calls Billing.create_portal_session(account, return_url) with return_url = {endpoint}/app/billing.
  2. On success: external redirect to session.url (Stripe-hosted customer portal).
  3. 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_end is set (formatted via toLocaleDateString)

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.