Test

Adding Usage-Based Billing

How to start charging customers based on usage in your web app

Overview

This guide will walk you through setting up usage-based billing in your Petal Pro application using Stripe. Instead of fixed subscription tiers, customers will be charged based on their actual usage of your service.

Prerequisites

If you haven't already, we recommend you complete the Remindly app guide before starting this one. If you do, skip to the Configure Stripe section below. Alternatively, you can keep reading to create a new application.

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:

Project Setup

Head over to the projects page and create a new project, then download v2.2.0. You can unzip it in your terminal with the command unzip petal_pro_2.2.0.zip.

First, rename the project:

mv petal_pro_2.2.0 usage_billing_demo
cd usage_billing_demo
mix rename PetalPro UsageBillingDemo

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 for usage-based billing. Login using your account. Make sure that Stripe is in "test mode" - this will allow you to test billing 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 Usage-Based Products

Instead of creating fixed-price subscription products, we'll create usage-based products that charge customers based on their consumption.

First create the base product:

stripe products create --name="API Usage"

Take note of the id returned, it will look like prod_xxxxx.

Create a usage-based price (charged per API call):

stripe prices create \
  --unit-amount=10 \
  --currency=usd \
  --billing-scheme=per_unit \
  --usage-type=metered \
  -d "recurring[interval]"=month \
  -d "recurring[usage_type]"=metered \
  --product="prod_xxxxx"

Create additional usage-based prices for different usage types:

Storage usage (per GB):

stripe prices create \
  --unit-amount=50 \
  --currency=usd \
  --billing-scheme=per_unit \
  --usage-type=metered \
  -d "recurring[interval]"=month \
  -d "recurring[usage_type]"=metered \
  --product="prod_xxxxx"

Compute hours:

stripe prices create \
  --unit-amount=100 \
  --currency=usd \
  --billing-scheme=per_unit \
  --usage-type=metered \
  -d "recurring[interval]"=month \
  -d "recurring[usage_type]"=metered \
  --product="prod_xxxxx"

Configure Customer Portal

When customers want to manage their usage-based billing, you need to enable some settings for the Customer Portal. Once logged into Stripe:

  1. Make sure Stripe is in "Test mode"

  2. Click on the cog on the top right, then click on Settings

  3. In the section titled "Billing", click on Customer Portal

  4. Expand the "Subscriptions" section

  5. Enable the setting "Customers can switch plans"

  6. Using "Find a test product..." add the usage-based prices you created

  7. Review the "Business information" section to customize the Customer Portal output

Local Development Setup

Petal Pro comes pre-configured for Stripe integration. There are three things you need to do to test the integration locally:

  1. Run the CLI web hook

  2. Add Stripe settings to your development environment

  3. Update the Petal Pro config with usage-based product/price details

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.

Once you are logged in via the Stripe CLI, you can run the following command:

stripe listen --forward-to localhost:4000/webhooks/stripe

You should see output like:

> 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

First, you'll need your STRIPE_SECRET and your STRIPE_WEBHOOK_SECRET. To get your STRIPE_SECRET:

  1. Go to the Stripe console

  2. Make sure you're in "Test mode"

  3. At the top of the screen, click on "Developers" then click "API keys"

  4. Under "Standard Keys", look for the "Secret key"

  5. Click "Reveal test key"

  6. Click the 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 Configuration for Usage-Based Billing

Petal Pro needs to be configured for usage-based billing instead of fixed subscriptions. Update your configuration:

config :petal_pro, :billing_products, [
  %{
    id: "usage_based",
    name: "Usage-Based Billing",
    description: "Pay only for what you use",
    usage_based: true,
    features: [
      "API calls: $0.10 per 1,000 calls",
      "Storage: $0.50 per GB/month",
      "Compute: $1.00 per hour"
    ],
    usage_items: [
      %{
        id: "api_calls",
        name: "API Calls",
        price: "price_1NLhPDIWVkWpNCp7trePDpmi", # Replace with your API calls price ID
        unit: "per 1,000 calls",
        unit_amount: 10
      },
      %{
        id: "storage",
        name: "Storage",
        price: "price_1NWBYjIWVkWpNCp7pw4GpjI6", # Replace with your storage price ID
        unit: "per GB/month",
        unit_amount: 50
      },
      %{
        id: "compute_hours",
        name: "Compute Hours",
        price: "price_1NXCZkIWVkWpNCp7qx5HqkJ7", # Replace with your compute price ID
        unit: "per hour",
        unit_amount: 100
      }
    ]
  }
]

To get the list of prices for your usage-based product:

# To get the Product id
stripe products search --query="name:'API Usage'"

# To list all Prices that belong to the product
stripe prices list --product="your_product_id" --active=true

Replace the price IDs in the configuration with the actual IDs returned from the Stripe CLI.

Testing Usage-Based Billing

In the web app, login as the admin user and navigate to the Organisations page:

http://localhost:4000/app/orgs

Click on your Organisation, then click "Subscribe". You'll see the usage-based billing page with the pricing breakdown.

Choose to subscribe to usage-based billing. You'll be redirected to the Stripe checkout session.

