Build postpaid billing
In a postpaid model, the customer commits to a recurring base fee that includes some allotted usage, then pays for whatever they consume above that allotment at end-of-cycle. "$99/month, includes 10,000 API calls, $0.01 per call after." This is the classic SaaS shape — predictable base revenue with usage-driven upside.
The integration composes a Subscription (for the base) with billing meters (for the overage). They settle together on each cycle: the subscription generates an invoice for the base, and your platform adds line items for the cycle's overage usage.
When postpaid fits
- You want recurring revenue + usage upside. Base fee is predictable; overage scales with growth.
- Customers are willing to be billed at end of cycle. Postpaid means surprise bills if usage spikes — your dunning and customer-comms need to handle this.
- Allotment + overage maps cleanly onto your product. Some products don't have a natural "included amount" (e.g., per-transaction marketplace fees); usage-based or prepaid fits those better.
Prerequisites
| Requirement | Details |
|---|---|
| Frame secret key | For server-side API calls. |
| Customer or Account record | The payer. |
| Stored payment method | The subscription will auto-charge on each cycle. |
| Recurring Product for the base plan | A purchase_type: recurring product with the base price. |
| Billing meter(s) for the overage axis | One meter per billable unit (calls, gigabytes, etc.). |
1. Create the base subscription product
Same as for any subscription: a recurring product with the base price:
curl --request POST \
--url https://api.framepayments.com/v1/products \
--header "Authorization: Bearer $FRAME_SECRET_KEY" \
--header 'Content-Type: application/json' \
--data '{
"name": "Pro Plan",
"description": "$99/month — includes 10,000 API calls",
"default_price": 9900,
"purchase_type": "recurring",
"recurring_interval": "monthly"
}'
The included allotment ("10,000 API calls") isn't a field on the product — Frame doesn't model "included usage" as a first-class concept. Your platform enforces the allotment in step 4 by deciding which events count as overage.
2. Create the overage meter
A meter for the usage you'll bill overage on, priced at the overage rate (not the all-in rate):
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_overage",
"display_name": "API call overage",
"description": "$0.01 per call above the plan allotment",
"aggregation": "sum",
"value": 0.01
}'
The meter is named for the overage axis (api_call_overage, not just api_call) so the events you route to it are pre-filtered to "calls above the allotment."
3. Create the subscription
Standard subscription creation, pointing the customer at the base product:
curl --request POST \
--url https://api.framepayments.com/v1/subscriptions \
--header "Authorization: Bearer $FRAME_SECRET_KEY" \
--header 'Content-Type: application/json' \
--data '{
"customer": "<customer_id>",
"product": "<pro_plan_product_id>",
"default_payment_method": "<payment_method_id>",
"currency": "USD",
"description": "Pro Plan — monthly + usage"
}'
Subscription begins billing at pending → active on the first cycle's invoice. Each subsequent cycle, Frame generates an invoice for the base. Overage line items get added by your platform.
4. Track usage; only log overage events
This is the load-bearing application logic. As customers use your platform, count their usage. Below the allotment, log nothing (or log to a non-billable meter for analytics). Above the allotment, log overage events:
async function handleApiCall(request, customer) {
const result = await processRequest(request);
// Increment a server-side counter for this customer's cycle
const usage = await incrementUsage(customer.id, 'api_call');
// Only log overage above the allotment
if (usage > customer.plan.api_call_allotment) {
await frame.billingMetricEvents.create({
customer: customer.frame_id,
event_name: 'api_call_overage',
reference: `${request.id}-overage`,
value: 1,
});
}
return result;
}
The incrementUsage counter is your responsibility. Options:
- Server-side counter in your DB. Increment per request; reset at cycle boundary. Simple but requires consistent transactional updates.
- Frame meter as the counter, application-side check. Log every event to a "tracking" meter (no value, just count); query the meter's aggregated total on each request to decide if the new event is overage. Higher latency, less hot-path overhead.
- Hybrid. Optimistic local counter + periodic reconciliation against Frame's meter aggregate.
Pick based on your throughput needs. For most platforms, server-side counter with periodic Frame reconciliation is the right balance.
5. Generate the overage invoice as a separate billing invoice
The subscription handles the base charge automatically on each renewal cycle. For the overage, generate a separate billing invoice via the dedicated endpoint — Frame aggregates the overage events in a window and produces the invoice atomically (line item + issue + charge for auto_charge):
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 call overage — June 2026",
"start_date": "2026-06-01T00:00:00Z",
"end_date": "2026-06-30T23:59:59Z"
}'
This is the canonical pattern: subscription invoice for the base, billing invoice for the overage. The customer sees two charges per cycle, but the accounting is clean — each invoice has a clear purpose, and Frame handles aggregation server-side via Billings::GenerateInvoiceForReportService.
Why not append overage line items to the subscription invoice? Two reasons. First, billing.invoice.generated fires after the billing pipeline has created + issued + (for auto-charge) collected the invoice — there's no pre-issue webhook window to append more line items. Second, invoice line items only accept product + quantity on create — you can't pass arbitrary pricing on the line, so the invoice's structure has to mirror the underlying products anyway.
If your platform requires a single combined invoice for the cycle (B2B preference, accounting tooling), the workaround is to skip the subscription primitive entirely: generate a single billing invoice per cycle with line items covering both base and overage. You lose the subscription state machine (active/past_due/etc.) and have to orchestrate the cadence yourself.
6. React to webhooks
switch (event.type) {
case 'customer.subscription.renewal.processing':
case 'subscription.renewal.processing':
// Subscription is charging the cycle. Prep your overage append if using Path A.
break;
case 'billing.subscriber_overage':
// Customer crossed the allotment threshold mid-cycle.
notifyCustomerOverage(event.data.customer);
break;
case 'billing.invoice.generated':
// Frame finished generating the overage invoice (line items + issue +
// charge for auto_charge are already done by the time this fires).
notifyCustomerOfOverageBill(event.data);
break;
case 'billing.charge.succeeded':
// Overage invoice's charge cleared (either initial or retry attempt).
markInvoicePaid(event.data);
break;
case 'customer.subscription.renewal.failed':
case 'subscription.renewal.failed':
// Charge failed; recover via the standard subscription flow.
initiateRecovery(event.data);
break;
}
billing.subscriber_overage is the mid-cycle "heads up, this customer's running hot" signal — surface it in your customer-facing dashboard and optionally send a "you're approaching X% above your plan, expect a $Y overage charge this cycle" notification. Setting overage expectations mid-cycle prevents end-of-cycle billing disputes.
Auto-retry on failed overage charges. When the overage billing invoice's auto-charge fails, Frame's Billings::RetryFailedChargesJob runs daily at 2 AM and retries up to 3 attempts (within a 72-hour window of the most recent attempt). You don't need to orchestrate billing-invoice retries on your side. After 3 attempts the charge is permanently failed and your platform handles customer-side recovery (notify, prompt for payment method update, etc.). This auto-retry applies to billing invoices specifically — subscription-renewal invoice retries are a separate concern (currently merchant-side).
Common variations
Tiered overage. First 5,000 overage calls at $0.01, next 10,000 at $0.005, etc. Frame doesn't model tiers within a meter. Either compute the effective per-event price client-side and pass via the event's value (with aggregation: sum), or use multiple meters with your app routing events to the right tier.
Per-feature overage. Each billable axis (calls, storage, users) gets its own meter and its own line item on the cycle invoice.
Grandfather pricing on plan changes. When a customer upgrades plans mid-cycle, decide whether the overage rate switches immediately or stays grandfathered for the rest of the cycle. Either is fine; pick the policy and stick to it.
Gotchas
Symptom: overage isn't appearing anywhere automatically. Why: Frame doesn't auto-include meter aggregates on subscription invoices, and the merchant has to trigger overage invoice generation explicitly via POST /v1/billing/billing_invoice. Fix: run a scheduled job at cycle close that generates the overage invoice per step 5.
Symptom: customer is being billed for usage below the allotment. Why: your code is logging events too eagerly — every usage event is being routed to the overage meter, not just the above-allotment ones. Fix: gate the event logging behind the allotment check; only call billingMetricEvents.create when usage > allotment.
Symptom: customer is going wildly over allotment with no notification. Why: no billing.subscriber_overage webhook handler, or thresholds aren't configured server-side. Fix: implement the webhook handler with a customer-facing notification; if thresholds aren't firing as expected, contact Frame support to tune.
Symptom: end-of-cycle invoice has the base + overage but the customer disputed because they expected the base only. Why: customer wasn't notified of overage as it accrued. Fix: the mid-cycle webhook + notification flow (step 6) is the prevention. Once a dispute lands, it's a customer-comms problem more than a billing problem.
Next steps
- Build subscriptions — the subscription mechanics underneath the base
- Build usage-based pricing — pure usage with no base
- Build prepaid billing — credits instead of postpaid invoices
- Metering concept for the deeper model