How to build usage-based billing with Stripe and Webflow

Learn how to record metered usage from a Webflow Cloud app and bill it through Stripe.

How to build usage-based billing with Stripe and Webflow

Ismail Ajagbe
Technical Author
View author profile
Ismail Ajagbe
Technical Author
View author profile
Table of contents

Stripe now has three generations of usage billing in its docs, and with it, you can deploy usage-based billing on Webflow.

Usage billing looks like a pricing decision and behaves like an engineering one. The invoice at the end of the month is only as trustworthy as the events you recorded during it, and there is no way to reconstruct a request you never metered.

What makes this harder than it should be in 2026 is that Stripe now has three generations of usage billing in its documentation, and two of them are the wrong place to start a new build.

This guide covers which one you should be on, then builds the metering and billing loop on Webflow Cloud, where your app already lives beside the marketing site.

Which Stripe usage billing product should you build on?

Stripe now routes new usage-based integrations to Metronome, keeps basic usage-based billing fully supported for anyone already on it, and marks the original usage records approach as legacy. Picking the wrong one means rebuilding the integration later.

Stripe states that Metronome is primary for all new integrations, with Metronome handling metering, rating and billing while Stripe handles payment collection, tax and revenue recognition.

In the same breath, it says that if you already have a basic usage-based billing integration, you don't need to migrate, and Stripe will continue to support it fully.

For a new build, the default answer is Metronome; basic is the exception, not the starting point. The comparison below is what each choice costs you, including the one category where basic is still clearly ahead:

What you need Basic usage-based billing Metronome
Pay-as-you-go pricing Supported Supported
Prepaid credits and drawdown Not available Supported
Enterprise contracts, commits, minimums Not available Supported
Ramp schedules and dimensional pricing Not available Supported
Real-time usage visibility Not available Supported
High-volume event ingestion Limited Supported
Connect, Adaptive Pricing, Workflows, Stripe Dashboard Supported Not available
Stripe Checkout Supported Limited; needs custom API calls and webhook configuration
What you need → Basic usage-based billing → Metronome
Pay-as-you-go pricing
Supported
Supported
Prepaid credits and drawdown
Not available
Supported
Enterprise contracts, commits, minimums
Not available
Supported
Ramp schedules and dimensional pricing
Not available
Supported
Real-time usage visibility
Not available
Supported
High-volume event ingestion
Limited
Supported
Connect, Adaptive Pricing, Workflows, Stripe Dashboard
Supported
Not available
Stripe Checkout
Supported
Limited; needs custom API calls and webhook configuration

Stripe does say basic "works best for businesses with pay-as-you-go pricing models", but that sentence sits under a heading addressed to people who already have a Billing Meters integration, so it is not an argument for starting there.

The bottom two rows are the real argument for basic on a new build. Stripe documents interoperability gaps between Metronome and several of its own products: Connect, Adaptive Pricing, Workflows and the Stripe Dashboard are unsupported, and Checkout works only with custom API calls and webhook configuration. If your product depends on any of those, basic is the defensible choice and the rest of this guide applies.

The rest of this guide builds the basic usage-based billing path, which fits two readers: you already bill through Billing Meters, or you need the Stripe products Metronome doesn't yet cover. If neither is true, stop here and start on Metronome.

What do you need to build usage billing with Stripe and Webflow?

You need a Stripe account with a meter and a usage-based price, a Webflow Cloud project to record events from, and somewhere durable to keep your own copy of what you counted.

Here is the full list:

  • A Stripe account, with a billing meter and a price that references it
  • A Webflow Cloud project running a Next.js app, with Next.js 15 or higher and Node.js 22 or later locally
  • A Stripe secret key and a webhook signing secret, stored as environment variables
  • Somewhere to record usage on your own side, such as the Webflow Cloud SQLite binding so that you can reconcile against Stripe later

Webflow Cloud runs on every site plan including the free Starter tier, though mounting the app to a custom domain needs Premium or higher. Once these are in place, everything hinges on one habit: never let a billable action happen without recording it, and never record it twice. Here's how.

