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: truecredits, 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
| Requirement | Details |
|---|---|
| Frame secret key | For server-side API calls. |
| Customer or Account record | The credit owner. |
| Stored payment method | For the customer to purchase credits. |
| Product representing the credit pack | A Product (purchase_type: one_time) that customers buy. The credit issuance ties to this product. |
| Billing meter | The 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'sbilling_creditsif you're issuing one credit pack per purchase.limited— whentrue, usage stops decrementing when credits hit zero (your application enforces the cutoff). Whenfalse, additional usage bills on-demand. Most prepaid platforms settrueso 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:
- They click "buy more credits" in your UI.
- Your backend creates an invoice (or runs a charge) for the credit-pack product.
- 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
- Build usage-based pricing — for the no-credits "pay as you go" pattern
- Build postpaid billing — subscription + overage instead of upfront purchase
- Billing credits concept for the underlying model