Metered billing with Elements
This guide walks you through how to use metered billing subscriptions for an email service that offers two levels of service, each with discounted pricing depending on how many emails are sent. It uses Stripe Elements to create a custom payment form you embed in your application. You can find code for an example implementation on GitHub.
You can also use Checkout if you don’t want to build a custom payment form, or one of our quickstart options if you aren’t ready to build an integration yet.
What you’ll build
This guide shows you how to:
- Model your subscriptions with Products and Prices
- Create a signup flow
- Collect payment information
- Create Subscriptions and charge customers
- Report usage
- Handle payment errors
- Let customers change their plan or cancel the subscription
How to model it on Stripe
API object definitions
Install Stripe libraries and tools
Install the Stripe client of your choice:
And install the Stripe CLI. The CLI provides the webhook testing you’ll need, and you can run it to create your products and prices.
To run the Stripe CLI, you must also pair it with your Stripe account. Run stripe login
and follow the prompts. For more information, see the Stripe CLI documentation page.
Set up webhooksServer
Because subscriptions are asynchronous, we recommend setting up webhooks to receive events from Stripe instead of polling for status updates. You can set up a webhook endpoint in the Dashboard, or with the Webhook Endpoints API.
The Stripe CLI provides a listen command for testing event monitoring during development.
Get started with these events:
Webhook | Why you need it |
---|---|
invoice.payment_failed | If the payment fails or the customer does not have a valid payment method, an invoice.payment_failed event is sent, and the subscription status becomes past_due . Use this event to notify your user that their payment has failed and request new card details. |
invoice.paid | Use this event to provision services. The status of the invoice will show up as paid. Store the status in your database to reference when a user accesses your service to avoid hitting rate limits. This event is also important for monitoring the status of payments that require customer authentication with 3D Secure. |
Here’s how to set up your webhook handler and verify the signature of the event.
Learn more about other events your webhook can listen for.
Create the business modelStripe CLI or Dashboard
You create your products and their pricing options with the Stripe CLI or in the Dashboard. This guide uses an email API platform as an example. It has two products with one pricing option for each:
- Basic option
- Tier one: 15 USD per month for 2,000 emails
- Tier two: An additional .00100 USD for each email after 2,000
- Premium option
- Tier one: 75 USD per month for 10,000 emails
- Tier two: An additional .00075 USD for each email after 10,000
To achieve this kind of pricing, you charge a flat fee and an additional amount based on how much customers use. With graduated tiers, customers initially pay the flat fee for the first 2,000 or 10,000 emails. If they upload more than that, they reach tier two and start paying for each additional email. You could also charge solely based on usage without the flat fee.
During each billing period, you create usage records for each customer and then Stripe adds them up to determine how much to bill for. This process is explained in a subsequent step but understanding the default behavior might impact how you create prices.
By default, Stripe sums up the quantity
of all usage records for a customer in a billing period and then multiplies the total by the unit_amount
of the price. If you want to change this behavior, you need to set aggregate_usage when you create the price. You can read more about this in our metered billing documentation but you can choose between using:
- The last usage record sent during a billing period
- The last usage record sent regardless of billing period
- The usage record with max (highest)
quantity
during a billing period
The default behavior is used in this guide so aggregate_usage
isn’t set manually.
Create the Stripe customerClient and Server
In this sample, you create a customer from the provided email.
On your application frontend, pass the customer email to a backend endpoint.
function createCustomer() { let billingEmail = document.querySelector('#email').value; return fetch('/create-customer', { method: 'post', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: billingEmail }) }) .then(response => { return response.json(); }) .then(result => { // result.customer.id is used to map back to the customer object // result.setupIntent.client_secret is used to create the payment method return result; }); }
On the backend, define the endpoint to create the customer object.
We recommend that you create the customer object together with your app’s user account. You should also store the response in your database, for example, customer.id
as stripeCustomerId
in your users
collection or table. This is to limit the number of calls you make to Stripe, for performance efficiency and to avoid possible rate limiting.
The server APIs this guide demonstrates are not secure by default. Make sure you properly enforce CORS by allowing access to these APIs only from your frontend application.
Example customer response:
{ "id": "cus_HAwLCi6nxPYcsl", "object": "customer", "address": null, "balance": 0, "created": 1588007418, "currency": "usd", "default_source": null, "delinquent": false, "description": "My First Test Customer",
Collect payment informationClient
Let your new customer choose a plan and provide payment information. In this example, the customer chooses between Basic and Premium.
Then use Stripe Elements to collect card information, and customize Elements to match the look-and-feel of the application.
Set up Stripe Elements
Stripe Elements is included with Stripe.js. Include the Stripe.js script on your checkout page by adding it to the head
of your HTML file.
Always load Stripe.js directly from js.stripe.com to remain PCI compliant. Don’t include the script in a bundle or host a copy of it yourself.
<head> <title>Subscription prices</title> <script src="https://js.stripe.com/v3/"></script> </head>
Create an instance of Elements with the following JavaScript:
// Set your publishable key: remember to change this to your live publishable key in production // See your keys here: https://dashboard.stripe.com/apikeys let stripe = Stripe(
); let elements = stripe.elements();'pk_test_f3duw0VsAEM2TJFMtWQ90QAT'
Add Elements to your page
Elements needs a place to live in your payment form. Create empty DOM nodes (containers) with unique IDs in your payment form and then pass those IDs to Elements.
<body> <form id="payment-form"> <div id="card-element"> <!-- Elements will create input elements here --> </div> <!-- We'll put the error messages in this element --> <div id="card-element-errors" role="alert"></div> <button type="submit">Subscribe</button> </form> </body>
Create an instance of an Element and mount it to the Element container:
let card = elements.create('card', { style: style }); card.mount('#card-element');
The card
Element simplifies the form and minimizes the number of fields required by inserting a single, flexible input field that securely collects all necessary card details. For a full list of supported Element types, refer to our Stripe.js reference documentation.
Use the test card number 4242 4242 4242 4242, any three-digit CVC number, any expiration date in the future, and any five-digit ZIP code.
Elements validates user input as it is typed. To help your customers catch mistakes, listen to change
events on the card
Element and display any errors.
card.on('change', function (event) { displayError(event); }); function displayError(event) { changeLoadingStatePrices(false); let displayError = document.getElementById('card-element-errors'); if (event.error) { displayError.textContent = event.error.message; } else { displayError.textContent = ''; } }
ZIP code validation depends on your customer’s billing country. Use our international test cards to experiment with other postal code formats.
Save payment details and create the subscriptionClient and Server
On the frontend, save the payment details you just collected to a payment method.
function createPaymentMethod(cardElement, customerId, priceId) { return stripe .createPaymentMethod({ type: 'card', card: cardElement, }) .then((result) => { if (result.error) { displayError(error); } else { createSubscription({ customerId: customerId, paymentMethodId: result.paymentMethod.id, priceId: priceId, }); } }); }
Define the createSubscription
function you just called, passing the customer, payment method, and price IDs to a backend endpoint.
This function calls other functions that are defined and explained in the following sections of this guide.
function createSubscription(customerId, paymentMethodId, priceId) { return ( fetch('/create-subscription', { method: 'post', headers: { 'Content-type': 'application/json', }, body: JSON.stringify({ customerId: customerId, paymentMethodId: paymentMethodId, priceId: priceId, }), }) .then((response) => { return response.json(); }) // If the card is declined, display an error to the user. .then((result) => { if (result.error) { // The card had an error when trying to attach it to a customer. throw result; } return result; }) // Normalize the result to contain the object returned by Stripe. // Add the additional details we need. .then((result) => { return { paymentMethodId: paymentMethodId, priceId: priceId, subscription: result, }; }) // Some payment methods require a customer to be on session // to complete the payment process. Check the status of the // payment intent to handle these actions. .then(handlePaymentThatRequiresCustomerAction) // No more actions required. Provision your service for the user. .then(onSubscriptionComplete) .catch((error) => { // An error has happened. Display the failure to the user here. // We utilize the HTML element we created. showCardError(error); }) ); }
On the backend, define the endpoint that creates the subscription for the frontend to call. The code updates the customer with the payment method, and then passes the customer ID to the subscription. The payment method is also assigned as the default payment method for the subscription invoices.
Here’s an example response. The minimum fields to store are highlighted, but you should store whatever your application will frequently access.
{ "id": "sub_HAwfLuEoLetEJ3", "object": "subscription", "application_fee_percent": null, "billing_cycle_anchor": 1588008574, "billing_thresholds": null, "cancel_at": null, "cancel_at_period_end": false, "canceled_at": null, "collection_method": "charge_automatically",
Provision access to your serviceClient and Server
To give the customer access to your service:
- Verify the subscription status is
active
. - Check the product the customer subscribed to and grant access to your service. Checking the product instead of the price gives you more flexibility if you need to change the pricing or billing interval.
- Store the
product.id
andsubscription.id
in your database along with thecustomer.id
you already saved.
On the frontend, you can implement these steps in the success callback after the subscription is created.
function onSubscriptionComplete(result) { // Payment was successful. if (result.subscription.status === 'active') { // Change your UI to show a success message to your customer. // Call your backend to grant access to your service based on // `result.subscription.items.data[0].price.product` the customer subscribed to. } }
It’s possible for a user to leave your application after payment information is submitted and before this function is called. Make sure to monitor the invoice.paid
event on your webhook endpoint to verify that the payment succeeded and that you should provision the subscription. Even though customers are charged at the end of the billing period for metered subscriptions, an initial invoice is still created for tracking purposes. The amount_due
and amount_paid
are set to 0
so the customer isn’t charged.
This is also good practice because during the lifecycle of the subscription, you need to keep provisioning in sync with subscription status. Otherwise, customers might be able to access your service even if their payments fail.
Report usageServer
Throughout each billing period, you need to report usage to Stripe so that customers are billed the correct amounts. You do this by creating usage records with a subscription item, quantity
used, and a timestamp
. How often you report usage is up to you, but this example uses a daily schedule. It’s best to send usage records in batches to reduce the number of API calls you need to make.
You also have the option of calculating usage yourself or having Stripe do it. In this example, Stripe does the calculations so the action
is set
. When reporting usage, use idempotency keys to ensure usage isn’t reported more than once in case of latency or other issues.
When you report usage, the timestamp
has to be within the current billing period, otherwise the call fails. If aggregate_usage
is set to sum
on the price, there’s an additional five minutes after the end of a billing period when you can report usage (this is to accommodate for clock drift). For all other aggregate_usage
values, the timestamp has to be within the billing period.
If you need to see the usage for a customer during a current period, you can retrieve the upcoming invoice and check the quantity
for each subscription_item
.
Manage payment authenticationClient and Server
If you support payment methods that require customer authentication with 3D Secure, the value of subscription.pending_setup_intent.status
is initially requires_action
. The response from the createSubscription
call looks like this:
{ "id": "sub_1ELI8bClCIKljWvsvK36TXlC", "object": "subscription", "status": "incomplete", ... "pending_setup_intent": { "status": "requires_action", "client_secret": "pi_91_secret_W9", "next_action": { "type": "use_stripe_sdk", ... }, ... } }
To handle this scenario, on the frontend notify the customer that authentication is required to complete payment and start the subscription. Retrieve the client secret for the payment intent, and pass it in a call to stripe.confirmCardSetup
.
function handlePaymentThatRequiresCustomerAction({ subscription, invoice, priceId, paymentMethodId }) { let setupIntent = subscription.pending_setup_intent; if (setupIntent && setupIntent.status === 'requires_action') { return stripe .confirmCardSetup(setupIntent.client_secret, { payment_method: paymentMethodId, }) .then((result) => { if (result.error) { // start code flow to handle updating the payment details // Display error message in your UI. // The card was declined (that is, insufficient funds, card has expired, etc) throw result; } else { if (result.setupIntent.status === 'succeeded') { // There's a risk of the customer closing the window before callback // execution. To handle this case, set up a webhook endpoint and // listen to setup_intent.succeeded. return { priceId: priceId, subscription: subscription, invoice: invoice, paymentMethodId: paymentMethodId, }; } } }); } else { // No customer action needed return { subscription, priceId, paymentMethodId }; } }
This displays an authentication modal to your customers, attempts payment, then closes the modal and returns context to your application.
Manage subscription payment failureClient and Server
Customers are charged at the end of the billing period, so it’s possible for payments to fail after the subscription is created because of changes to the payment method. For example, a customer might close their account because they lost their card, or maybe the card has insufficient funds to cover the payment.
When payments fail, an invoice.payment_failed
event is sent. If the status of the payment_intent
is requires_payment_method
, you need to bring the customer back to your site and collect new payment information. You can build a form similar to the one you built earlier in this guide to collect payment information. When you have the payment method ID, you can update the customer with the new payment method, and assign it as the new default for the subscription.
Cancel the subscriptionClient and Server
It’s common to allow customers to cancel their subscriptions. This example adds a cancellation option to the account settings page.
The example collects the subscription ID on the frontend, but you will most likely get this information from your database for your logged in user.
function cancelSubscription() { return fetch('/cancel-subscription', { method: 'post', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ subscriptionId: subscriptionId, }), }) .then(response => { return response.json(); }) .then(cancelSubscriptionResponse => { // Display to the user that the subscription has been canceled. }); }
On the backend, define the endpoint for your frontend to call.
After the subscription is canceled, update your database to remove the Stripe subscription ID you previously stored, and limit access to your service.
When a subscription is canceled, it cannot be reactivated. Instead, collect updated billing information from your customer, update their default payment method, and create a new subscription with their existing customer record.
Test your integration
To make sure your integration is ready for production, you can work with the following test cards. Use them with any CVC, postal code, and future expiration date.
Card number | What it does |
---|---|
Succeeds and immediately creates an active subscription. | |
Requires authentication. confirmCardPayment() will trigger a modal asking for the customer to authenticate. Once the user confirms, the subscription will become active. See manage payment authentication. | |
Succeeds on initial attaching to Customer object, but fails when the customer is charged at the end of the billing period. The payment_intent has a value of requires_payment_method . See manage subscription payment failure step. |
Check out the documentation for testing Billing for more information and ways to test your integration.