Because Stripe is in test mode, you can use the test card 4242 4242 4242 4242. Fill out the details and hit the Subscribe button.

Once the subscription is created, you can start reporting usage to Stripe.

Reporting Usage

To charge customers based on their usage, you need to report usage data to Stripe. Here's how to implement usage reporting:

Create Usage Records

In your application code, report usage when customers consume resources:

defmodule UsageBillingDemo.Billing do
  def report_api_usage(subscription_item_id, quantity) do
    Stripe.UsageRecord.create(%{
      quantity: quantity,
      timestamp: System.system_time(:second),
      subscription_item: subscription_item_id
    })
  end

  def report_storage_usage(subscription_item_id, gb_used) do
    Stripe.UsageRecord.create(%{
      quantity: gb_used,
      timestamp: System.system_time(:second),
      subscription_item: subscription_item_id,
      action: "set"  # Use "set" for storage to replace previous value
    })
  end

  def report_compute_usage(subscription_item_id, hours) do
    Stripe.UsageRecord.create(%{
      quantity: hours,
      timestamp: System.system_time(:second),
      subscription_item: subscription_item_id
    })
  end
end

Usage Tracking Examples

Track API calls in your controllers:

defmodule UsageBillingDemoWeb.APIController do
  use UsageBillingDemoWeb, :controller
  
  def api_endpoint(conn, params) do
    # Your API logic here
    result = perform_api_operation(params)
    
    # Report usage
    if subscription_item_id = get_subscription_item_id(conn.assigns.current_org, "api_calls") do
      UsageBillingDemo.Billing.report_api_usage(subscription_item_id, 1)
    end
    
    json(conn, result)
  end
end

Track storage usage:

defmodule UsageBillingDemo.Storage do
  def update_storage_usage(org_id) do
    storage_used_gb = calculate_storage_usage(org_id)
    
    if subscription_item_id = get_subscription_item_id(org_id, "storage") do
      UsageBillingDemo.Billing.report_storage_usage(subscription_item_id, storage_used_gb)
    end
  end
end

Routes and Access Control

The usage-based billing system uses similar routes to the subscription system:

For org-based billing:

  • /app/org/:org_slug/subscribe - Subscribe to usage-based billing

  • /app/org/:org_slug/subscribe/success - Subscription success page

  • /app/org/:org_slug/billing - View current usage and billing

  • /app/org/:org_slug/subscribed_live - Access for subscribed organizations

For user-based billing:

  • /app/subscribe - Subscribe to usage-based billing

  • /app/subscribe/success - Subscription success page

  • /app/billing - View current usage and billing

  • /app/subscribed_live - Access for subscribed users

Add new routes for subscribed users in lib/usage_billing_demo_web/routes/subscription_routes.ex:

scope "/app", UsageBillingDemoWeb do
  pipe_through [:browser, :authenticated, :subscribed_user]
  # New routes for :user mode belong here
  live "/usage_dashboard", UsageDashboardLive
end

scope "/app", UsageBillingDemoWeb do
  pipe_through [:browser, :authenticated, :subscribed_org]
  # New routes for :org mode belong here  
  live "/org/:org_slug/usage_dashboard", OrgUsageDashboardLive
end

Production Deployment

Once your demo is ready, you can push it to production. There are three steps:

  1. Push the demo app to Fly.io

  2. Setup webhook endpoint on Stripe

  3. Configure Fly.io environment variables

Deploy to Fly.io

To start with Fly.io, use the following guide: [Fly.io deployment guide]

Setup Webhook Endpoint

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:

  1. Open the Stripe console

  2. Make sure you're in "Test mode"

  3. Click on "Developers" at the top of the page

  4. Then click on "Webhooks"

  5. Click on "Add endpoint"

  6. For the "Endpoint url" enter: https://your-host-name.fly.dev/webhooks/stripe

  7. Under "Select events to listen to" click "+ Select events" and add:

    • customer.subscription.created

    • customer.subscription.updated

    • invoice.payment_succeeded

    • invoice.payment_failed

  8. Click "Add endpoint"

Configure Environment Variables

Set the required environment variables in Fly.io:

fly secrets set \
  STRIPE_SECRET="sk_test_xxx" \
  STRIPE_WEBHOOK_SECRET="whsec_xxx" \
  STRIPE_PRODUCTION_MODE="false"

Going Live

All Stripe configuration up until this point has been setup exclusively in "Test mode". To prepare your app for production:

Using Live Mode with Stripe CLI

The Stripe CLI defaults to "Test mode". To work in "Live mode" you'll need to pass the --live flag:

stripe products list --live

Production Configuration

You'll need to repeat the Configure Stripe section, re-creating the products in "Live mode". Before updating the configuration in /config/config.exs, copy the test configuration to /config/dev.exs:

config :petal_pro, :billing_products, [
  %{
    id: "usage_based",
    name: "Usage-Based Billing",
    # ... test configuration
  }
]

That way you can stick with "Test mode" on your dev machine and use /config/config.exs for your production server.

Last updated

Was this helpful?