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

RequirementDetails
Frame secret keyFor server-side API calls.
Customer or Account recordThe payer.
Stored payment methodThe subscription will auto-charge on each cycle.
Recurring Product for the base planA purchase_type: recurring product with the base price.
Billing meter(s) for the overage axisOne 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