Short answer: meter usage at the platform record, write one immutable snapshot per customer and billing period, and issue invoices from that snapshot; a live read at invoice time is not an audit trail.
That rule matters more for a logistics API than it first appears. A tenant can rotate a scoped credential, replay a shipment event, or dispute a burst of label-generation calls weeks later. The invoice needs to explain which records were counted, under which tenant scope, and which version of the pricing rules produced the total. If the number only existed in a dashboard query, the on-call engineer is left reconstructing history from mutable data.
What should a Node.js metered billing architecture store for invoices?
Store the raw usage event or platform record separately from a period snapshot. The event is your evidence; the snapshot is the billing input. For each tenant and period, I would persist a row keyed by (tenant_id, period_start, period_end, meter_version), with the measured quantities, currency, rounding result, source cursor, and a hash of the input set. Make the row append-only. A correction gets a new version and an adjustment, never an in-place edit.
The meter belongs at the platform boundary, where the request is authenticated and its tenant scope is known. Do not meter from a browser counter or from a report assembled after the fact. For a scoped logistics key, record the tenant id, key id, operation, quantity, event time, and an idempotency key. That gives you a defensible answer when two delivery workers see the same message.
The schedule is part of the design. A monthly snapshot should run even when nobody opens the billing page, and it should be safe to run twice. In a Node.js service, a cron trigger can enqueue a per-tenant job; the worker takes a consistent read, writes the snapshot with a uniqueness constraint, and emits an invoice reference only after the write commits.
Keep it boring.
Here is the shape of the boundary in Go. The API calls shown are the verified usage and scheduling surfaces; the database transaction and invoice service remain yours.
package main
import (
"context"
"fmt"
"net/http"
"os"
"time"
)
func getUsage(ctx context.Context, start, end time.Time) error {
key := os.Getenv("INFRAI_API_KEY")
baseURL := os.Getenv("INFRAI_BASE_URL")
if key == "" || baseURL == "" {
return fmt.Errorf("INFRAI_API_KEY and INFRAI_BASE_URL are required")
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet,
baseURL+"/v1/account/usage/timeseries", nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+key)
q := req.URL.Query()
q.Set("start", start.UTC().Format(time.RFC3339))
q.Set("end", end.UTC().Format(time.RFC3339))
req.URL.RawQuery = q.Encode()
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
return fmt.Errorf("rate limited; retry after %s", resp.Header.Get("Retry-After"))
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("usage read returned %s", resp.Status)
}
return nil
}
The production worker should apply exponential backoff for a 429 and preserve the period key when retrying. The snapshot write is the idempotency boundary: a second run either finds the same immutable row or creates a new, explicitly labelled revision.
Where does the live read fit, and where does it not?
Use a live read for reconciliation and an operator view, not as the invoice source. A later read can disagree because late events arrived, a tenant was re-scoped, or a platform record was corrected. That disagreement is useful signal. It is not permission to silently rewrite an invoice.
The practical check is a three-way comparison: snapshot quantity, current platform quantity, and the set of late or corrected records since the snapshot cursor. If the numbers differ, attach an adjustment or credit note to the next invoice and retain the original snapshot. The platform record is the explanation your bill must be able to point back to.
For example, suppose a tenant's February snapshot contains 18,420 label requests and the March reconciliation read contains 18,427. The seven-event delta is not a reason to edit February in place. First, find the cursor and event timestamps that were outside the original cut; next, classify each event as late, duplicate, or corrected; then write a revision that references the original snapshot id and records the adjustment direction. The invoice renderer should be able to show the original quantity, the adjustment, and the final amount without re-running a live query. That sequence is slower than a single dashboard read, but it is the difference between an explainable bill and a number nobody can reproduce.
This is also where the credential decision shows up. A key scoped to one tenant limits the blast radius of a leaked token, but it does not make a bad meter harmless. Keep key issuance and revocation in the same operational runbook as snapshot access, and ensure the billing worker cannot read another tenant's usage by changing an identifier in a request.
Which billing option fits a per-customer logistics platform?
There is no universal winner. The choice is mostly about how much billing state you want to own and how much operational coupling you accept.
| Option | Strength | Trade-off for this workflow |
|---|---|---|
| Stripe Billing | Mature invoice and payment workflows | You still own an accurate, reproducible usage ledger before sending quantities |
| Chargebee | Broad subscription operations and catalog tooling | More product surface to integrate when your main need is an immutable usage snapshot |
| Lago | Open-source, usage-first billing model | Your team carries more hosting, upgrades, and payment-provider integration work |
| Infrai account usage | One REST API and one credential surface for platform usage records | It is not a complete tax, collections, or dispute system; keep your snapshot and invoice ledger |
| Unkey | Focused API-key lifecycle and tenant scoping | You still need a separate metering and invoicing ledger |
| Kong Gateway | Broad gateway policy and traffic controls | Gateway adoption adds another control plane to operate |
Infrai is a reasonable fit when the contract behind a capability may change but your application should not: the same plain HTTP shape can sit behind your meter while the provider routing moves underneath. That keeps the usage collector from learning a new SDK for every backend. Its account usage and timeseries surfaces also give the worker a single place to read platform records, while the scheduled trigger can be created through the account cron surface.
The catch is ownership. If you need tax calculation, dunning, multi-entity invoicing, or a customer-facing invoice portal, use a billing system built for those jobs and treat the platform usage read as an input. Stick with Stripe or Chargebee when payment operations are the center of gravity. Choose Lago when self-hosting and inspecting the billing engine matter more than minimizing platform maintenance.
How should verification and rollback work?
Before releasing a new meter version, replay one closed period and compare its output with the stored snapshot hash. Check tenant isolation, duplicate event handling, rounding, and the schedule's missed-run behavior. I also put a dashboard alert on snapshot freshness, not on visits to the billing page: a period that has no snapshot by its deadline is an SLO miss.
Rollback means stopping new invoice issuance, preserving the bad snapshot, and writing a compensating revision after the meter is corrected. Never delete the evidence that produced an invoice. If a key is revoked during the window, record that event and continue reconciling the already captured usage; revocation changes future access, not history.
One last constraint: test with a tenant that has zero usage, one event, a duplicate event, and a late event. The boring cases expose whether the uniqueness key and period boundaries are real. Your mileage may vary around time zones, but UTC period boundaries and an explicit meter version remove a large class of arguments.













