Lately I’ve been working on adding monthly plans to Fabrik, using Stripe to handle recurring payments.
Stripe’s API is well documented, easy to use and there are client libraries available for most popular languages/frameworks. That said, the thing I struggled with the most is how to handle failed payments.
In this post I cover what happens when a subscription payment fails in Stripe and how you can handle this within your own applications.
Creating Subscriptions
Customers can sign up for a free 14 day trial of Fabrik with no card required.
I decided not to put our trials through Stripe, at least not right away. Instead we manage trial subscriptions internally and only create the subscription in Stripe when the customer activates (purchases a plan).
A key aspect of building subscription based products is how to determine whether a customer’s subscription is valid, i.e. can they use my product right now?
Below is an example of a trial subscription in Fabrik:
{
"PlanId": "plans/1",
"CustomerId": "users/2241",
"Start": "2015-06-08T12:12:58.9741574Z",
"End": null,
"TrialEnd": "2015-06-22T12:12:58.9741574Z",
"CurrentPeriodStart": "2015-06-08T12:17:08.0000000Z",
"CurrentPeriodEnd": "2015-06-22T12:12:59.0000000Z",
"Status": "Trialing",
"ProviderId": null,
"CancellationReason": null
}
To determine whether the customer has an active subscription we check if:
subscription.CurrentPeriodEnd > now
We handle this appropriately within our customer dashboard, redirecting the customer to an upgrade page once their trial expires.
When a customer does upgrade we create the subscription in Stripe and store Stripe’s subscription identifier.
If the customer is still trialing when they upgrade, any remaining trial period is preserved by setting the trial_end
date when we create the subscription in Stripe.
Renewing Subscriptions
When a customer’s subscription is renewed in Stripe a number of things happen, each with a corresponding event:
- An invoice is created -
invoice.created
- The subscription billing period is updated -
customer.subscription.updated
- After an hour (giving you time to add any additional charges) Stripe attempts to charge the customer.
- Given payment is successful an
invoice.payment_succeeded
event is raised.
The way to handle these events within your own application is to register a webhook; a HTTP endpoint that Stripe will send details of the event to.
To renew a customer subscription we listen for the invoice.payment_succeeded
event and then do the following:
- Find the customer subscription using the Stripe identifier (included in the event payload).
- Retrieve the subscription details from the Stripe API.
- Update our subscription’s
CurrentPeriodStart
andCurrentPeriodEnd
with the Stripe subscription’speriod_start
andperiod_end
. - Create a customer invoice using the details from the Stripe event.
Handling failed payments
At some point customer payments will fail, for example, a customer’s card may have expire.
What happens at this point is largely determined by your retry settings in Stripe. Here you can control how many times Stripe will attempt to charge the customer and what the final action is:
- Cancel subscription
- Mark subscription unpaid
- Leave subscription as-is
The first scenario I want to cover is when the initial payment(s) fail but the customer updates their card details before the final payment attempt.
Scenario: Customer updates card before the final payment attempt
Let’s assume that a customer’s card has expired and their subscription is due for renewal. As I explained in the previous section, Stripe will create an invoice and then attempt to pay it using the payment source (card) it has for the customer.
We have our retry settings configured to retry payment twice:
- 1 day after the previous attempt
- 3 days after the previous attempt
So a total 5 days after the initial payment attempt.
When the payment fails for the first time the following will happen:
- If the payment failed due to the card being declined a
charge.failed
event will be raised. - The subscription will be marked as
past_due
-customer.subscription.updated
- An
invoice.payment_failed
event will be raised containing the details of the failed invoice. - The invoice will be updated to record the details of the payment attempt -
invoice.updated
. - The customer will be marked as delinquent -
customer.updated
It’s important to note that the subscription’s billing period will always be updated as this happens before the payment attempt is made. For this reason don’t use customer.subscription.updated
event to update the customer’s subscription period within your own application.
We listen for the invoice.payment_failed
event and use this to email the customer informing them that we were unable to take payment and that they should update their card details.
The invoice.payment_failed
event includes details of the next retry:
attempted: true
paid: false
attempt_count: 1
next_payment_attempt: 1433978643
You can use this to tailor your emails to your customer, letting them know when the next payment attempt will take place.
On the final attempt, next_payment_attempt
will be null
so this can be your “final warning” to the customer.
Let’s assume the customer has seen the email and updates their card details within our application. Since we don’t support multiple payment methods, each time a customer changes their card details we just replace their default payment source. This can be done by updating the customer through the Stripe API. From the docs:
If you pass the source parameter, that becomes the customer’s active source (e.g., a card) to be used for all charges in the future. When you update a customer to a new valid source: for each of the customer’s current subscriptions, if the subscription is in the past_due state, then the latest unpaid, unclosed invoice for the subscription will be retried
Also:
Passing source will create a new source object, make it the new customer default source, and delete the old customer default if one exists.
When updating the customer’s payment source, any invoices for subscriptions in a past_due
state will be paid and the following events occur:
charge.succeeded
customer.subscription.updated
- the subscription status is set back toactive
.invoice.payment_succeeded
invoice.updated
- The invoice is marked as paid and closed.customer.updated
- the customer’s deliquent flag is set back tofalse
.
Scenario: Customer updates card after the final payment attempt
After the final payment attempt the subscription can be marked as unpaid or cancelled, depending on your retry settings.
Originally I had our settings configured to mark the subscription as unpaid. After the final payment attempt an customer.subscription.updated
event will be raised with the new status.
Our webhook handler would then locate the matching subscription and put it into a suspended state:
if (stripeSubscription.Status == "unpaid")
{
var subscription = subscriptionService.GetSubscriptionByProviderId(stripeSubscription.Id);
subscription.Suspend();
subscriptionService.UpdateSubscription(subscription);
}
The problem with this approach is that the billing cycles are not changed following re-activation. From the docs:
When a subscription is in the unpaid state, we’ll still generate future invoices, though they will be closed by default. These invoices will not be attempted to be paid by our systems.
In order to set a customer’s subscription status back to “active” from an “unpaid” or “past_due” state, you will need to open their most recent invoice and successfully pay it. Paying any other invoice that is not the most recent invoice will not change the subscription’s status.
This is simple enough - we can use the API to find the latest invoice and then manually attempt to pay it.
If payment succeeds the subscription will be re-activated and the billing cycle (current_period_*
) will be unchanged. If you don’t actually de-activate your product after payment fails for the last time then this work fine for you.
Our total retry period lasts for 5 days and we provide a 5 day grace period so that the customer has time to update their payment information before we actually de-activate their site.
If the customer adds their payment information 10 days after the final payment attempt (when their subscription is marked as unpaid
in Stripe) then what we want is for their billing cycle to be restarted - after all they’ve not had use of the product for 10 days so shouldn’t be charged.
Unfortunately it’s not possible to change the billing cycle in Stripe so the only solution is to cancel the customer’s subscription and then re-subscribe to the same plan.
For this reason we changed our retry settings to Cancel subscription after the final attempt:
- After the final payment attempt the subscription is cancelled -
customer.subscription.deleted
is raised. - In our webhook for this event we mark the subscription as pending within our application and inform the customer that their site has been de-activated.
- When they update their payment information we re-subscribe the customer to their original plan thus creating a new subscription in Stripe starting from the current date.
Testing Failed Payments
To understand exactly how the subscription lifecycle works I manually created trial subscriptions within the dashboard using the card 4000 0000 0000 0341
. This card will fail when Stripe attempts to pay the first invoice.
I then updated the subscription to end the trial in a few minutes time using cURL:
curl https://api.stripe.com/v1/customers/cus_1234/subscriptions/sub_1234 \
-u sk_test_XXX: \
-d trial_end=1433928024
If you need to convert dates into UNIX timestamps you can use this site.
To test our application’s webhook handlers we store the JSON payloads in local files and load these directly in our tests.
Building SaaS products is hard!
During my time building Fabrik I’ve been compiling lots of useful examples and documentation including a complete series on building a SaaS product using Stripe. I’ve not decided yet whether to release this as a blog series, a video-cast or even a short book.
In any case, if this is the sort of thing that interests you, please join my mailing list and I’ll let you know as and when I release new content.