5 steps to record usage and bill it through Stripe

The build is a meter, a price, a route that records events, a webhook for billing outcomes, and your own ledger to compare against Stripe.

The order matters here, because each step depends on a decision made in the one before it.

1. Create the meter and decide what one unit means

In the Stripe Dashboard, create a billing meter and give it an event name. That name is the string your code will send; it matches the meter's event_name and is capped at 100 characters.

This is also nearly irreversible: once you configure a meter, Stripe won't let you change anything on it beyond the display name. The harder part of this step isn't in Stripe. Decide, in writing, what a single billable unit is: one API call, one thousand tokens, one seat-day, one gigabyte-hour.

Disputed invoices usually trace back to this definition being fuzzy, because a customer who reads "requests" differently from you is not wrong; they are reading an ambiguous word.

Note the aggregation too. Stripe offers three formulas, Sum, Count and Last, so a meter that bills on the most recently reported value behaves differently from one that adds up requests, and there is no peak or maximum option to design around.

You finish this step with an event name written down and a one-sentence definition of a unit that a support agent could read out loud.

2. Attach the meter to a price and a subscription

Create a product and a recurring price that references the meter, then subscribe your customer. The subscription makes usage billable; a meter on its own records numbers that never make it to an invoice.

This is also where you lock in the pricing model. A flat per-unit rate is the simplest option, and tiered pricing is available, but if you want credits that draw down or a minimum commitment, stop and revisit the Metronome decision above rather than approximating it with coupons.

Keep the Stripe customer ID somewhere you can reach it from the app, because every meter event you send has to name the customer it belongs to. Storing it against your own user record at signup keeps it where you need it.

End this step with a live subscription whose upcoming invoice shows a usage line at zero.

3. Record meter events from a Route Handler

Your server should record usage when the billable thing happens, not in the browser. A client that can call your billing endpoint can under-report.

The handler is small:

// app/api/usage/route.ts
import { NextRequest, NextResponse } from 'next/server'
import Stripe from 'stripe'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string, {
  maxNetworkRetries: 2,
})

export async function POST(request: NextRequest) {
  const { customerId, units, requestId } = (await request.json()) as {
    customerId: string
    units: number
    requestId: string
  }

  if (!customerId || !requestId) {
    return NextResponse.json({ error: 'Missing customer or request ID' }, { status: 400 })
  }

  await stripe.billing.meterEvents.create({
    event_name: 'api_request',
    payload: {
      // Both keys are the meter's defaults. If you renamed them on the
      // meter, these must match the meter, not the other way round.
      stripe_customer_id: customerId,
      // value is a string, even though it holds a number.
      value: String(units),
    },
    // Stripe enforces uniqueness on this within a rolling window of
    // at least 24 hours, so reuse it on a retry. Max 100 characters.
    identifier: requestId,
  })

  return NextResponse.json({ recorded: true })
}

Three details in there are easy to get wrong. The payload keys are the meter's defaults: stripe_customer_id and value, and if you renamed either on the meter, the payload has to match the meter rather than the reverse.

The value is sent as a string even though it represents a number. Stripe enforces uniqueness on an identifierwithin a rolling period of at least 24 hours, so a retry inside that window that reuses the identifier is safe, while one that generates a new identifier bills twice.

A redelivery arriving days later falls outside the window, which is a second argument for the ledger in the next step.

Two limits shape how you call this at volume. Stripe caps meter events at 1,000 per second per account, and separately allows only one concurrent call per customer per meter, so a burst of parallel requests for one customer will collide. Pre-aggregating before you send is the usual answer.

Deploy this and a call should return { recorded: true }, with the event appearing against the customer in the Dashboard shortly afterward rather than instantly.

4. Keep your own ledger and reconcile against Stripe

Write every billable action to your own store as well as to Stripe. This feels redundant for about a week, and then it is the only thing that lets you answer a customer asking why their invoice went up.

