Metered Usage
How to add metered usage to your Stripe subscription
Before you start
Creating a metered usage subscription is similar to:
💳Adding a subscriptionThe main differences being:
In Stripe you create a Meter and you create Products/Prices that are metered
In Petal Pro you record and synchronise usage with Stripe
The following will guide you through the process of adding metered usage to the AI chat feature.
Starting with a fresh app
For this tutorial you will need:
Access to Petal Pro (you can purchase it on petal.build)
Elixir & Erlang installed (you can follow our installation guide to get up and running)
PostgreSQL running: Postgres.app works well for Mac or we have a docker-compose.yaml file that will run Postgres for you
An account on Stripe
For deployment (optional) you will need:
An account on Fly.io
Download
Head over to the projects page and create a new project, then download v3.0.0. You can unzip it in your terminal with the command unzip petal_pro_3.0.0.zip
.
Run
First, rename the project:
mv petal_pro_3.0.0 sub_demo
cd sub_demo
mix rename PetalPro SubDemo
Next, start the demo app:
mix setup
mix phx.server
Configure Stripe
Before we do anything inside the demo app, we need to add some Stripe Products. Login using your account. Make sure that Stripe is in "test mode" - this will allow you to test purchasing without a credit card.
Install the Stripe CLI
Follow these directions to install the Stripe CLI. Then login via the command line (the instructions to login are also on this page):
stripe login
Create a product with usage-based prices
First create the "Essential" product:
stripe products create --name="Essential"
Take note of the id
returned, it will look like prod_xxxxx
.
Create the Monthly price for metered usage:
stripe prices create \
--unit-amount=5 \
--currency=usd \
-d "transform_quantity[divide_by]"=1000 \
-d "transform_quantity[round]"=up \
-d "recurring[interval]"=month \
-d "recurring[usage_type]"=metered \
--product="prod_xxxxx"
Make sure you replace prod_xxxxx
with the id
returned from the previous call. usage_type
is set to "metered" - which means that the unit price isn't fixed. The price is 5 cents per 1000 units (rounding up - meaning a minimum of $0.05 will be charged on single use).
Add the "Business" and "Enterprise" products
In this guide we'll stick to per month pricing to keep it simple for the end user.
Repeat the process (above) to create a "Business" and an "Enterprise" product. Use the table below as a point of reference for suggested prices:
Monthly price
$0.05 per 1000 units
$0.5 per 15,000 units
$5 per 200,000 units
Enable switching plans for the Customer Portal
When purchasing a subscription, Petal Pro will redirect to Stripe. In the case where the user switches from an existing plan to another - you need to enable some settings for the Customer Portal. Once logged into Stripe:
Make sure Stripe is in Sandbox or Test mode
Click on the cog on the top right (
Settings
)In the section titled "Billing", click on
Customer Portal
Expand the "Subscriptions" section
Enable the setting,
Customers can switch plans
Using "Find a test product..." add prices from the Essential, Business and Enterprise products
In addition, you may want to review the "Business information" section - to customise the output of the Customer Portal.
Create a Meter
The final step is to create a Meter. A Meter allows you to record usage against a particular Subscription/Price. Think of it as a way of grouping cost associated with a feature. In this guide we're going to create a Meter for the AI chat feature:
stripe billing meters create \
--display-name="AI tokens" \
--event-name=ai_tokens \
-d "default_aggregation[formula]"=sum
Configure Petal Pro
Petal Pro comes pre-configured for Stripe integration. There are three things you need to do to test the integration locally:
Run the CLI web hook
Add Stripe settings to your development environment
Update the Petal Pro config with product/price details (created above) from your Stripe account
Run the CLI web hook
In order to test your Stripe integration on your dev machine, you'll need to run a web hook. This is done using the Stripe CLI (see the Install the Stripe CLI section for more information).
Once you are logged in via the Stripe CLI, you can run the following command:
stripe listen --forward-to localhost:4000/webhooks/stripe
> Ready! You are using Stripe API Version [2022-11-15]. Your webhook signing secret is whsec_xxxxxxxxxxxxxxx (^C to quit)
Take note of the whsec_xxxxxxxxxxxxxxx
secret. This will be used as your STRIPE_WEBHOOK_SECRET
in the next section.
Add Stripe settings to your development environment
First, you'll need your STRIPE_SECRET
and your STRIPE_WEBHOOK_SECRET
. To get your STRIPE_SECRET
:
Go to the Stripe console
Make sure you're in Sandbox or Test mode
At the bottom left of the screen, click on
Developers
then clickAPI keys
Under "Standard Keys", look for the "Secret key"
Click the secret key to copy it
To get the STRIPE_WEBHOOK_SECRET
, see the instructions under Run the CLI web hook.
The next step is to add these as environment variables to your demo app. Make sure that you install direnv and that you're in the root of the demo project:
cp .envrc.example .envrc
Uncomment these lines at the bottom of the file:
# If using Stripe for payments:
export STRIPE_SECRET=""
export STRIPE_WEBHOOK_SECRET=""
export STRIPE_PRODUCTION_MODE="false"
Update the values you obtained (above) for STRIPE_SECRET
and STRIPE_WEBHOOK_SECRET
.
The .envrc
file is listed in .gitignore
. Meaning that the secrets will live on your development machine and will not be accidentally pushed to the git
repo.
To activate the new environment variables:
direnv allow
Update the Product config
The following shows how you can update the Essential plan:
config :petal_pro, :billing_products, [
%{
id: "essential",
name: "Essential",
description: "Essential description",
most_popular: true,
features: [
"Essential feature 1",
"Essential feature 2",
"Essential feature 3"
],
plans: [
%{
id: "essential-monthly",
name: "Monthly",
- amount: 1900,
+ unit_amount: 5,
+ per_units: 1000,
interval: :month,
allow_promotion_codes: true,
- trial_days: 7,
items: [
- %{price: "price_1NLhPDIWVkWpNCp7trePDpmi", quantity: 1}
+ %{price: "price_xxxxx"}
]
},
- %{
- id: "essential-yearly",
- name: "Yearly",
- amount: 19_900,
- interval: :year,
- allow_promotion_codes: true,
- items: [
- %{price: "price_1NWBYjIWVkWpNCp7pw4GpjI6", quantity: 1}
- ]
- }
]
},
# Business and Enterprise config too
}
amount
has been replaced with unit_amount
and per_units
- this controls what the user sees for each plan on the subscribe page. Eliminating trial_days
means that you don't have to wait until the trial is over to see upcoming charges.
In this guide we're focusing on monthly pricing - the essential-yearly
product has been removed.
When the user selects a plan, Petal Pro will use these settings to make a Stripe call. In the line where we're adding price_xxxxx
we're omitting quantity
. If the quantity
were present it would be passed up too. In the case of a usage-based price, passing up quantity
will result in a Stripe error.
Replace price_xxxxx
with a valid price id. To get the list of prices for the "Essential" product:
# To get the "Essential" Product id
stripe products search \
--query="name:'Essential'"
# To list all Prices that belong to the "Essential" product
stripe prices list \
--product="essential_product_id" \
--active=true
Repeat the above process for the Business and Enterprise plans.
Update the Meter config
Add a reference to our "AI Tokens" meter:
config :petal_pro, :billing_meters, [
- # %{
- # id: "mtr_test_61SNiSGOruDKkjzQ241IWVkWpNCp70L2",
- # name: "API Usage",
- # event_name: "api_meter"
- # }
+ %{
+ id: "mtr_xxx_xxxxx",
+ name: "AI Tokens",
+ event_name: "ai_tokens"
+ }
]
To get the current list of meters:
stripe billing meters list
Find the meter that has the event_name
of ai_tokens
. Use it's id
to replace mtr_xxx_xxxxx
.
Enable the CollectMeterEvents
GenServer
CollectMeterEvents
GenServerThe CollectMeterEvents
GenServer is used to record meter usage events without blocking the calling process. Uncomment the CollectMeterEvents
GenServer to enable it:
defmodule PetalPro.Application do
def start(_type, _args) do
children = [
...
# Start the meter collection GenServer
- # PetalPro.Billing.Meters.CollectMeterEvents,
+ PetalPro.Billing.Meters.CollectMeterEvents,
...
]
opts = [strategy: :one_for_one, name: PetalPro.Supervisor]
Supervisor.start_link(children, opts)
end
end
Enable the synchronisation job
Synchronisation with Stripe is handled by the MeterSyncWorker
Oban job. Uncomment the Oban configuration to enable it:
config :petal_pro, Oban,
repo: PetalPro.Repo,
queues: [default: 5, billing: 5],
plugins: [
{Oban.Plugins.Pruner, max_age: 3600 * 24},
{Oban.Plugins.Cron,
crontab: [
# {"@daily", PetalPro.Workers.ExampleWorker}
- # {"@daily", PetalPro.Billing.Providers.Stripe.Workers.MeterSyncWorker},
+ {"@daily", PetalPro.Billing.Providers.Stripe.Workers.MeterSyncWorker},
# {"* * * * *", EveryMinuteWorker},
# {"0 * * * *", EveryHourWorker},
# {"0 */6 * * *", EverySixHoursWorker},
# {"0 0 * * SUN", EverySundayWorker},
# More examples: https://crontab.guru/examples.html
]}
]
Handling metered usage
There are two steps with regards to handling metered usage:
Record the usage
Sync the usage with Stripe
Record the usage
Metered usage is stored in the Petal Pro database. To record an event, use the record_event
function:
alias PetalPro.Billing.Meters
Meters.record_event(meter_id, customer_id, subscription_id, quantity)
record_event
is non-blocking and enqueues the event using the CollectMeterEvents
GenServer. In the background, the CollectMeterEvents
GenServer writes the event to the database as soon as it can.
Sync the usage with Stripe
When it comes to metered usage events, Petal Pro is considered the source of truth. For example, when you record an event you specify the subscription_id
. You can't filter by subscription_id
when retrieving meter usage events via the Stripe API. However, you can when you return meter usage events from the database. This is used in the billing page to match meter usage events to the upcoming invoice.
Synchronisation with Stripe is handled by the MeterSyncWorker
Oban job - which will upload unprocessed events to Stripe. A metered usage event is considered processed if:
The event has been uploaded successfully (it will be marked as sent)
There is an error (it will be marked with the error)
Meter events with errors will not synchronise with Stripe. If something is not right, check the billing_meter_events
table for error messages. Events will be re-processed if the error_message
is removed
Metered events are processed in batches to avoid excessive uploads using the Stripe API. How often the synchronisation is run is based on the Oban configuration.
By default, this will happen once a day. However, for testing in development you might find it useful to increase the frequency. The following will run the Oban job every minute:
-{"@daily", PetalPro.Billing.Providers.Stripe.Workers.MeterSyncWorker}
+{"* * * * *", PetalPro.Billing.Providers.Stripe.Workers.MeterSyncWorker}
Updating the chat feature
Let's implement the following things to update the AI chat feature so that it can record metered usage:
Remove month/year toggle from the subscribe page
Change the user AI chat route - so it can load subscriptions
Update the menu
Update the Langchain calls to return usage
Adjust the AI chat feature so that it can consume the usage
Remove month/year toggle from the Subscribe page
Just remove the interval_selector
from the pricing_panels_container
:
<.container class="my-12">
<.h2 class="mb-8 text-center">{gettext("Choose a plan")}</.h2>
- <BillingComponents.pricing_panels_container panels={length(@products)} interval_selector>
+ <BillingComponents.pricing_panels_container panels={length(@products)}>
<%= for product <- @products do %>
<BillingComponents.pricing_panel
id={"pricing-product-#{product.id}"}
Change the user AI chat route
Recording metered usage requires a customer and subscription id:
alias PetalPro.Billing.Meters
Meters.record_event(meter_id, customer_id, subscription_id, quantity)
The simplest way to get this data is through the SubscriptionRoutes
. First, remove the existing route:
live "/users/org-invitations", UserOrgInvitationsLive
live "/users/two-factor-authentication", EditTotpLive
- live "/ai-chat", UserAiChatLive
live "/orgs", OrgsLive, :index
live "/orgs/new", OrgsLive, :new
Then add the following to SubscriptionRoutes
:
scope "/app", PetalProWeb do
pipe_through [:browser, :authenticated, :subscribed_user]
get "/subscribed", PageController, :subscribed
live_session :subscription_authenticated_user,
on_mount: [
{PetalProWeb.UserOnMountHooks, :require_authenticated_user},
{PetalProWeb.OrgOnMountHooks, :assign_org_data},
{PetalProWeb.SubscriptionPlugs, :subscribed_user}
] do
live "/subscribed_live", SubscribedLive
+ live "/ai-chat", UserAiChatLive
end
end
scope "/app", PetalProWeb do
pipe_through [:browser, :authenticated, :subscribed_org]
scope "/org/:org_slug" do
get "/subscribed", PageController, :subscribed
live_session :subscription_authenticated_org,
on_mount: [
{PetalProWeb.UserOnMountHooks, :require_authenticated_user},
{PetalProWeb.OrgOnMountHooks, :assign_org_data},
{PetalProWeb.SubscriptionPlugs, :subscribed_org}
] do
live "/subscribed_live", SubscribedLive
+ live "/ai-chat", UserAiChatLive
end
end
end
For a :user
based subscription, it'll look the same:
http://localhost:4000/app/ai-chat
But for an :org
based subscription, the url will look something like this:
http://localhost:4000/app/org/abbott-quigley/ai-chat
In either case, the Live View will be mounted with the current Customer and Subscription.
Update the menu
For this guide, we're using :org
based subscriptions. In menus.ex
remove the :user_chat_ai
menu item:
def main_menu_items(nil), do: []
# Signed in main menu
- def main_menu_items(current_user), do: build_menu([:dashboard, :orgs, :subscribe, :user_ai_chat], current_user)
+ def main_menu_items(current_user), do: build_menu([:dashboard, :orgs, :subscribe], current_user)
# Signed out user menu
def user_menu_items(nil), do: build_menu([:sign_in, :register], nil)
Then update the org menu:
[
get_link(:org_dashboard, org),
get_link(:org_settings, org),
- get_link(:org_subscribe, org)
+ get_link(:org_subscribe, org),
+ get_link(:org_chat, org)
],
& &1
)
And add the get_link
function:
defp get_link(:org_chat, org) do
if Customers.entity() == :org do
%{
name: :org_chat,
path: ~p"/app/org/#{org.slug}/ai-chat",
label: gettext("AI Chat"),
icon: "hero-command-line"
}
end
end
Update the Langchain calls to return usage
The following adds the on_llm_token_usage
callback. To enable it, include_usage
must be set to true
:
%{
on_llm_new_delta: fn llm_chain, delta ->
send(live_view_pid, {:chat_delta, delta, llm_chain})
+ end,
+ on_llm_token_usage: fn llm_chain, usage ->
+ send(live_view_pid, {:usage, usage, llm_chain})
end
}
],
- stream: true
+ stream: true,
+ stream_options: %{include_usage: true}
})
}
|> LLMChain.new!()
on_llm_token_usage
sends a message to the calling Live View.
Adjust the AI chat feature
Finally, add the following handle_info
callback in user_ai_chat_live
:
@impl true
def handle_info({:usage, %Elixir.LangChain.TokenUsage{input: input, output: output}, _assistant}, socket) do
customer = socket.assigns.customer
subscription = socket.assigns.subscription
meter = PetalPro.Billing.Meters.get_meter_by_event_name("ai_tokens")
PetalPro.Billing.Meters.record_event(meter.id, customer.id, subscription.id, input + output)
{:noreply, socket}
end
This consumes the message sent from the Langchain helper. Because the code is inside the Live View and it's protected by the subscription routes, we can grab the customer
and the subscription
as an assign. The input
and output
parameters represent the number of tokens that have been used. Once we lookup the meter
we have enough information to call record_event
.
Ready to go!
In the web app, login as the admin user.
Navigate to the Organisations page:
http://localhost:4000/app/orgs
Click on your Organisation, then click "Subscribe". You'll see the following page:

