Implementing API billing with Stripe

Writing new billing and payments code with Stripe for our video call API

# Intro — the job to be done

Our video calling API is a self-serve product with monthly subscription billing. We've used Stripe for credit card processing since we started our company, in 2016. But when we were planning the launch of a new API product, we realized that the requirements were different enough from what we'd done before that we'd need to write new billing and payments code.

This promised to be a fun exercise! Stripe's own API design, documentation, and systems infrastructure are the gold standard that all of us who make API-based products aspire to. So writing Stripe-related code is always a pleasure and a learning experience.

In addition, we knew that Stripe had added quite a lot of functionality to [Stripe Billing](https://stripe.com/us/billing?&utm_campaign=paid_billing&utm_network=g&utm_medium=cpc&utm_source=google&ad_content=269766573782&utm_term=stripe%20billing&utm_matchtype=e&utm_adposition1t1&utm_device=c&gclid=Cj0KCQiAk-7jBRD9ARIsAEy8mh4KSm68RSau6kQlxUYg03p_F2u5ZEdsmeGo1IAM1p9hvogh-FkvoR0aAhlHEALw_wcB) (Stripe's suite of subscriptions and recurring revenue features) since we'd last had a chance to update our payments code.

We start out most projects by writing up a quick [jobs to be done](https://www.intercom.com/books/jobs-to-be-done) outline. Here's what our new billing and payments code would need to do:

- Give our users an easy way to sign-up for (and cancel) subscriptions to our API products
- Keep track of who is subscribed to what, and how much usage they've accrued, so we can do respond correctly to each API call, and set up the right functionality and UI for each video call
- Send monthly invoices, which include charges for several different metered usage line items, and appropriately handle payment and invoice overdue events

Our dashboard, where users can subscribe to an API plan

# A data model

If we start a project with a jobs-to-be-done outline, we start *development* with a data model. Or, put a different way, until we have a data model we're confident in, we're just prototyping, not implementing!

For this project, the data model needs to translate between our representation of customers, subscriptions, and settings and those same concepts in Stripe's APIs.

So we needed to dig in to understand how Stripe's [recurring billing features](https://stripe.com/docs/billing/subscriptions/modeling) are designed.

## Stripe customers, products, plans, subscriptions, and invoices

Almost all recurring billing tasks, in Stripe's systems, are built on top of the following five top-level abstractions:

- A [customer record](https://stripe.com/docs/api/customers) is an account with a unique id, an email address, and (usually) a stored payment method.
- A [product](https://stripe.com/docs/api/service_products) is something that you charge a customer for. It's useful to think of a product as mapping directly to a line item on an invoice.
- A [plan](https://stripe.com/docs/api/plans) is what you charge for a specific product (how much, and how the amount is calculated). You can create several different plans for each product. For example, you might have different plans for customers who are paying in different currencies.
- A [subscription](https://stripe.com/docs/api/subscriptions) is an association between a customer record, and one or more plans. A subscription generates recurring invoices.
- An [invoice](https://stripe.com/docs/api/invoices) generated by a subscription (usually) attempts to charge a customer's credit card, and Stripe [events/webhooks](https://stripe.com/docs/billing/webhooks) are created to help you track and respond to pre- and post-payment events.

## Daily.co users, teams, and API plans

In our world, we have users, teams, and plans.

- A user is someone with an email address who has created a [Daily.co account](http://daily.co).
- A team is a Daily.co subdomain (for example, `your-team.daily.co`). All video call rooms are created under a team subdomain, and multiple users can share access to a team.
- An API plan is a paid tier of service, billed monthly, that allows a **team** to use our API calls to create, configure, and use rooms, recordings, and other Daily.co features. (Note that what we call a "plan" is more like what Stripe calls a "subscription" than what Stripe calls a "plan!")

## Mapping between two systems

A good first question to ask about a data model is, "where does the data live?"

In general, we think it's a good idea to make Stripe the "one true source" for as much of your customer and billing data as possible. Stripe's systems are proven, secure, and well designed. Stripe's interactive dashboards are very helpful for viewing your payments data, and let you do things like modify customer subscriptions and issue refunds (so you don't have to write code for every corner case you'll need to handle).

### A platonically simple (but totally workable) recurring billing implementation

Often, a recurring billing data model can look like this:

- Your system
   - User account ←→ Stripe customer id
   - A Stripe plan id for each thing a customer can subscribe to
- In Stripe
   - Customer data
   - Credit card data
   - Subscriptions data
   - Invoices and payments history

All you need to keep track of, in your database, is a Stripe customer ID for each of your user accounts. Then, somewhere in your code, you also hardcode a Stripe plan id for each product that your customers can subscribe to. And you're done. (Well, done with the data model. You still have a little bit of Stripe API code to write!)

The user lifecycle looks like this:

- New subscription
   1. Use the [Stripe Elements](https://stripe.com/payments/elements) UI library to get credit card information and then [create a Stripe customer](https://stripe.com/docs/api/customers/create) with the token provided by the library. (The credit card data goes straight to Stripe, so you don't have to worry about any of the many, many, security and compliance issues that come with handling credit card data.)
   2. Look up the Stripe plan id in your code, and [create a Stripe subscription](https://stripe.com/docs/api/subscriptions/create) that sets this customer up on the plan. If you need to store extra information about the subscription, you can embed arbitrary `metadata` in the Stripe subscription object.
- Look up subscription data for a user (to verify access to features, or to display info to the user)
   1. Using the stored Stripe customer ID, [list the subscriptions](https://stripe.com/docs/api/subscriptions/list) for the customer. The subscription will include information about billing, such as payment status. You can also use any metadata you embedded in the subscription object.
- Cancel a subscription
   1. Using the stored Stripe customer ID, [list the subscriptions](https://stripe.com/docs/api/subscriptions/list) for the customer.
   2. Find the subscription you need to cancel (if there's more than one, you can filter by which plans the subscription includes, or by any metadata you've embedded in the subscription object). [Cancel the subscription](https://stripe.com/docs/api/subscriptions/cancel), optionally specifying a cancellation date and whether or not to prorate unused time.

That's it! You just need to write five or six back-end functions, add Stripe Elements to your UI, and you have a fully functioning recurring billing setup. (You'll need to use the Stripe dashboard to keep track of customers with credit cards that expire, or who have invoices that fail to automatically charge for any other reason. We didn't talk at all, above, about webhooks, handling payment events, or updating credit card info. But if you're just launching a product, that's probably okay — you can add webhooks and credit card update forms later.)

### A little more complexity

We prototyped our recurring billing system, initially, so that it looked exactly like the one described above. Then we added a bit more information and logic in our database and code, based on a couple of additional requirements we have.

We track several kinds of usage on a per-minute basis. Our free API tier is limited to 2,000 video call minutes per month. Our paid tiers have a monthly subscription cost that includes a certain number of video call minutes, plus per-minute charges for additional basic call usage, for large calls, and for recordings. This means we need real-time access to usage data in order to gate functionality based on accrued usage.

We also need to store configuration information about each video call room. A team can have thousands of rooms. When we check whether someone can join a room, and what features to enable in our in-call UI, we have to look at combined room configuration and team configuration fields. Because we already have this code written, and we need access to "plan" information every time we check room access, we added two subscription-related properties to the team configuration info we store in our database: `api_plan_id` and `api_plan_expires`.

### Parts of our db schema that relate to billing

- StripeCustomers table
   - Team id — the Daily.co team the Stripe customer record is associated with
   - Stripe customer id
- Team properties table
   - Team id
   - Api_plan_id — which of our API plans (not Stripe plans) the user is subscribed to
   - Api_plan_expires — when the plan will expire, set by our code that handles the Stripe `invoice.payment_succeeded` webhook
- Several events tables that track usage
   - Team id
   - Event properties
   - Event start time
   - Event end time

## Metadata stored in the Stripe subscriptions object

Every Stripe subscription that we create includes two metadata fields, `api_plan_id` and `team_name`. We use `api_plan_id` to filter the subscriptions list for customers that have multiple subscriptions with us. (We have other products in addition to our API.) We use the `team_name` field as a convenient way to know what customer we're looking at, when we're using the Stripe dashboard.

### Billing for usage

Stripe has a suite of [metered billing](https://stripe.com/docs/billing/subscriptions/metered-billing) features that are quite powerful. You can send [usage data](https://stripe.com/docs/api/usage_records) to Stripe, and Stripe can automatically calculate usage-based costs on each invoice. Advanced calculations like tiered pricing are supported, too!

We initially planned to use these metered billing features to handle invoicing for our per-minute usage charges. We ran into three issues, though.

1. A Stripe unit cost must be an even number of cents. Our per-minute prices aren't even number of cents. So to use Stripe's metered billing features we would have to charge for blocks of minutes (10 minutes, or 100 minutes, or an hour).
2. Usage data needs to be sent to Stripe continuously. There's no way to submit usage data to Stripe after the end of a billing period.

    Much of our usage data is highly granular (video call minutes). One obvious approach would be to submit a usage record to Stripe when a client exits a call. This would create a very large number of usage records, though, and would inevitably miss some usage. (Sometimes people exit calls by, for example, closing the lid on the laptop they are using!) At the same time, though, some of our video calls are very, very long-running. Lots of people use our systems for always-on video portals. An always-on user can stay connected to a video call for weeks!

    Another approach would be to run a cron job to regularly total up usage and submit usage records. This would require that we either drop, or roll over, usage between the time of the last cron job and the time of the monthly invoice. We'd also need to handle some slightly tricky corner cases (cache status info from previous cron jobs, re-run cron jobs that fail, etc).

3. Using metered billing plans makes changing customer subscriptions more complicated. Stripe requires that you delete usage records before removing metered plan items from a subscription. There's no automatic way to roll those usage charges over to the following invoice (which would be analogous to the proration option that's available for non-metered plans).

We could certainly have worked around all of these. And we did start down that road. But in the end we decided that calculating usage-based costs ourselves would, for our use case, result in cleaner and easier to maintain code.

Stripe sends an [invoice.created](https://stripe.com/docs/billing/lifecycle#invoice-lifecycle) webhook event for each subscription upon the close of each billing period. Our handler for that webhook runs a usage query on our database, then adds line items to the invoice for each usage-based charge.

The only small thing we're not able to do is show each line's usage number in the **Qty** column, the way that a natively generated Stripe metered billing item would. Instead, we include the units used in the item description.

Sample invoice line items. All the usage-based items are "Qty 1", with the actual usage numbers included in the item description text

# Example code

## API Plan definitions

Each of our API plans is defined by an entry in a `plans` object, in a top-level utility file called `PlansAndConfigs.js`. Both our front-end and back-end code include parts of this file.

You can view the entire [plans object literal](https://gist.github.com/kwindla/c93ccda41d5143daa9dda3027a63ae3a), if you'd like to see the full structure. The parts that relate to Stripe are `stripePlans`, and `usage`.

The `stripePlans` property is a list of Stripe plan ids that make up a subscription to our API plan. We define these plans in the Stripe dashboard, and we force the Stripe plan id to be the same in both our test and production Stripe data.

The `usage` property is a list of objects that are used to add metered usage line items to each monthly invoice.

- The `field` property is a key in the usage records returned by our internal usage query functions
- The `description` property is the first part of the line item description for the invoice
- The `costf` property is a function that takes a number of minutes of usage as an argument, and returns a cost, in dollars.

## Creating a customer

We use the [react-stripe-elements](https://stripe.com/docs/recipes/elements-react) package to embed credit card data entry forms into our user interface. Here's what the (customized) form looks like in a modal that's part of the Daily.co dashboard.

The Elements machinery creates an opaque token that we can use to create a Stripe customer. When the "Start subscription" button is clicked, above, we run code that looks like this. (The production version of this code is actually spread out across three functions, and we're ignoring error handling. We'll ignore error handling in all the code samples here.)

   -- CODE language-js --
   // display UI "spinner" and block input
   this.setState({ processing: true });
   // get an opaque token from Stripe Elements
   let payload = await stripe.createToken({
     type: 'card',
     name: name,
   });
   // send a request to our own server to create a Stripe customer record
   let response = await fetch("/dashboard/create-customer", {
     method: "POST",
     headers: { Authorization ... },
     body: { stripetoken: payload.token.id, teamId: dailyTeam.id }
   });
   // update the UI
   dispatch(refreshBillingInfo(await response.json()));
   this.setState({ processing: false });

On the server, the code to create the new customer looks like this.

   -- CODE language-js --
   // ping Stripe to create a new customer object
   // (res.locals.user is set by an auth middleware function)
   let customer = await stripe.customers.create({
     email: res.locals.user.email,
     source: req.body.stripeToken,
   });
   // make a database record to associate the Stripe customer object
   // with a Daily.co team. we're using the Sequelize ORM library
   let cusRecord = models.StripeCustomer.create({
     teamId: req.body.teamId,
     stripeCustomerId: customer.id,
   });
   // return the new customer billing info in a form that our
   // front-end code can consume
   let billingInfo = {
     stripeCustomerId: customer.id,
     email: customer.email,
     card: customer.sources.data[0],
     subscriptions: [],
   };
   res.status(200).send({ billingInfo });

## Updating payment info

Updating credit card info is very similar.  On the server side, we look up an existing customer record and modify it, instead of creating a new one.

   -- CODE language-js --
   let cusRecord = await models.StripeCustomer.findOne({
     where: { teamId }
   });
   let customer = await stripe.customers.update(
     cusRecord.stripeCustomerId, { source: req.body.newStripeToken }
   );
   let billingInfo = {
     stripeCustomerId: customer.id,
     email: customer.email,
     card: customer.sources.data[0],
     subscriptions: customer.subscriptions.data,
   };
   res.status(200).send({ billingInfo });

## Subscribing, canceling, and upgrading

Subscribing customers to plans is easy. Handling upgrades, downgrades, and cancellations requires making some policy decisions. It's surprisingly easy for corner cases to proliferate if you allow customers to transition between any plan, any time during a billing cycle.

We decided:

- Customers can't downgrade from our top paid tier to our lower paid tier. If someone really wants to downgrade, she can cancel and then subscribe again.
- Upon cancellation, we cancel the plan immediately. (We don't cancel at the end of the billing cycle, which is a common way to structure Software as a Service plans.) We do this to avoid the possibility that a customer continues to rack up usage-based charges, and then is surprised when the final bill arrives. When a customer cancels, we refund a prorated amount of the base plan charge, and also charge for any accrued usage.
- Customers can upgrade from our lower paid tier to our upper paid tier. When a customer upgrades, the prorated difference between the plans is charged on the next monthly invoice. (This is Stripe's default approach to handling plan changes.)

Here's code that **subscribes** a customer to one of our paid tiers. (Again, simplified and with no error handling!)

   -- CODE language-js --
   // this code is inside an HTTP POST request handler, and the
   // stripeCustomerId and dailyPlanId are supplied as body
   // arguments to the POST request. res.locals.team is filled
   // in by a middleware function. we look up the list of
   // stripePlanIds that map to a Daily Plan in the `plans` config
   // structure described above.
   let stripeCustomerId = req.body.stripeCustomerId,
       dailyPlanId = req.body.dailyPlanId,
       stripePlanIds = plans[dailyPlanId].stripePlans;
   // ask Stripe to create a subscription for this customer, consisting
   // of our list of plan ids.
   let subscription = stripe.subscriptions.create({
     customer: stripeCustomerId,
     items: stripePlanIds.map((id) => ({ plan:id })),
     metadata: {
       api_plan_id: dailyPlanId,
       team_name: res.locals.team.name,
     },
   });
   // keep track in our database of the (Daily.co) plan this team
   // is subscribed to
   await res.locals.team.setProperty({
     name: 'api_plan_id',
     value: dailyPlanId
   });
   // set the plan expiration time to one hour in the future, to allow
   // the credit card charge time to clear. when the payment processes,
   // our webhook handling code will update the plan expiration time
   await res.locals.team.setProperty({
     name: 'api_plan_expires',
     value: Math.floor(Date.now()/1000) + 60*60
   });
   res.status(200).send({ subscription });

Here's code that **cancels** a subscription.

   -- CODE language-js --
   let stripeCustomerId = req.body.stripeCustomerId,
       apiPlanId = req.body.apiPlanId,
       stripeCustomer = await stripe.customers.retrieve(stripeCustomerId),
       subscription = stripeCustomer.subscriptions.find(
         (s) => s.metadata.api_plan_id === apiPlanId
   );
   
   await stripe.subscriptions.del(
     subscription.id, { invoice_now: true, prorate: true }
   );
   await res.locals.team.setDomainProperty({
     name: 'api_plan_id',
     value: null
   });
   await res.locals.domain.setDomainProperty({
     name: 'api_plan_expires',
     value: null
   });
   res.status(200).send({ deleted: true });

Here's code that **upgrades** a subscription, which is more complicated than subscribing to and cancelling subscriptions. We have to "diff" the subscription items list ourselves. This code is generalized and should work to switch between arbitrary plans. But note that this approach fails if the upgrade process tries to remove any metered billing plan items from the subscription.

   -- CODE language-js --
   import { pull } from 'lodash';
   
   let stripeCustomerId = req.body.stripeCustomerId,
       oldPlanId = req.body.oldPlanId,
       newPlanId = req.body.newPlanId,
       newStripePlanIds = plans[newPlanId].stripePlans,
       stripeCustomer = await stripe.customers.retrieve(stripeCustomerId),
       subscription = stripeCustomer.subscriptions.find(
         (s) => s.metadata.api_plan_id === oldPlanId
       );
   
   let items = buildStripeChangedItemsList(subscription, newStripePlanIds);
   await stripe.subscriptions.update(subscription.id, {
     items, metadata: { api_plan_id: dailyPlanId }
   });
   await res.locals.team.setProperty({
     name: 'api_plan_id',
     value: newPlanId
   });
   res.status(200).send({ modified: true });
   
   function buildStripeChangedItemsList(subscription, newStripePlanIds) {
     // loop through a subscription's items checking if they are
     // present in the new items list. if present, consider them
     // unchanged and ignore them. if not present, we need to mark them
     // deleted in our subscriptions.update call. finally, we also need
     // to add any new items into the subscriptions.update call
   
     // first, copy the Stripe plan ids array so we can modify it
     // destructively
     let planIds = newStripePlanIds.slice(),
         itemsModList = [];
     for (var item of subscription.items.data) {
       if (planIds.includes(item.plan.id)) {
         // unchanged - remove from planIds array.
         pull(planIds, item.plan.id);
       } else {
         // not present in new plan, so we need to mark this as
         // deleted. (this will fail for metered-plan items)
         itemsModList.push({ id: item.id, deleted: true });
       }
     }
     // add in remaining new items and return
     return itemsModList.concat(planIds.map((id) => ({ plan: id })));
   }

## Webhook: adding usage-based line items to an invoice

As described in **Billing for usage**, above, we don't use Stripe's metered pricing plan features. Instead, we manually add line items to our invoices for our usage-based costs.

Here's how we handle the `invoice.created` webhook that Stripe sends just before finalizing an invoice.

   -- CODE language-js --
   // inside an HTTP handler for Stripe webhooks
   const event = stripe.webhooks.constructEvent(
         req.rawBody,
         req.headers['stripe-signature'],
         process.env.STRIPE_INVOICE_HOOK_SECRET
       );
   
   if (event.type === 'invoice.created') {
     let invoice = event.data.object;
     if (invoice.finalized_at) {
       // this invoice is already finalized. it's likely an initial
       // subscription charge. in any case, we can't modify a
       // finalized invoice.
       res.status(200).send();
       return;
   
       let subscription = await stripe.subscriptions.retrieve(invoice.subscription);
       await stripeInvoiceCreated(invoice, subscription);
       res.status(200).send();
       return;
     }
   }
   
   //
   
   async function stripeInvoiceCreated(invoice, subscription) {
     let apiPlan = plans[subscription.metadata.api_plan_id],
         stripeCustomerId = invoice.customer,
         timeStart = invoice.period_start,
         timeEnd = invoice.period_end;
     let stripeCustomer = await models.StripeCustomer.findOne({
       where: { stripeCustomerId }
     });
     let team = await models.Team.findOne({ where: {id: stripeCustomer.teamId} });
     
     let periodUsage = await usageTotals({ tfStart, tfEnd, team });
     for (var u of apiPlan.usage) {
       // here we create a new invoice line item. we have to set the
       // amount because our unit costs may be sub-cent. we aren't
       // allowed to pass both a quantity and an amount, so we embed
       // the quantity in the item description.
       let minutes = periodUsage[u.field],
           cost = Math.floor(u.costf(minutes)*100); // convert to cents
       await stripe.invoiceItems.create({
         customer: stripeCustomerId,
         invoice: invoice.id,
         currency: 'usd',
         amount: cost,
         description: `${u.description} (qty ${minutes})`
       });
     }
   }

## Webhook: extending a subscription's expires time

We also listen for Stripe's `invoice.payment_succeeded` webhook, and — as suggested in the [Stripe subscription webhooks tutorial](https://stripe.com/docs/billing/webhooks) — update our `api_plan_expires` team property when each monthly subscription payment succeeds.

This code is very simple!

   -- CODE language-js --
   // again, inside our HTTP handler for Stripe webhooks
   if (event.type === 'invoice.payment_succeeded') {
     let invoice = event.data.object,
         subscription = await stripe.subscriptions.retrieve(invoice.subscription);
     await stripeInvoicePaymentSucceeded(invoice, subscription)
     res.status(200).send();
     return;
   }
   
   //
   
   async function stripeInvoicePaymentSucceeded(invoice, subscription) {
     // canceled subscription, ignore. assume we set api_plan_expires in
     // our cancellation code
     if (subscription.canceled_at) {
       return;
     }
   
     // period end in the past. ignore. old invoices being paid, or
     // something pathological
     if (subscription.current_period_end < Date.now()/1000) {
       return;
     }
   
     let stripeCustomerId = invoice.customer,
         paidUntil = subscription.current_period_end,
         stripeCustomer = await models.StripeCustomer.findOne({
           where: { stripeCustomerId }
         }),
         team = await models.Team.findOne({ where: {id: stripeCustomer.teamId} });
   
     // set expires to 3 days after the next billing date, to give
     // failed charges a chance to automatically retry
     await team.setProperty({
       name: 'api_plan_expires',
       value: paidUntil + 60*60*24*3
     });
   }

# Questions, comments, suggestions?

We love to talk about writing production-quality code! If you have thoughts, please send us email: help@daily.co. Or find us on Twitter at [@trydaily](https://twitter.com/trydaily).

Daily.co is a complete platform for 1-click video calling.
Learn more about our products: Daily.co API, a video calling API for developers; Daily.co TV for conference rooms and always-on portals; or our free no download browser video calls.

Recent posts