Build prepaid billing

In a prepaid model, customers pay for usage before they consume it. Buy 10,000 API calls upfront, the balance decrements as they hit your endpoints, repurchase when they get close to zero. The mechanics: a billing credit record holds the prepaid balance; usage events against the right meter decrement it; webhooks tell you when the customer's running low.

This is the cleanest model for platforms where customers want predictable spend and merchants want predictable cash flow — money lands at credit-purchase time, not at end-of-cycle invoice time.

When prepaid fits

  • Predictable cash flow matters more than usage-following revenue. You collect upfront; usage just consumes what's already paid for.
  • Customers want spending caps. With limited: true credits, usage hard-stops at zero — no surprise bills.
  • Pricing model is simple per-unit. Tiered or volume-discounted pricing is awkward to retrofit into prepaid; pick usage-based or postpaid for those.
  • Repurchase friction is OK. Customers have to take an explicit action to top up. Wrap it in a "low balance" notification + one-click repurchase to minimize friction.

Prerequisites

RequirementDetails
Frame secret keyFor server-side API calls.
Customer or Account recordThe credit owner.
Stored payment methodFor the customer to purchase credits.
Product representing the credit packA Product (purchase_type: one_time) that customers buy. The credit issuance ties to this product.
Billing meterThe meter that usage events route to.

1. Define the meter

Same shape as for usage-based pricing — a meter says how to aggregate events and what each unit costs:

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": "1 credit per API call",
    "aggregation": "sum",
    "value": 1
  }'

In prepaid mode, value: 1 means "1 credit per event" — the credit balance is denominated in the same units the meter counts. If you want a more granular model (e.g., "expensive endpoints cost 5 credits"), set value higher and pass the per-event cost via the event's value field with aggregation: sum.

2. Create the credit-pack product

Create a Product that represents what the customer is buying when they top up:

curl --request POST \
  --url https://api.framepayments.com/v1/products \
  --header "Authorization: Bearer $FRAME_SECRET_KEY" \
  --header 'Content-Type: application/json' \
  --data '{
    "name": "10,000 API call credits",
    "default_price": 1000,
    "purchase_type": "one_time",
    "billing_credits": 10000
  }'

The billing_credits field on the Product captures the credit allotment per purchase. This is how Frame knows that buying this product issues 10,000 credits.

3. Issue credits when the customer purchases

When the customer buys the credit-pack product (via your checkout, an invoice, a payment link), issue the corresponding billing credit:

curl --request POST \
  --url https://api.framepayments.com/v1/billing/billing_credit \
  --header "Authorization: Bearer $FRAME_SECRET_KEY" \
  --header 'Content-Type: application/json' \
  --data '{
    "customer": "<customer_id>",
    "product": "<credit_pack_product_id>",
    "billing_credits": 10000,
    "limited": true,
    "expires": "2027-06-05T00:00:00Z"
  }'

Field decisions:

  • billing_credits — the prepaid balance. Match the product's billing_credits if you're issuing one credit pack per purchase.
  • limited — when true, usage stops decrementing when credits hit zero (your application enforces the cutoff). When false, additional usage bills on-demand. Most prepaid platforms set true so customers don't accidentally rack up overage.
  • expires — credits expire after this timestamp. Common patterns: 1 year (annual credit packs), end-of-quarter (subscription-bundled credits), no-expiration (long-tail balances; omit the field).

The credit is created in inactive and needs to be activated for usage to consume it (some flows auto-activate via IssueBillingCreditService). Check your platform's flow + activate explicitly if needed.

4. Log usage events as usual

Once credits are active, log usage events the same way as for usage-based pricing:

await frame.billingMetricEvents.create({
  customer: customer.frame_id,
  event_name: 'api_call',
  reference: `${request.id}-call`,
  value: 1,
});

Frame routes the event to the meter, looks up the customer's active credits scoped to a product matching the meter's logic, and decrements available_credits accordingly. No additional API call required from your side.

5. Surface depletion progress to the customer

Customers want to know how much they have left. Use the threshold-progress endpoint:

curl --request GET \
  --url "https://api.framepayments.com/v1/billing/report/threshold_progress?customer_id=<customer_id>" \
  --header "Authorization: Bearer $FRAME_SECRET_KEY"