Stripe processes meter events asynchronously, which their documentation says plainly: aggregated usage in meter event summaries and on upcoming invoices might not immediately reflect events you just sent.

That has two consequences worth designing around. A usage dashboard that reads from Stripe will lag, so read it from your own ledger instead. And a reconciliation job that compares your counts with Stripe's has to allow for that delay rather than alerting on every difference.

The Webflow Cloud SQLite binding is close enough to the app to make this cheap and a single table of customer, event name, quantity, identifier and timestamp is enough. Use the same identifier you sent to Stripe so the two sides can be joined.

After this step, you can produce, for any customer and any period, the list of events you believe you charged for.

5. Handle billing outcomes with a verified webhook

Metering is only half the loop. The other half is reacting when an invoice fails, a subscription lapses, or usage crosses a threshold you care about, and that arrives as a webhook.

Verification on Webflow Cloud has one specific requirement worth knowing before you debug it:

// app/api/stripe-webhook/route.ts
import { NextRequest, NextResponse } from 'next/server'
import Stripe from 'stripe'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string, {
  maxNetworkRetries: 2,
})

export async function POST(request: NextRequest) {
  const signature = request.headers.get('stripe-signature')
  if (!signature) {
    return NextResponse.json({ error: 'Unsigned request' }, { status: 400 })
  }

  // Read the raw text. Parsing first changes the bytes and the
  // signature will never match.
  const body = await request.text()

  let event: Stripe.Event
  try {
    // Async form: the Workers build uses Web Crypto, which cannot
    // run synchronously.
    event = await stripe.webhooks.constructEventAsync(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET as string
    )
  } catch {
    return NextResponse.json({ error: 'Bad signature' }, { status: 400 })
  }

  if (event.type === 'invoice.payment_failed') {
    const invoice = event.data.object as Stripe.Invoice
    await flagAccountForDunning(invoice.customer as string)
  }

  return NextResponse.json({ received: true })
}

The Workers build of the Stripe library selects a Web Crypto provider, and that provider only does asynchronous work, so the synchronous constructEvent throws rather than verifying.

Use constructEventAsync, which Stripe's own Cloudflare Worker sample uses. Read the body as raw text as well, because parsing and re-serializing changes the bytes the signature covers.

Once this is live, a failed payment should move the account into whatever dunning state you use, and you should be able to trigger it from the Stripe CLI rather than waiting for a real card to decline.

What causes Stripe usage billing to fail on Webflow Cloud?

Usage billing problems are often silent, which is what makes them expensive. Nothing errors, the product keeps serving traffic, and the first symptom is an invoice that nobody can explain to the customer receiving it.

Four failures come up repeatedly, and the first is the one to test for deliberately before launch.

Usage never appears on the invoice

Cause: The meter event is accepted but isn't connected to anything billable. Usually, the subscription uses a price that doesn't reference the meter, or the event name in the code doesn't match the meter's event name exactly, including case.

Fix: Check the event name character for character against the meter, then confirm the customer's subscription is on a price that references that meter. A meter event for a valid customer with no matching subscription is accepted and recorded, so a 200 response tells you nothing about whether it will ever be billed.

Stripe does report the other cases asynchronously rather than at the API: subscribe to v1.billing.meter.no_meter_found and v1.billing.meter.error_report_triggered, which surface a mismatched event name, an unknown customer, a missing value or a timestamp too far in the past.

Customers are billed twice for the same action

Cause: A retry sent a second meter event with a new identifier. Anything that can run twice will eventually run twice: a queue redelivery, a client retry, a deploy that replays a job.

Fix: Derive the identifier from the thing being billed rather than generating it at send time, so the same action always produces the same identifier. If you already have a request ID or a job ID, use that.

A duplicate send inside Stripe's rolling uniqueness window is then rejected rather than charged, and your own ledger shows two attempts against one identifier, which is what tells you the retry happened at all.

Randomly generated identifiers are the failure mode here precisely because they look correct in testing: a single manual run never repeats, so the bug only appears under production retry conditions.

