Building Subscription Cancellations with Stripe in 3 simple steps

July 22, 2019

One important feature to add when building subscriptions is allowing people cancel. And easily. You don't want people having to contact you to cancel their subscription. Or worst still, issue a charge back once billed. So how do you do this using Stripe?

1. Add a cancel subscription button/link

The first step is to add a "cancel subscription" link or button to a very visible area of the billing or subscription page. Some sites hide this in obscurity. Some hide it many steps away. Don't do that. It is a dark pattern. You can add an additional confirmation step; a confirmation modal for example; just to be sure it wasn't clicked by mistake.

2. Tell Stripe to cancel at the end of billing cycle

Once the user confirms "Cancellation", make a request to Stripe's API on the server side. Note that the API we are calling here is to update the subscription and set it to cancel at the end of the billing period. We are doing this because even though the customer has clicked "cancel" s/he should still enjoy the full subscription features till the end of the billing period.

const stripe = require("stripe")(process.env.STRIPE_KEY)
;

async function cancelSubscription(user) {
  try {
    // todo: Get user subscription id as saved in db when subscribed
    return await stripe.subscriptions.update(user_subscription_id, {
      cancel_at_period_end: true
    });
  }
  catch(e) {
    throw e;
  }
}

So we really haven't cancelled the user's subscription yet. We've only told Stripe to do so when it's the end of the active subscription. But how do we know when Stripe does so we can update our own records and mark the subscription as cancelled? Webhooks.

3. Set a webhook to update records once cancelled

Finally, we setup a webhook that Stripe will send notification to once the subscription is automatically cancelled at the end of the billing period. The webhook is simply a script Stripe sends the subscription and customer details to. To setup a webhook, go to your Stripe dashboard, click on DevelopersWebhooksAdd endpoint (Endpoints receiving events from your account). Add an absolute URL to the script that will handle the event and select the event you want to receive: customer.subscription.deleted.

Here is what the event Stripe sends to the webhook (POST) looks like.

{
  "id": "evt_xxxxxxxxxxxxxxxx",
  "object": "event",
  "account": "acct_xxxxxxxxxxxxxxxx",
  "api_version": "2019-05-16",
  "created": 1562758323,
  "data": {
    "object": {
      "id": "sub_xxxxxxxxxxxxxx",
      "object": "subscription",
      "application_fee_percent": null,
      "billing": "charge_automatically",
      "billing_cycle_anchor": 1435228277,
      "billing_thresholds": null,
      "cancel_at": null,
      "cancel_at_period_end": false,
      "canceled_at": 1562758323,
      "collection_method": "charge_automatically",
      "created": 1435228277,
      "current_period_end": 1593081077,
      "current_period_start": 1561458677,
      "customer": "cus_xxxxxxxxxxxxxx",
      "days_until_due": null,
      "default_payment_method": null,
      "default_source": null,
      "default_tax_rates": [
      ],
      "discount": null,
      "ended_at": 1562758323,
      "items": {
        "object": "list",
        "data": [
          {
            "id": "si_xxxxxxxxxxxxxxxxxxxxxxx",
            "object": "subscription_item",
            "billing_thresholds": null,
            "created": 1435228277,
            "metadata": {
            },
            "plan": {
              "id": "yearly",
              "object": "plan",
              "active": true,
              "aggregate_usage": null,
              "amount": 1499,
              "billing_scheme": "per_unit",
              "created": 1434390436,
              "currency": "usd",
              "interval": "year",
              "interval_count": 1,
              "livemode": false,
              "metadata": {
              },
              "nickname": null,
              "product": "prod_xxxxxxxxxxxxxx",
              "tiers": null,
              "tiers_mode": null,
              "transform_usage": null,
              "trial_period_days": null,
              "usage_type": "licensed"
            },
            "quantity": 1,
            "subscription": "sub_xxxxxxxxxxxxxx"
          }
        ],
        "has_more": false,
        "total_count": 1,
        "url": "/v1/subscription_items?subscription=sub_xxxxxxxxxxxxxx"
      },
      "latest_invoice": "in_xxxxxxxxxxxxxxxxxxxxxxx",
      "livemode": false,
      "metadata": {
      },
      "plan": {
        "id": "yearly",
        "object": "plan",
        "active": true,
        "aggregate_usage": null,
        "amount": 1499,
        "billing_scheme": "per_unit",
        "created": 1434390436,
        "currency": "usd",
        "interval": "year",
        "interval_count": 1,
        "livemode": false,
        "metadata": {
        },
        "nickname": null,
        "product": "prod_xxxxxxxxxxxxxx",
        "tiers": null,
        "tiers_mode": null,
        "transform_usage": null,
        "trial_period_days": null,
        "usage_type": "licensed"
      },
      "quantity": 1,
      "schedule": null,
      "start": 1435228277,
      "start_date": 1435228277,
      "status": "canceled",
      "tax_percent": null,
      "trial_end": null,
      "trial_start": null
    }
  },
  "livemode": false,
  "pending_webhooks": 1,
  "request": {
    "id": null,
    "idempotency_key": null
  },
  "type": "customer.subscription.deleted"
}

Since we know what Stripe's event data looks like, we can easily build our script. The script only needs to update the user's billing record and remove access to paid features. Here is a boilerplate Nodejs script.

const stripe = require("stripe")(process.env.STRIPE_KEY)
, app = require('express')()
, bodyParser = require('body-parser')
;

app.post('/webhook',
 bodyParser.raw({type: 'application/json'}), async (req, res) => {
  try {
    // Confirm events are actually from Stripe [url]
    const signature = req.headers['stripe-signature'];
    const event = stripe.webhooks.constructEvent(req.body, signature,
                   process.env.STRIPE_SIGNKEY);
    // Only production events allowed [url]
    if (!event.livemode)
      return res.end();
    // The event type has to be customer.subscription.deleted
    if (event.type != 'customer.subscription.deleted')
      return res.end();

    // todo: Update db and set customer (event.data.object.customer)
    //       subscription to canceled
    // todo: remove access to paid features

    res.end();
  }
  catch (e){
    // todo: log error
    res.end();
  }
}

And that's it. Simple steps right?

Plover allows you send automated emails to your customers when there is a Stripe payment event. Subscribe to the newsletter for blog and product updates.