You built an AI feature. You want to charge for what people actually use — tokens, requests, generations, seats — not a flat $20 that loses money on your power users and overcharges everyone else. Stripe is the obvious rail.

Here's the thing most tutorials won't tell you: the usage-based billing API you'll find in older guides no longer exists. Stripe removed the legacy usage_records endpoint in its 2025-03-31 "Basil" API release. You can't create legacy metered prices anymore, and the subscription_items/.../usage_records call is gone unless you pin an ancient API version. If you're integrating today, meters are the only supported path — and they're genuinely better. This is the working flow.

The mental model#

Metered billing is four objects that click together:

  1. Meter — a named counter (ai_tokens) that defines how usage aggregates.
  2. Price — a per-unit price that points at the meter ($0.01 / unit).
  3. Subscription — attaches that price to a customer. No quantity; usage comes from events.
  4. Meter events — you POST one every time usage happens; Stripe tallies them.

At the end of each billing period, Stripe aggregates the events per customer with the meter's formula, multiplies by the price, and writes the invoice line. That's the whole system.

The single most important shift: aggregation lives on the meter now, not the price. In the old world you set aggregate_usage on the price. In the new world the meter owns it, and the only formulas are sum, count, and last.

Step 1 — Create the meter#

The meter names the event you'll send and how to roll it up. default_aggregation.formula is sum (add all values — per-token billing), count (number of events — per-request billing), or last (the last value in the period — good for gauge/seat "current level" billing).

curl https://api.stripe.com/v1/billing/meters \
  -u "$STRIPE_SECRET_KEY:" \
  -d "display_name=AI tokens used" \
  -d "event_name=ai_tokens" \
  -d "default_aggregation[formula]=sum" \
  -d "value_settings[event_payload_key]=value" \
  -d "customer_mapping[type]=by_id" \
  -d "customer_mapping[event_payload_key]=stripe_customer_id"
const meter = await stripe.billing.meters.create({
  display_name: 'AI tokens used',
  event_name: 'ai_tokens',
  default_aggregation: { formula: 'sum' },
  value_settings: { event_payload_key: 'value' },
  customer_mapping: { type: 'by_id', event_payload_key: 'stripe_customer_id' },
});
// meter.id -> "mtr_..."

event_name is the string every usage event will carry. value_settings.event_payload_key says which key in the event payload holds the number (defaults to value); customer_mapping says which key identifies the customer (defaults to stripe_customer_id).

Step 2 — Create a metered price that points at the meter#

The price sets the money. usage_type: 'metered' and recurring.meter are the metered-specific fields. Note there is no aggregate_usage here anymore — that moved to the meter.

const price = await stripe.prices.create({
  currency: 'usd',
  unit_amount: 1,               // $0.01 per unit
  recurring: { interval: 'month', usage_type: 'metered', meter: meter.id },
  product_data: { name: 'AI usage' },
});

Step 3 — Subscribe the customer#

Metered items take no quantity — the quantity is whatever the meter events add up to.

const subscription = await stripe.subscriptions.create({
  customer: 'cus_123',
  items: [{ price: price.id }],
});

Step 4 — Report usage as it happens#

Every time the customer burns tokens (or makes a request, or whatever you meter), POST a meter event. event_name must match the meter. The payload is a string map — send "1500", not 1500. Include an identifier (a UUID) so retries are safe: Stripe deduplicates the same identifier within a rolling ~24h window.

curl https://api.stripe.com/v1/billing/meter_events \
  -u "$STRIPE_SECRET_KEY:" \
  -d "event_name=ai_tokens" \
  -d "payload[stripe_customer_id]=cus_123" \
  -d "payload[value]=1500" \
  -d "identifier=$(uuidgen)"
await stripe.billing.meterEvents.create({
  event_name: 'ai_tokens',
  payload: { stripe_customer_id: 'cus_123', value: '1500' }, // strings!
  identifier: crypto.randomUUID(),
});

Wire that call in right where you already know the cost — the same place you log a completion's token count, increment a request counter, or provision a seat.

Step 5 — The invoice writes itself#

Meter events are processed asynchronously — they will not show up in aggregates or the upcoming invoice instantly, so never gate your UI on immediate reflection. At period close Stripe aggregates per customer using the formula and generates the line. Want to see the running total before billing? Read the event summaries:

const summaries = await stripe.billing.meters.listEventSummaries(meter.id, {
  customer: 'cus_123',
  start_time: periodStart,   // minute-aligned unix seconds
  end_time: periodEnd,
});

The gotchas that will actually bite you#

Takeaway#

Metered billing on Stripe is now four objects and one rule: the meter owns aggregation, the price owns money, and you POST an event every time usage happens. Ignore any guide that reaches for usage_records — that API is gone. Build on meters, send string payloads with idempotent identifiers, remember events are async, and test against a future-dated clock. Once it's wired, you charge exactly for what you deliver, which is the only pricing model that survives a cheap-inference world where your customers' usage — and your costs — swing wildly from one account to the next.