DEV Community

Cover image for Stop Fighting Stripe's Usage API: Build Simple Metering That Just Works
Jbee - codehooks.io
Jbee - codehooks.io

Posted on

Stop Fighting Stripe's Usage API: Build Simple Metering That Just Works

TL;DR: Stripe's usage-based billing API is overcomplicated. Instead of wrestling with meters, meter events, and usage records, just capture your own usage data and create invoices directly when the billing period ends.


The Stripe Usage API Pain

Don't get me wrong - Stripe is fantastic. Their payment processing, subscriptions, and checkout are best-in-class. I've used Stripe for years and recommend it to everyone.

But their usage-based billing API? That's where things get weird.

If you've ever tried to implement usage-based billing with Stripe, you know the frustration.

First, there's the terminology soup: Meters, Meter Events, Usage Records - three different concepts just to track "customer used X". Wait, there's also "Usage Reporting" and the "Billing Meter Events API". Which one do you use? The docs bounce you between these pages like a pinball machine, and half the examples are deprecated.

Then you discover the aggregation limitations. Stripe meters only support sum and count_distinct. Need max for peak storage billing? avg for response time SLAs? min for guaranteed minimums? You're on your own.

Here's what really hurts: events are write-only. Once you send a meter event, you can't query it back. Made a mistake? Sent duplicates? You won't know until the invoice looks wrong. And when a customer disputes a charge, good luck - aggregation happens in Stripe's black box with zero visibility into how totals are calculated.

Deduplication is entirely your problem. Network hiccup caused a retry? You might double-bill your customer. Stripe's idempotency keys help, but you're responsible for generating and managing them.

Want weekly billing cycles? Custom 30-day periods? Real-time usage dashboards for your customers? The API isn't built for that. And don't even think about changing a meter's configuration - want a different aggregation? Create a new meter. Rename an event? New meter. It's append-only all the way down.

A Better Way: Own Your Metering

What if you just:

  1. Captured usage events in your own database
  2. Aggregated them your way (hourly, daily, monthly - whatever you need)
  3. Created Stripe invoices when you're ready

No mysterious black box. No "why is this number wrong?" No vendor lock-in on your core business data.

And here's the kicker: this pattern works with any payment provider - Stripe, PayPal, Paddle, LemonSqueezy, or even your own invoicing system. You own the metering, you choose where to send the bill.

The Solution: Self-Hosted Usage Metering

I built a serverless metering template that does exactly this:

Your App  →  Metering API  →  Webhook  →  Invoice
Enter fullscreen mode Exit fullscreen mode

Configure What to Track

Define your metrics and aggregation in systemconfig.json:

{
  "periods": ["monthly"],
  "events": {
    "api.calls": { "op": "sum" },
    "storage.bytes": { "op": "max" },
    "compute.seconds": { "op": "sum" }
  },
  "webhooks": [{
    "url": "https://your-app.com/webhooks/create-invoice",
    "secret": "whsec_your_secret",
    "enabled": true
  }]
}
Enter fullscreen mode Exit fullscreen mode

That's it. Three metrics, monthly aggregation, webhook when the period ends.

Capture Usage

Now your app sends usage events as they happen:

// Single event
await fetch('https://your-metering.codehooks.io/usage/api.calls', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'x-apikey': API_KEY },
  body: JSON.stringify({
    customerId: 'sub_ABC123',  // Use Stripe subscription ID!
    value: 1
  })
});

// Or batch for high-volume
await fetch('https://your-metering.codehooks.io/usagebatch', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'x-apikey': API_KEY },
  body: JSON.stringify([
    { eventType: 'api.calls', customerId: 'sub_ABC123', value: 1 },
    { eventType: 'storage.bytes', customerId: 'sub_ABC123', value: 50000 }
  ])
});
Enter fullscreen mode Exit fullscreen mode

Pro tip: Use the Stripe subscription ID as customerId. This makes invoice creation trivial.

Get the Webhook

When a period completes (e.g., end of month), you get a webhook:

{
  "type": "aggregation.completed",
  "customerId": "sub_ABC123",
  "period": "monthly",
  "data": {
    "periodStart": "2025-01-01T00:00:00.000Z",
    "periodEnd": "2025-01-31T23:59:59.999Z",
    "events": {
      "api.calls": 15420,
      "storage.bytes": 1073741824,
      "compute.seconds": 3600
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Creating the Stripe Invoice

Now the easy part - turn that webhook into an invoice. Here's a Node.js example, but this works in any language - Python, Go, Ruby, whatever your stack uses:

// webhook-handler.js (Node.js example)
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

app.post('/webhooks/create-invoice', async (req, res) => {
  // Verify webhook signature (HMAC-SHA256)
  if (!verifySignature(req)) {
    return res.status(401).send('Invalid signature');
  }

  const { customerId, period, data } = req.body;

  // customerId IS the Stripe subscription ID
  const subscriptionId = customerId;

  // Get subscription to find the customer
  const subscription = await stripe.subscriptions.retrieve(subscriptionId);

  // Add fixed monthly plan fee
  await stripe.invoiceItems.create({
    customer: subscription.customer,
    amount: 4900,  // $49.00 base plan
    currency: 'usd',
    description: "`Pro Plan - ${data.periodStart.slice(0,7)}`"
  });

  // Add usage-based charges
  const pricing = {
    'api.calls': 0.001,           // $0.001 per call
    'storage.bytes': 0.00000001,  // $0.01 per GB
    'compute.seconds': 0.0001     // $0.0001 per second
  };

  for (const [metric, value] of Object.entries(data.events)) {
    const amount = Math.round(value * pricing[metric] * 100); // cents

    if (amount > 0) {
      await stripe.invoiceItems.create({
        customer: subscription.customer,
        amount,
        currency: 'usd',
        description: "`${metric}: ${value.toLocaleString()} units`"
      });
    }
  }

  // Create and finalize the invoice
  const invoice = await stripe.invoices.create({
    customer: subscription.customer,
    auto_advance: true,  // Auto-finalize and send
    collection_method: 'charge_automatically'
  });

  console.log(`Created invoice ${invoice.id} for ${subscriptionId}`);
  res.json({ invoiceId: invoice.id });
});
Enter fullscreen mode Exit fullscreen mode

That's it. No meters. No meter events. No usage records. Just data you control and invoices that make sense.

Why This Is Better

Stripe Usage API Self-Hosted Metering
Data lives in Stripe Data lives in YOUR database
Limited aggregation (sum only) 7 operations: sum, avg, min, max, count, first, last
Fixed periods Flexible: hourly, daily, weekly, monthly, yearly
Hard to debug Query your events anytime
Vendor lock-in Portable - it's just JSON
Complex setup Deploy in 5 minutes

Get Started

First, create a free account at codehooks.io if you don't have one.

# Create your metering backend
npx codehooks create mymetering --template saas-metering-webhook
cd mymetering

# Configure your events
vim systemconfig.json

# Deploy
npx codehooks deploy

# Start sending events
curl -X POST https://your-project.codehooks.io/usage/api.calls \
  -H "x-apikey: YOUR_KEY" \
  -d '{"customerId":"sub_ABC123","value":1}'
Enter fullscreen mode Exit fullscreen mode

The full template includes:

  • Batch event capture (POST /usagebatch)
  • Configurable aggregation periods and operations
  • HMAC-signed webhooks
  • Event querying API
  • Cron-based batch processing

Check out the full source code and stop fighting with Stripe's usage API.


What's your experience with usage-based billing? Drop a comment below - I'd love to hear your war stories!


Built with Codehooks.io - Deploy serverless micro backends in seconds

Top comments (0)