Customer Portal Configuration Checklist

Copy Markdown View Source

This guide documents the three Stripe Dashboard toggles every Accrue host app must enable on its Customer Billing Portal configuration before going live. All three close revenue-recovery loopholes that the default portal config leaves wide open.

Background — the "cancel without dunning" footgun

Stripe's out-of-the-box Customer Portal lets customers cancel immediately with one click and zero friction. From the host app's point of view, this turns the portal into a "cancel my account" button that bypasses every dunning workflow Accrue offers (Accrue.Billing.Dunning, [:accrue, :ops, :dunning_exhaustion], grace periods, retain-offer flows, and so on).

The portal's out-of-the-box defaults erase much of the revenue-recovery surface unless three specific toggles are flipped in the Stripe Dashboard. Flipping them is free, takes 30 seconds, and is a one-time per-mode (test + live) action.

Programmatic configuration via BillingPortal.Configuration is deferred to a future processor release. Until then this guide is the canonical install-time checklist — same convention as Pay (Rails) and Cashier (Laravel).

The three required toggles

Open the Stripe Dashboard → Settings → Billing → Customer portal in both test mode and live mode and configure the following:

1. Retain offers — ENABLED

Section: Cancellations → Retain offers.

Action: enable at least one retain offer (e.g. "50% off the next month", "switch to annual for 20% off"). Stripe presents the offer to customers who click cancel before processing the cancellation.

Why: this is the single highest-impact lever in the entire portal. Empirically, retain offers convert ~10–25% of cancellations into saves. With it disabled the portal cancels with no resistance.

2. Require cancellation reason — ENABLED

Section: Cancellations → Cancellation reason.

Action: toggle "Ask for a cancellation reason" on. Choose either "required" or "optional with reasons list" — required is strongly preferred for the survey signal.

Why: gives the host app a structured cancellation_details.reason field on the resulting customer.subscription.deleted / customer.subscription.updated webhook payload. Without this you have no churn-reason data to feed back into product / pricing / support.

3. Cancellation timing — at_period_end (NOT immediate)

Section: Cancellations → When to cancel.

Action: select "At end of billing period". Do NOT select "Immediately".

Why: with "Immediately" selected the customer is refunded prorated charges and loses access on the spot. With at_period_end the customer keeps access through the period they already paid for, the subscription transitions to cancel_at_period_end: true, and Accrue.Billing.Subscription.canceling?/1 returns true so the host app can trigger any "we're sorry to see you go" mailers, retention campaigns, or win-back flows during the grace period.

This is also the only setting that makes reversing a scheduled cancellation useful — you can't undo a subscription that has already been hard-deleted.

Verifying the checklist

After flipping all three toggles, click "Save" in the Dashboard. Stripe assigns the new configuration a bpc_* id which you can find under Settings → Billing → Customer portal → Active configuration.

To pin Accrue to that exact configuration (recommended for production so a future Dashboard edit can't silently reset the toggles), pass the id to Accrue.BillingPortal.Session.create/1:

{:ok, session} =
  Accrue.BillingPortal.Session.create(%{
    customer: current_user.customer,
    return_url: url(~p"/account"),
    configuration: "bpc_1Nx9aB2eZvKYlo2C..."
  })

If :configuration is omitted Stripe uses the account default — fine for development, risky for production because a future Dashboard edit to the default config silently affects every portal session.

Future programmatic support

When BillingPortal.Configuration lands in a future processor release, this checklist will be replaced (additively, no breaking change) with an Accrue.BillingPortal.Configuration.create/1 helper that ensures the three toggles are set in code. Until then the Dashboard checklist above is the source of truth.

See also