Catching it quickly matters, because Stripe only lets you cancel a meter event within 24 hours of sending it, and canceling usage that has already landed on a finalized invoice does not correct that invoice.

The usage dashboard disagrees with Stripe

Cause: Reading current usage from Stripe and expecting it to be immediate. Stripe aggregates meter events asynchronously, so a summary read moments after sending will legitimately be behind.

Fix: Read the customer-facing number from your own ledger, which is authoritative for what you counted, and treat Stripe as authoritative for what gets charged. Reconcile the two on a schedule with a tolerance window rather than comparing them in real time, and alert only when a difference persists past the delay.

The distinction matters when a customer disputes a charge: you need to say what you counted and when you sent it, independent of what Stripe has finished aggregating. A dashboard wired directly to Stripe cannot answer that question, and it will also show customers a number that moves for reasons they cannot see.

The build fails after adding the billing routes

Cause: An export const runtime = 'edge' directive in one of the new route files. Webflow Cloud deploys Next.js through the OpenNext Cloudflare adapter, which does not support the Next.js edge runtime.

Fix: Delete the line and redeploy, since Route Handlers already run on the Workers runtime without it. Search the whole project rather than the file you last edited, because one stray directive anywhere fails the whole build.

Check this first because Webflow's own bring-your-own-app page still tells Next.js readers to add the directive, so the instruction you followed may be what's breaking the build. Billing projects hit it often, since payment routes are frequently lifted from examples written for other hosting platforms where the directive is either required or harmless.

What you can build next with Stripe and Webflow

Once usage is metered and reconciled, the interesting work is what you do with the number before the invoice arrives: usage alerts at a threshold, a soft cap that degrades rather than cuts off, or an in-app meter that shows customers where they stand.

If your product also sells one-off purchases alongside the subscription, our Stripe digital products guide covers that flow, and the authentication and payments guide covers the account layer underneath both. For no-code connection routes, see the Webflow and Stripe integration.

For deeper customization beyond what those cover, Webflow's developer docs set out the Route Handler, storage and environment options available on Webflow Cloud.

Frequently asked questions

Should I use Metronome or basic usage-based billing?

Stripe says Metronome is its primary platform for all new integrations. Choose basic only if you already bill through Billing Meters or if you need Connect, Adaptive Pricing, Workflows or the Stripe Dashboard, which Metronome does not yet support.

Why is my usage number different from Stripe's?

Stripe processes meter events asynchronously, so recently sent events may not show up in summaries or on the upcoming invoice yet. Show customers the figure from your own ledger and reconcile with Stripe on a schedule that allows for the delay.

What stops a retry from billing a customer twice?

The identifier on the meter event. Stripe enforces uniqueness on it within a rolling period of at least 24 hours, so reusing the same identifier on a retry inside that window is safe. Derive it from the billed action rather than generating a fresh one at send time.

Can I record usage from the browser?

You can, but you shouldn't. If the client can call it, the client can also decline to call it or call it with different numbers. Record usage server-side when the billable work happens.

Do I still need my own usage records if Stripe has them?

Yes. Your ledger lets you answer billing questions, investigate disputes, and detect events that never reached Stripe. Stripe is authoritative for what gets charged; your store is authoritative for what you counted.


Last Updated
September 4, 2026
Category

Related articles


verifone logomonday.com logospotify logoted logogreenhouse logoclear logocheckout.com logosoundcloud logoreddit logothe new york times logoideo logoupwork logodiscord logo
verifone logomonday.com logospotify logoted logogreenhouse logoclear logocheckout.com logosoundcloud logoreddit logothe new york times logoideo logoupwork logodiscord logo

Get started for free

Try Webflow for as long as you like with our free Starter plan. Purchase a paid Site plan to publish, host, and unlock additional features.

Get started — it’s free
Watch demo

Try Webflow for as long as you like with our free Starter plan. Purchase a paid Site plan to publish, host, and unlock additional features.