Build usage-based pricing
The simplest billing shape Frame supports: a customer pays for exactly what they used, nothing more, nothing less. No monthly base fee, no included allotment, no overage tier. Aggregate the events, multiply by rate, generate an invoice at the end of the cycle.
The full integration is three steps: define the meter, log events, invoice on cadence. The hard parts aren't in Frame — they're in your platform's instrumentation discipline.
Prerequisites
| Requirement | Details |
|---|---|
| Frame secret key | For server-side API calls. |
| Customer or Account record | The payer. Pre-create or attach inline. |
| Decision: what units are you billing | API calls, gigabytes, minutes, users, transactions. Pick one per meter. |
1. Define the billing meter
A meter defines the routing key (event_name), the aggregation, and the unit price. One meter per billable unit:
curl --request POST \
--url https://api.framepayments.com/v1/billing/metering \
--header "Authorization: Bearer $FRAME_SECRET_KEY" \
--header 'Content-Type: application/json' \
--data '{
"event_name": "api_call",
"display_name": "API calls",
"description": "$0.001 per API call across all endpoints",
"aggregation": "sum",
"value": 0.001
}'
Field-by-field decisions:
event_name— pick something stable. Once your platform is logging events with this name, renaming requires migration. Lowercase, underscore-separated.aggregation—sumfor "every event has a count,"countfor "every event is +1 regardless of value,"count_uniquefor active-user billing,time_durationfor elapsed-time billing,markup_percentagefor fee-on-transactions,averagefor analytics-style metrics. See billing meters.value— the unit price. Frame multiplies aggregated usage ×valueto get the invoice amount.
For "$0.01 per API call up to 1M, $0.005 after" — Frame doesn't model tiered pricing in a single meter. Either compute the price client-side and pass it as event value, or use two meters and route in your code.
2. Log events at the moment of usage
In your application, log a BillingMetricEvent every time the customer consumes a unit:
// Pseudo-code inside your API handler
async function handleApiCall(request, customer) {
const result = await processRequest(request);
// Log the billable event
await frame.billingMetricEvents.create({
customer: customer.frame_id,
event_name: 'api_call',
reference: `${request.id}-call`, // idempotency key
value: 1,
timestamp: new Date().toISOString(),
});
return result;
}
Two non-negotiables:
reference must be globally unique per event. Frame uses it for idempotent dedup — if you retry the same logging call (network blip, app restart, queue redelivery), the duplicate is silently dropped rather than double-billed. Derive it from your business logic: request ID, idempotency key, a deterministic composite of customer + timestamp + action. Whatever generation scheme you pick, ensure the same logical event produces the same reference.
Fail open, not closed. If event logging fails (Frame is down, your network is partitioned), don't reject the customer's request — that turns a billing-system outage into a product-availability outage. Queue the event locally and retry; the worst case is delayed billing, which is recoverable.
try {
await frame.billingMetricEvents.create({ ... });
} catch (err) {
// Queue locally for retry; don't block the user's request
await localQueue.push({ event: 'api_call', customer, reference, ... });
}
3. Read aggregated usage
At any point — for customer-facing dashboards or end-of-cycle invoicing — pull the aggregated total:
# All meters' usage for a customer this period
curl --request GET \
--url "https://api.framepayments.com/v1/billing/report/events?customer_id=<customer_id>" \
--header "Authorization: Bearer $FRAME_SECRET_KEY"
# Drill into one meter
curl --request GET \
--url "https://api.framepayments.com/v1/billing/report/event/api_call?customer_id=<customer_id>" \
--header "Authorization: Bearer $FRAME_SECRET_KEY"
The response gives you the aggregated value and the calculated price (aggregated value × meter value).
4. Generate the invoice via the billing pipeline
Frame exposes a dedicated endpoint that aggregates a usage window into an invoice atomically — creates the invoice, attaches the line item from the aggregated value, and issues it (for auto_charge flows the charge runs in-line). You don't compose this manually.
curl --request POST \
--url https://api.framepayments.com/v1/billing/billing_invoice \
--header "Authorization: Bearer $FRAME_SECRET_KEY" \
--header 'Content-Type: application/json' \
--data '{
"customer": "<customer_id>",
"collection_method": "auto_charge",
"payment_method": "<payment_method_id>",
"description": "API usage — June 2026",
"start_date": "2026-06-01T00:00:00Z",
"end_date": "2026-06-30T23:59:59Z"
}'
What happens server-side: Billings::GenerateInvoiceForReportService aggregates all in-window events for the customer, creates the invoice, adds the line item, issues for auto-charge, then fires billing.invoice.generated on completion. Your platform doesn't need to read the aggregation separately or stitch line items together.
Common cadences:
- End of calendar month. Cron on the 1st: for each customer with non-zero usage in the prior month, call
POST /v1/billing/billing_invoicewith the prior-month window. - Per-event threshold. When a customer's running usage total crosses a dollar threshold, generate an invoice for the threshold window. Useful for customers without stored payment methods or for sandbox-only platforms.
- Subscription rollup. Customer has a recurring subscription? Use the subscription for the base charge; use
billing_invoicefor the overage. See build postpaid billing.
For invoice mechanics including the auto_charge / request_payment distinction, see invoices.
5. React to threshold + overage webhooks
Subscribe to billing webhooks for proactive customer notifications:
switch (event.type) {
case 'billing.subscriber_usage':
// Periodic snapshot — could be daily/weekly depending on Frame config.
updateCustomerDashboard(event.data);
break;
case 'billing.subscriber_overage':
// Customer crossed a usage threshold; surface a warning.
notifyCustomer(event.data.customer, 'high_usage_warning');
break;
case 'billing.invoice.generated':
// Frame finished generating + issuing an invoice from rolled-up usage.
// For auto_charge invoices, the charge has already run by the time this fires.
notifyCustomerOfNewInvoice(event.data);
break;
case 'billing.charge.succeeded':
// Invoice's charge cleared (either initial attempt or retry).
markInvoicePaid(event.data);
break;
}
Auto-retry on failed charges. When an auto_charge billing invoice fails to collect, Frame runs Billings::RetryFailedChargesJob daily at 2 AM, retrying up to 3 attempts. Failed billing charges that haven't exhausted attempts are picked up automatically — you don't need to orchestrate retries. After 3 attempts (or 72 hours since last update), the charge is considered permanently failed and your platform handles customer-side recovery (notify, prompt for payment method update, etc.).
Worked example: API platform
Charging $0.001 per API call, $0.10 per uploaded file:
- Create two meters:
api_call(sum, $0.001) andfile_upload(count, $0.10). - In your API handlers, log
api_callper request andfile_uploadper upload. - Run a cron on the 1st of each month: for each customer with non-zero usage, create + issue an invoice.
That's the full integration. Customer pays for what they used; Frame handles the math.
Common variations
Free tier. Allow N units of usage for free before billing kicks in. Two paths: (a) check the customer's usage server-side and only log events above the free threshold, (b) issue billing credits equal to the free allotment that decrement before paid billing starts. (b) is cleaner because the bookkeeping is in Frame.
Volume discounts. Compute the effective per-unit price in your application based on the customer's tier and pass it as event value with aggregation: sum. The customer's invoice reflects the discounted rate without exposing the tiering to Frame.
Multi-product platform. Create one meter per billable axis. Don't try to overload one meter to bill multiple things; the aggregation and price live on the meter, not on the event.
Gotchas
Symptom: customer's usage report shows zero despite logging events. Why: most commonly, the events' event_name doesn't exactly match a meter's event_name. Fix: the routing is exact-match. List recent events via the metering events endpoint and check what names are coming through.
Symptom: same reference returns success but the count didn't increment. Why: Frame is silently deduping — idempotency working as intended. Fix: if you meant to log a distinct event, generate a fresh reference per occurrence.
Symptom: invoice math is off by 1-2 cents from your expected total. Why: aggregation happens server-side and rounds at the invoice line item, not per event. Fix: if exact-penny precision matters, surface the aggregated value as displayed by Frame, not your local-computed value.
Symptom: usage events are being logged but the customer-facing dashboard is stale. Why: aggregation rolls up asynchronously; the report endpoint may lag fresh events by seconds-to-minutes. Fix: this is expected for high-volume meters. For real-time customer dashboards, maintain a local counter and reconcile against Frame's aggregated total on cycle close.
Next steps
- Build prepaid billing — the credit-based prepaid pattern
- Build postpaid billing — subscription with overage
- Metering concept for the deeper model