Metering
Metering is Frame's primitive for tracking what customers actually do on your platform. You log an event whenever a customer consumes a unit of usage — an API call, a stored gigabyte, a minute of compute, a sent email. Frame aggregates those events against a billing metric (the rule for how to count and price), then surfaces the aggregated value for invoicing.
The mechanic underpins three different billing shapes:
- Usage-based pricing — bill purely on what was consumed (no recurring base; the invoice equals usage × rate).
- Prepaid billing — customer purchases credits upfront; usage decrements the balance; bill when credits run out.
- Postpaid billing — customer accrues usage over a cycle; bill at the end against a stored payment method.
All three use the same two primitives: a BillingMetric (the definition) and BillingMetricEvents (the per-usage records). What differs is how you compose them with subscriptions, credits, and invoicing.
The flow
1. Define a metric (BillingMetric)
"we charge $0.001 per API call, aggregated by sum"
2. Log events as usage happens (BillingMetricEvent)
"customer X made 1 call at 2026-06-05T14:23:00Z"
...
"customer X made 1 call at 2026-06-05T14:23:01Z"
3. Read aggregated usage (Billing reports)
"customer X made 42,318 calls this cycle"
4. Invoice or decrement credits
42,318 × $0.001 = $42.32
Each step has its own primitive and its own API surface. The split lets you log events at write time (high throughput, fire-and-forget) and aggregate at read time (when an invoice generates or a customer dashboard renders).
Logging events
When a customer triggers a billable action, log a BillingMetricEvent:
curl --request POST \
--url https://api.framepayments.com/v1/billing/metering_events \
--header "Authorization: Bearer $FRAME_SECRET_KEY" \
--header 'Content-Type: application/json' \
--data '{
"customer": "<customer_id>",
"event_name": "api_call",
"reference": "req_8f3a2c1e_2026-06-05T14:23:00Z",
"value": 1,
"timestamp": "2026-06-05T14:23:00Z"
}'
The fields:
customeroraccount— the payer the usage attaches to. Mutually exclusive; one is required.event_name— must match an existingBillingMetric'sevent_nameon the merchant. Frame routes the event to that metric for aggregation.reference— globally unique idempotency key. Replaying with the samereferenceis a safe no-op; Frame silently dedups.value— the numeric quantity for this event. Sum metrics tally these; count metrics ignore (each event counts as 1).timestamp— when the usage happened (your server's clock). Optional; defaults to receipt time. Useful for batched submissions or backfill.start_time/end_time— used bytime_durationmetrics that bill for elapsed time rather than per-event.markup_percentage— used bymarkup_percentagemetrics.metadata— k/v pairs for your internal tracking.
The reference field is the single most important field for correctness. Use it to derive a value from your business logic (request ID, idempotency key, composite of customer + timestamp + action) so that double-submits don't double-bill. Frame catches duplicates via a global uniqueness constraint and returns a duplicate-reference signal rather than an error — so blind retries are safe.
Aggregation
A metric's aggregation field controls how events combine:
| Aggregation | What it does | Example use |
|---|---|---|
sum | Adds up value across events | $0.001 per call, value = call count per event |
count | Counts events (ignores value) | $0.10 per upload, regardless of file size |
count_unique | Counts distinct values | Per-active-user billing — value = user ID |
average | Mean of value | Average latency, average request size |
time_duration | Sums (end_time − start_time) | Per-minute compute billing |
markup_percentage | Sums (value × markup%) | Marketplace fees on transaction volume |
Pick once at metric creation; switching aggregation later requires creating a new metric and migrating event logging to the new event_name.
Lifecycle
A BillingMetricEvent walks through:
logged(initial) — event accepted; aggregation hasn't computed yet.aggregated— included in a periodic rollup; downstream calculations can read it.invoiced— attached to an invoice that's been generated.paid— the invoice settled.
You don't drive these transitions manually; they happen as the billing pipeline runs.
Reporting
To read aggregated usage for a customer or subscription:
- Per customer:
GET /v1/billing/report/customer?customer_id=<id>— total usage across all metrics. - Per metric for a customer:
GET /v1/billing/report/event/<event_name>?customer_id=<id>— drill into a single metric. - All metrics for a customer:
GET /v1/billing/report/events?customer_id=<id>— itemized breakdown. - Per subscription:
GET /v1/billing/report/subscription?subscription_id=<id>— usage scoped to one subscription. - Credit depletion progress:
GET /v1/billing/report/threshold_progress?customer_id=<id>— how close the customer is to running out of prepaid credits.
These power your customer-facing usage dashboards and your internal monitoring alerts.
Webhooks
The billing pipeline emits seven event types via BillingWebhooksService:
billing.subscriber_overage— usage exceeded a threshold; merchant should react.billing.subscriber_usage— periodic usage snapshot.billing.credits_usage— credits were consumed against usage.billing.credits_expiration— credits expiring soon, or expired.billing.threshold.approaching— close to credit depletion warning.billing.invoice.generated— usage rolled into an invoice.billing.charge.succeeded— invoice collected.
Subscribe to billing.threshold.approaching and billing.subscriber_overage if your platform needs to surface usage warnings to customers proactively.
How metering composes with other billing
- Pure usage-based: define metrics, log events, generate an invoice at end of cycle via
POST /v1/billing/billing_invoice. No subscription needed. See build usage-based pricing. - Prepaid + metered: issue billing credits on customer signup, log usage events, credits decrement automatically. See build prepaid billing.
- Subscription + overage: customer pays a base subscription fee for an included usage allotment; events above that allotment generate a separate billing invoice per cycle. See build postpaid billing.
The metering primitives don't care which composition you pick. Same events, same aggregation, same reports — the surrounding orchestration (subscription, credits, invoice generation cadence) differs.
The billing invoice endpoint
POST /v1/billing/billing_invoice is Frame's canonical "turn aggregated usage into a billed invoice" endpoint. You pass a customer + window (start_date + end_date) + collection method + payment method, and Frame's Billings::GenerateInvoiceForReportService runs the full pipeline: aggregate events in the window, create the invoice with the line item, issue (and charge for auto_charge), fire billing.invoice.generated. Failed auto_charge invoices retry automatically via Billings::RetryFailedChargesJob (daily, up to 3 attempts).
You don't manually call POST /v1/invoices + POST /v1/invoice_line_items + POST /v1/invoices/<id>/issue for usage-billed flows — the billing invoice endpoint composes all of it. Use the manual invoice endpoints only for non-usage charges (one-off B2B billing, manual reconciliation invoices).
Gotchas
Symptom: events with the same reference keep being silently accepted. Why: reference is globally unique; same value = idempotent replay (intentional). Fix: this is correct behavior. If you actually meant to log a separate event, generate a fresh reference per occurrence (e.g., include a timestamp or counter in the derivation).
Symptom: event_name doesn't match any metric → API rejects the event. Why: events route to metrics by event_name; an unrecognized name has no metric to attach to. Fix: create the BillingMetric first (POST /v1/billing/metering with the same event_name), then log events against it.
Symptom: time_duration aggregation but events don't have start_time / end_time. Why: time-duration metrics require both fields per event; without them there's no interval to sum. Fix: update your event-logging code to capture both; for events already logged without them, the duration counts as zero.
Symptom: count_unique aggregation returning unexpected counts. Why: it counts distinct values of the value field across events. If you've been passing constants, the unique count is 1. Fix: set value to the entity ID you want to deduplicate on (e.g., the user ID for per-active-user billing).
Reference
For the full API surface, see POST/v1/billing/metering and POST/v1/billing/metering_events.