The response lists active credits with their remaining balances. Show this in the customer's dashboard prominently — "you have 2,317 calls remaining" is the single most important metric for a prepaid customer.

For real-time depletion display (event-by-event), maintain a local counter that decrements optimistically and reconciles against Frame's aggregate periodically. Frame's report endpoint has a small lag for high-volume meters.

6. Notify on low balance via webhooks

Subscribe to:

switch (event.type) {
  case 'billing.threshold.approaching':
    // Customer is close to running out. Send "low balance" notification + repurchase CTA.
    notifyCustomerLowBalance(event.data.customer);
    break;

  case 'billing.credits_usage':
    // A usage event decremented credits. Optional — used for fine-grained dashboards.
    refreshCustomerUsageView(event.data);
    break;

  case 'billing.credits_expiration':
    // Credits expiring soon (or just expired). Notify so customer can repurchase.
    notifyCustomerCreditsExpiring(event.data);
    break;
}

Credits auto-expire. Frame runs Billings::ExpireBillingCreditsJob twice daily (2 AM + 2 PM) to transition credits past their expires timestamp from active to expired. You don't need to schedule expiration on your side — set expires at issuance and the job picks them up. The billing.credits_expiration webhook fires on transition.

The billing.threshold.approaching webhook is your retention-saver. By the time customers hit zero, you've lost them for the moment they wanted to take an action — proactive low-balance notifications are the highest-leverage UX work in a prepaid platform.

7. Repurchase flow

When the customer wants to top up:

  1. They click "buy more credits" in your UI.
  2. Your backend creates an invoice (or runs a charge) for the credit-pack product.
  3. On payment success, issue a new BillingCredit identical to the first one.
# Charge for the credit pack
curl --request POST \
  --url https://api.framepayments.com/v1/transfers \
  --header "Authorization: Bearer $FRAME_SECRET_KEY" \
  --data '{
    "amount": 1000,
    "currency": "USD",
    "account_id": "<account_id>",
    "payment_method_id": "<payment_method_id>"
  }'

# After the transfer succeeds, issue the credits
curl --request POST \
  --url https://api.framepayments.com/v1/billing/billing_credit \
  --header "Authorization: Bearer $FRAME_SECRET_KEY" \
  --data '{
    "customer": "<customer_id>",
    "product": "<credit_pack_product_id>",
    "billing_credits": 10000,
    "limited": true
  }'

Multiple active credits per customer accumulate. Frame decrements in the order Frame's logic prefers (typically oldest-first, but verify against your usage). For customer-facing display, sum all active credits' available_credits to show total remaining.

Common variations

Auto-replenish. When credits hit a low threshold, automatically charge the customer's stored payment method and issue a fresh credit pack. Use the billing.threshold.approaching webhook as the trigger; check whether the customer opted into auto-replenish at signup.

Subscription-bundled credits. A monthly subscription product carries billing_credits: 1000 (1,000 credits/month). On subscription activation, issue the initial credit; on each renewal, issue another. Set expires to the next renewal date so unused credits don't accumulate forever.

Multi-product credits. Some platforms have credits scoped per product (1,000 API call credits, 100 file upload credits). Issue one BillingCredit per product. Usage routes to credits matching the meter's product.

Gotchas

Symptom: customer paid for credits but their balance shows zero. Why: the credit was created in inactive and never activated. Fix: call activate explicitly after payment confirmation, or audit your issuance flow to ensure it activates on success.

Symptom: usage isn't decrementing the credit. Why: credits are product-scoped; usage events that don't route to a meter whose product matches the credit won't consume. Fix: verify the meter ↔ product ↔ credit chain in your platform's data; the customer's credit and the usage event's meter need to share a product.

Symptom: customer hits zero but usage keeps going through. Why: limited: false (or omitted). Without the cap, usage continues at on-demand rates. Fix: set limited: true when issuing credits, or enforce the cutoff at your application layer (more flexible if you want overage tolerance for trusted customers).

Symptom: customer paid for a top-up but the new credit isn't showing in their balance. Why: either the credit was created but not activated, or your application is reading a cached balance that doesn't include the latest credit. Fix: refresh via threshold-progress endpoint; verify the credit's status is active.

Next steps