How To Add Usage-Based Billing To Your SaaS App

Kyle Gawley
Kyle Gawley
Gravity founder
How To Add Usage-Based Billing To Your SaaS App

Discover how to boost your SaaS app's revenue with usage-based billing using Stripe in this step-by-step guide.

Let's say you're building an AI image generation app and want to charge users based on the number of images they generate each month.

You can achieve this easily with some usage tracking in your database and Stripe volume pricing.

Let's dive in!

1. Setup Stripe Usage Plans

Stripe includes a volume pricing model that enables you to charge based on the number of units consumed during the subscription period. 

Add a new price to your Stripe product and set the model to volume. You can then set the price per unit. Later, we'll tell Stripe how many units were consumed during the billing period, and Stripe will charge accordingly - easy! 

Create a volume pricing plan in Stripe

⚠️  Stripe doesn't support switching from a non-volume pricing model to a volume pricing model on the same subscription. Ensure all your prices are volume pricing if you want to allow users to switch between plans in your app.

2. Track The Usage

You track the usage for each account in your application and then periodically report it to Stripe. You could report it to Stripe every time a feature is used and skip this step, but:

1. it’s more efficient to batch it into one API call to Stripe than 100s
2. you probably want to store your usage reports for internal reporting

Database Structure

Create a new table in your database called usage, with the following fields:

id: primary key
account_id: string, not nullable, references id in account
period_start: timestamp, not nullable
period_end: timestamp
quantity: not nullable, default to 0 
reported: boolean, not nullable, default to false

This table will store usage reports for each period and contains a reported boolean to determine if the usage has been reported to Stripe.

A period ends when usage is reported, and a new row with a period_start of now will be opened.

Incrementing Usage

In our AI image generation app, you'll want to increment the usage each time a user generates a new image; in your app it will be something different.

So, on the API endpoint where this happens, increment the quantity of the latest open usage report by 1 (or more if you provide batching).

This is the SQL code for this with Knex.js is:

async function increment({ account, quantity }){ 

 return await db('usage')
 .increment('quantity', quantity || 1)
 .where({ account_id: account })
 .andWhere('reported', false)
 .andWhere('period_end', null)

}

and for Mongo (Mongoose):

async function increment({ account, quantity }){ 

 await Usage.findOneAndUpdate({

  account_id: account,
  reported: false,
  period_end: null

 },{

  $inc: { quantity: quantity || 1 }

 });
}

3. Report Usage To Stripe

The final step is to report the usage to Stripe. I recommend doing this at least once per day to ensure Stripe is always up to date. If you only report every week or month, your invoices will be off, and some customers will be billed for usage that occurred this month, next month. 

Create a Background Job

We want the usage to be reported in the background every day, so I'm using the background jobs included with Gravity to handle this for me. You can set up your jobs with BullJS if you don't have Gravity.

const db = require('./knex');
const stripe = require('stripe')(process.env.STRIPE_SECRET_API_KEY);

// get unreported usage
 const reports = await db('usage')
.select('usage.id', 'account_id', 'period_start', 'period_end', 'quantity', 'reported', 'stripe_subscription_id')
.join('account', 'usage.account_id', 'account.id')
.where({ reported: false });

// loop reports
if (reports.length){
  for (report of reports){

    // get the subscription item id
    const sub = stripe.subscriptions.retrieve(report.stripe_subscription_id);
    const id = sub?.items?.data?.[0]?.id;

    if (id)
      await stripe.subscriptionItems.createUsageRecord(id, { quantity: report.quantity });
    
    // open a new report for the next period for this account
    await db('usage').insert({ account_id: account, quantity: 0, period_start: new Date() });

  }

  // close reported reports
  return await db('usage')
  .update({ period_end: new Date(), reported: true })
  .whereIn('id', reports.map(x => { return x.id }))

}

We first get a list of usage reports where reported is set to false – these are reports that haven't been reported to Stripe yet. I've joined stripe_subscription_id from the account table to update the subscription on Stripe.

Next, we loop over each report and:

  1. Get the stripe subscription
  2. Get the stripe subscription item ID
  3. Report the usage to Stripe
  4. Open a new usage report for the next period on this account 

Finally, we close all of the open usage reports using the IDs.

Cancelling Subscriptions

If a user cancels their subscription, ensure you pass the invoice_now and prorate options to Stripe; otherwise, they won't be billed for uncharged usage.

return await stripe.subscriptions.cancel(subscription_id, {
prorate: true, invoice_now: true });

If this sounds like too much of a headache, usage-based billing is in included out-of-the-box in my SaaS boilerplate so you can build a usage-based billing app hassle-free.

Photo credit: Jason Richard

Download The Free SaaS Boilerplate

Build a full-stack web application with React, Tailwind and Node.js.