Modeling Usage Based Billing with Stripe
Let's take a look at the Prefab pricing page and go step through step how we modelled it using Stripe's new Usage Based Billing APIs.
Prefab sells things like Feature Flags and Dynamic Log Levels with awesome developer tooling. We charge money on two dimensions, both of which try to reflect the effort it takes us to support our customers. Those dimensions are:
Servers We need to keep a live connection open for each of the servers side SDK that you use to connect. We charge $2 / server for this, but that goes down to $1 or $.80 at higher volume.
Requests Front end clients make requests to get feature flag evaluations. We charge for these requests at the rate of $10 per 1.5M, with better pricing on the higher tiers.
Modeling Associated Products
To setup billing in Stripe, you need to decide what your products and prices are going to be. This sounds like it should be easy, but there are actually a lot of different ways to go about modeling things. Here's what we did.
In this diagram the green boxes represent the "product" and the blue represent prices.
Having a single "product" for "Servers" is clearly the right move, but what about the base tier pricing? You could probably model the 3 Free/Basic/Enterprise as a single "product" called "platform" with 3 different prices. Why not do that? Well the Stripe pricing table let's you use an HTML widget <stripe-pricing-table>
. It was our intention to use this weidget, because the less code the better, right? When you go to setup that widget however, it was very much "add your product" as the primary choices. Since for us, the choice here is Basic/Pro/Enterprise, this lead us to have a Product for each.
The <stripe-pricing-table>
lets you add more than one price for the same product, but this seems to be for allowing monthly / annual billing.
Moving Beyond Pure Usage Based
Originally, we were purely usage based, ie "just $1 / server" but we discovered customers wanted easier ways to estimate their bill. Ask a customer to estimate their bill and there's friction, but tell them it's "$99 for up to 100 server connections" and they can just say "oh great, we have fewer than that so the bill won't be more than $99/month". It's a touch irrational when you've got your engineer hat on, but it turns out that being kind to the actual people doing the software purchasing is a good idea for us.
In order to have tiers in combination with usage based overage we end up with pricing of the form "$99 for the first 100 and then $99 per 100 after that". The term of art for this is Graduated / Tier based pricing even though our tiers are the same price. We'll get into more details shortly.
Connecting Subscriptions to Prices
When we go to create a subscription, we'll see that a subscription is basically just a payment method, associated with a set of prices
. Each connection is called a SubscriptionItem
.
Stripe::SubscriptionItem.create({
subscription: 'sub_1Mr6rbL',
price: 'price_1Mr6rdLkdI'
})
Here's an ER diagram of the billing modeling, representing a customer subscribed to the Pro
plan. You can see that the basic and enterprise prices are not connected to the user's subscription.
I've named the prices Pro.Servers.0
I would highly recommend that you do something similar and add in a number to indicate the price version. These prices are pretty immutable once you get going and it's easy to make mistakes. A little bit of version control in your naming will prevent server-price-basic-v2-copy-use-this-one
type fiascos.
The important take-away here is that in our world, there are really "tightly correlated prices". If you have the "pro" prices for your subscription, then you need to have the "pro.server" price for your Server product.
The main code that we're going to end up writing is the logic to ensure that these prices remain correlated as subscriptions change.
Usage Based Billing
Here's the data model of Stripe's usage based billing, right from their documentation.
This is much better than the previous model, which we discussed earlier.
Our usage based tracking is really going to be very simple. For the prices basic.requests.0
, pro.requests.0
etc, we just set them up to reference the same meter: billable_requests
. This makes our code that records usage totally oblivious to what subscription the customer has, which is just what we want.
This is also really useful for trials / free tiers. We create a customer
for each Prefab team when they signup and we can instantly start tracking usage for them against a Meter. Eventually our customer will add a subscription and at that point the usage can start making it onto an invoice. But it's nice to be able fully separate the concern of measuring and tracking usage from the more intricate dance of creating and adjusting subscriptions & prices.
def create_billable_requests_meter_events(amount, api_usage, event_type)
attr = {
event_name: "billable_requests",
timestamp: api_usage.day.to_time(:utc).to_i,
payload: {
stripe_customer_id: @team.stripe_customer_id,
value: amount,
}
Stripe::Billing::MeterEvent.create(attr)
end
Rendering the Pricing Table
Ok, so we've seen how we modelled our Products and Prices. We've started recording metered usage for our customers. How do we connect the two? How do we actually create a subscription and add the correct 3 prices as subscription items?
The promise of Stripe handling the entire pricing table was appealing to me. With our product information already in the system, I was able to quickly create a PricingTable object from my Stripe Product Catalog and then I just dropped the following into my billing page.
<script async src="https://js.stripe.com/v3/pricing-table.js"></script>
<stripe-pricing-table pricing-table-id="prctbl_9999999"
publishable-key="pk_live_uCl3999999999">
</stripe-pricing-table>
This rendered me a nice looking pricing table and clicking on the button took me to a nice checkout experienve, so at first I thought I was all done.
Problems:
- A small thing, but I had to make a separate dark mode and light mode table which was... unfortunate.
- It only worked for Creating a subscription. I couldn't use the same tool to let people upgrade / downgrade a subscription. This was bigger bummer. (You see I can only 'cancel' the subscription from the Customer Portal).
Because of #2, if I was going to let people switch from Basic to Pro without canceling Basic, I essentially felt the need to rewrite a pricing table. If I was going to do that anyway, then I wanted it to be consistent, so I did not end up using this pricing widget.
The Code We Wrote
The code we wrote came in 4 parts. Here are the 4 things that our code needs to accomplish:
- Creating a subscription. eg
basic
andbasic.requests.0
andbasic.servers.0
. - Change a subscription from eg
basic
topro
which should removebasic
and addpro
- Catch webhook for subscription change and "reconcile". Remove
basic.requests.0
andbasic.servers.0
and addpro.requests.0
andpro.servers.0
. - Mapping in code of the underlying stripe price IDs, so that when we reconcile we can know what price ID to add in production for pro.
1) Basic Create of the Core Subscription
def self.create_checkout_session(team, new_plan_name, return_url)
new_object = StripeProducts.find_by(name: new_plan_name)
session = Stripe::Checkout::Session.create(
customer: team.stripe_customer_id,
line_items: [{
price: new_object.product_price_id,
quantity: 1
},
{
price: new_object.servers_price_id
},
{
price: new_object.requests_price_id
}
],
mode: 'subscription',
success_url: return_url,
cancel_url: return_url,
subscription_data: { billing_cycle_anchor: StripeProducts.get_billing_cycle_anchor }
)
session
end
We don't actually have to add the servers and requests prices here, since the webhook would reconcile them for us. However the checkout page is better if it has all 3 prices at the time of customers adding a credit card.
2) Change a Subscription
def self.change_subscription(team, new_plan_name)
subscription = team.get_subscription
old_product_object = get_product_for(subscription)
new_object = StripeProducts.find_by(name: new_plan_name)
if old_product_object
old_si = subscription.items.filter { |si| si.price.id == old_product_object.product_price_id }.first
Stripe::SubscriptionItem.delete(old_si.id)
end
new_si = Stripe::SubscriptionItem.create(
subscription: subscription.id,
price: new_object.product_price_id
)
end
3) Ensure Correct Related Products
def process_webhooks
case data.dig("type")
when 'customer.subscription.created' || 'customer.subscription.updated'
process_subscription
end
end
def process_new_subscription
subscription = data.dig("data", "object")
team = Team.find_by_stripe_customer_id(subscription.dig("customer"))
ensure_correct_subscription_items(team)
end
def ensure_correct_subscription_items(team)
subscription = team.get_subscription
prices_to_add(subscription).each do |price_id|
Stripe::SubscriptionItem.create({
subscription: subscription.id,
price: price_id
})
end
subscription_items_to_remove(subscription).each do |item|
Stripe::SubscriptionItem.delete(item.id, clear_usage: true)
end
end
def prices_to_add(subscription)
## if subscription is Pro, return [pro.requests.0, pro.servers.0]
end
def prices_to_remove(subscription)
## if subscription is Pro, return anything that isn't "pro.*"
end
4) A Mapping of Associated Products
Backing all of this, we did have to have a map of these "associated prices". When the "reconciler" runs it can use this mapping to find the request and server price ids for the given product price id.
class StripeProducts < ActiveHash::Base
LOG = SemanticLogger[self]
self.data = [
{ id: 1,
name: 'Basic',
default: OpenStruct.new(
product_price_id: 'price_1Or44444444',
requests_price_id: 'price_1Or5555555',
servers_price_id: 'price_1Or06666666'
),
production: OpenStruct.new(
product_price_id: 'price_1O77777777',
requests_price_id: 'price_1O8888888',
servers_price_id: 'price_1Oz1999999'
)
},
{ id: 2,
name: 'Pro',
default: OpenStruct.new(
product_price_id: 'price_1Or4111111',
...
This big file of the various Pricing IDs is not my favorite, but it works. I considered Stripe Terraform but it didn't have support for the new Meter object yet. I considered generating my prices in code and then saving the IDs out to a file. That might be a more elegant solution, but the quantity of these was below the threshold that was an obvious win for automation in my opinion.
Conclusion
The great thing about Stripes new support for Usage Based Billing and Meters is that the "usage" part has gotten very simple and is hardly something you need to think about anymore. Just decide your meters and record usage for the customer.
It still takes a decent amount of thinking for correctly model your products and prices, but now you can focus on these as their own problem. I hope taking a look at what we did at Prefab was helpful. Get in touch if you want to run your modeling past me while this is all still loaded in my brain.