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:

  • customer or account — the payer the usage attaches to. Mutually exclusive; one is required.
  • event_name — must match an existing BillingMetric's event_name on the merchant. Frame routes the event to that metric for aggregation.
  • reference — globally unique idempotency key. Replaying with the same reference is 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 by time_duration metrics that bill for elapsed time rather than per-event.
  • markup_percentage — used by markup_percentage metrics.
  • 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:

AggregationWhat it doesExample use
sumAdds up value across events$0.001 per call, value = call count per event
countCounts events (ignores value)$0.10 per upload, regardless of file size
count_uniqueCounts distinct valuesPer-active-user billing — value = user ID
averageMean of valueAverage latency, average request size
time_durationSums (end_time − start_time)Per-minute compute billing
markup_percentageSums (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.