What happens when a paying customer's card expires?
Stripe retries the charge on a schedule and emails the customer. Your app receives invoice.payment_failed, which moves the subscription to past_due rather than canceling it. Access continues, because past_due is a recoverable state and most of these recover. Only customer.subscription.deleted ends entitlement.
The sequence
- The renewal charge fails. Stripe fires
invoice.payment_failed. - Stripe retries over the following days and emails the customer each time. That schedule is configurable in the dashboard and is called dunning.
- Either a retry succeeds, and
invoice.payment_succeededarrives, or Stripe gives up and cancels, which firescustomer.subscription.deleted.
The status enum in app/models/subscription.rb has past_due for exactly the middle of that, and
entitled? does not include it.
Decide what past due means for you
This is a product decision, not a technical one, and the code makes it a one-line change:
def entitled? = active? || trialing?
As written, a past due account loses access immediately. That is the strict reading, and it is defensible for a high-cost service. For most products it is too harsh: the common cause is a card that expired, the customer intends to pay, and locking them out while Stripe is still retrying turns a billing hiccup into a cancellation.
Adding past_due? to that method gives them the retry window. Whichever you choose, choose it on
purpose.
Do not build your own dunning emails
Stripe already sends them, with the card update link, on a schedule you can configure. A second set of emails from your app arrives alongside Stripe's and says the same thing less well. Enable Stripe's and spend the effort on the billing portal link instead, which is where they fix it.
Check who is currently in that state
bin/kamal app exec --interactive --reuse 'bin/rails console'
Subscription.where(status: "past_due").count
A number that only grows means customer.subscription.deleted is not enabled on your webhook
endpoint, so nothing ever resolves the state in either direction. The handler that would move it is
in app/controllers/webhooks/stripe_controller.rb.
The related failure
A customer who updates their card in the billing portal produces
customer.subscription.updated. If that event is not enabled, the payment succeeds and your app
still shows them as past due.