Choose a plan to subscribe to. Then use Stripe to complete the process - you can use 4242 4242 4242 4242
as your credit card for testing.
Once you have completed the process, go back to the Organisations page and click on "AI Chat":

Ask a question - this will trigger the code that collects metered usage. Now go to the Admin page and click on "Oban Web":

Make sure that the MeterSyncWorker
job has successfully completed.
Finally, go to your Organisation, then click on "Org Settings" and "Billing". You should see something similar to this:

If everything went well, you should see an "Upcoming" amount and "AI Tokens" usage data . If you configure multiple Meters, they will be automatically grouped on this screen.
Deployment with Fly.io
Once your demo is ready, you can push it to production. To do so, there's three steps:
Push the demo app to Fly.io
Setup webhook endpoint on Stripe
Configure Fly.io environment variables
Push the demo app to Fly.io
To start with Fly.io, use the following guide:
🌎Deploy to Fly.ioSetup webhook endpoint on Stripe
You can use the Stripe CLI to run the webhook when working on your own dev machine. However, for a hosted service, you need to configure an endpoint in the Stripe console:
Open the Stripe console
Make sure you're in "Test mode"
Click on
Developers
at the top of the pageThen click on
Webhooks
Click on
Add endpoint
For the Endpoint url
enter the following (replace your-host-name
with the name of your fly application):
https://your-host-name.fly.dev/webhooks/stripe
Further down the page under the heading "Select events to listen to" - click on + Select events
. Add the following events to the list:
customer.subscription.created
; andcustomer.subscription.updated
Finally, click the Add endpoint
button.
Configure Fly.io environment variables
The demo app running in Fly.io needs to be configured for Stripe. We need two settings. First, to obtain the STRIPE_SECRET
:
In Stripe, make sure you're in "Test mode"
Click on
Developers
Click on
API Keys
Click on
Reveal test key
Click the key to copy it. This is for our
STRIPE_SECRET
environment variable
Next, to obtain the STRIPE_WEBHOOK_SECRET
:
In
Developers
click onWebhooks
Click on the endpoint you just created
Under "Signing secret" click on
Reveal
Copy the secret
Now at the command line, run the following command:
fly secrets set _
STRIPE_SECRET="sk_test_xxx" _
STRIPE_WEBHOOK_SECRET="whsec_xxx" _
STRIPE_PRODUCTION_MODE="false"
Once Fly has finished deploying you're new secrets, you should be able to purchase a subscription using the Fly demo app!
Using the CLI with "Live mode"
The Stripe CLI defaults to "Test mode". To work in "Live mode" you'll need to pass in an extra parameter. For example, if you wanted to list products in live mode you would do the following:
stripe products list --live
Not all Stripe CLI commands support the --live
flag. But it will work with the commands listed in this tutorial.
Recreate the Stripe Products
You'll need to repeat the Configure stripe section - re-creating the products in "Live mode". This means that you'll end up creating the same products, but they will have different ids. Before you update the configuration in /config/config.exs
, I suggest that you copy the following section to /config/dev.exs
:
config :petal_pro, :billing_products, [
%{
id: "essential",
name: "Essential",
description: "Essential description",
most_popular: true,
# ...
}
That way you can stick with "Test mode" on your dev machine and then use /config/config.exs
for your production server.
Last updated
Was this helpful?