-
Notifications
You must be signed in to change notification settings - Fork 170
Credit‐Based Billing: Implementation Guide
Not every billing model fits neatly into "charge per API call." Sometimes your customers need to prepay for a pool of credits, burn them down over time, and top up when they run low. This is credit-based billing — and it's the dominant model for AI products, token-based APIs, and any service where customers want spending predictability.
This guide covers when to use credits vs. pure usage billing, how to implement credit grants and wallets with Flexprice, and real-world patterns you can steal for your own billing system.
Both models charge based on consumption. The difference is when money changes hands and how customers think about spending.
Pure usage billing works best when consumption is relatively predictable, the unit economics are straightforward (e.g., $0.01 per request), and customers are comfortable with variable monthly invoices. It's the simpler model to implement — events come in, metrics get aggregated, invoices go out at end of cycle.
Credit-based billing is the better fit when:
- Customers want spending caps. Prepaid credits create a natural budget boundary. The customer buys 100,000 credits, and when they're gone, they either top up or get cut off. No surprise invoices.
- Your pricing unit is abstract. AI tokens, compute units, and "platform credits" don't map cleanly to a dollar amount per raw event. Credits let you define an internal currency that absorbs pricing complexity.
- You offer promotional or trial access. Grant 10,000 free credits to new signups. When they burn through them, the conversion to paid is a natural upsell — not a hard paywall.
- Enterprise contracts involve committed spend. The customer commits to $50K/year in credits. They draw down over time, and unused credits either roll over or expire per the contract terms.
In practice, many products use hybrid models — a base subscription plus a credit wallet for variable usage. Flexprice supports both natively.
Before writing code, here's the data model:
Wallet: A balance container associated with a customer. A customer can have multiple wallets (e.g., one for promotional credits, one for paid credits). Each wallet has its own balance, expiration rules, and priority in the deduction order.
Credit Grant: An operation that adds credits to a wallet. Grants can be:
- Prepaid — customer purchased them (tied to a payment)
- Promotional — issued for free (trials, support credits, goodwill)
- Committed — part of an enterprise contract with minimum spend
Deduction: When usage events are processed, the billing engine deducts credits from the customer's wallet(s) according to a configurable priority order. Promotional credits are typically burned first, then paid credits.
Overage: What happens when the wallet hits zero? Options: block the customer's access, allow continued usage and invoice the overage at a defined rate, or trigger an auto top-up.
First, create a wallet for your customer. This is a one-time setup per customer (or per credit type if you want separate promotional and paid wallets).
curl -X POST https://api.cloud.flexprice.io/v1/wallets \
-H "Content-Type: application/json" \
-H "x-api-key: $FLEXPRICE_API_KEY" \
-d '{
"customer_id": "cust_8xk29f",
"currency": "credits",
"name": "Primary Credit Wallet"
}'Add credits to the wallet. You can set expiration dates, grant types, and metadata:
curl:
curl -X POST https://api.cloud.flexprice.io/v1/credit-grants \
-H "Content-Type: application/json" \
-H "x-api-key: $FLEXPRICE_API_KEY" \
-d '{
"wallet_id": "wal_3kd92x",
"amount": 50000,
"grant_type": "prepaid",
"expires_at": "2026-07-04T00:00:00Z",
"metadata": {
"source": "enterprise_contract",
"contract_id": "contract_2024_q2"
}
}'Python SDK:
client.credit_grants.create(
wallet_id="wal_3kd92x",
amount=50000,
grant_type="prepaid",
expires_at="2026-07-04T00:00:00Z",
metadata={"source": "enterprise_contract"},
)Go SDK:
client.CreditGrants.Create(ctx, &fp.CreditGrantInput{
WalletID: "wal_3kd92x",
Amount: 50000,
GrantType: "prepaid",
ExpiresAt: time.Date(2026, 7, 4, 0, 0, 0, 0, time.UTC),
Metadata: map[string]string{"source": "enterprise_contract"},
})Real-world credit systems need more than just "add credits, subtract credits." Here's what Flexprice handles out of the box:
Configure a wallet to automatically purchase more credits when the balance drops below a threshold. This prevents service interruption for customers who want uninterrupted access:
{
"auto_top_up": {
"enabled": true,
"threshold": 5000,
"amount": 25000,
"payment_method_id": "pm_stripe_abc123"
}
}When the balance hits 5,000 credits, Flexprice charges the customer's payment method for 25,000 more credits and adds them to the wallet. No developer intervention required.
Each credit grant can have its own expiration date. When credits expire, they're removed from the available balance. Flexprice stacks multiple grants with different expiration dates and deducts from the earliest-expiring credits first (FEFO — First Expiring, First Out).
For subscription-based credit models (e.g., "10,000 credits/month included"), you can configure whether unused credits roll over to the next period or expire at cycle end. Rollover caps prevent unbounded accumulation.
Trigger webhook notifications when a customer's wallet balance drops below a configurable threshold. Pipe these into your notification system to prompt customers to top up before they hit zero:
{
"alerts": [
{"threshold_percent": 20, "webhook_url": "https://your-app.com/hooks/low-credits"},
{"threshold_percent": 5, "webhook_url": "https://your-app.com/hooks/critical-credits"}
]
}Here's a complete integration example — a Node.js service that provisions credits when a customer completes a Stripe checkout:
import Flexprice from "@flexprice/sdk";
import Stripe from "stripe";
const fp = new Flexprice({ apiKey: process.env.FLEXPRICE_API_KEY });
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
// Webhook handler for Stripe checkout.session.completed
export async function handleCheckoutComplete(session) {
const customerId = session.metadata.flexprice_customer_id;
const creditAmount = parseInt(session.metadata.credit_amount);
const walletId = session.metadata.wallet_id;
// Grant credits in Flexprice
const grant = await fp.creditGrants.create({
wallet_id: walletId,
amount: creditAmount,
grant_type: "prepaid",
expires_at: getExpirationDate(90), // 90-day expiry
metadata: {
stripe_session_id: session.id,
stripe_payment_intent: session.payment_intent,
},
});
console.log(`Granted ${creditAmount} credits to ${customerId}`, grant);
// Check wallet balance for confirmation
const wallet = await fp.wallets.get(walletId);
console.log(`Current balance: ${wallet.balance} credits`);
}
function getExpirationDate(days) {
const date = new Date();
date.setDate(date.getDate() + days);
return date.toISOString();
}The key pattern: your payment system handles money, Flexprice handles credit accounting. The Stripe webhook fires, you call the Flexprice API to grant credits, and the billing engine takes it from there — tracking deductions as usage events flow in.
Map model inference to credit deductions. Different models cost different credit amounts:
{
"event_name": "model.inference",
"external_customer_id": "cust_8xk29f",
"properties": {
"model": "gpt-4",
"input_tokens": 2000,
"output_tokens": 500,
"credits": 25
}
}Use a Flexprice metered feature with SUM aggregation on the credits property. The wallet deduction happens automatically based on the aggregated usage.
Charge 1 credit per standard API call, 5 credits per premium endpoint:
def track_api_call(customer_id: str, endpoint: str):
credits = 5 if endpoint.startswith("/v1/premium/") else 1
client.events.ingest(
event_name="api.call",
external_customer_id=customer_id,
properties={"credits": credits, "endpoint": endpoint},
)For long-running jobs, emit periodic heartbeat events:
// Emit every 60 seconds while job is running
ticker := time.NewTicker(60 * time.Second)
for range ticker.C {
if job.IsComplete() {
break
}
client.Events.Ingest(ctx, &fp.EventInput{
EventName: "compute.minute",
ExternalCustomerID: customerID,
Properties: map[string]interface{}{
"credits": 1,
"job_id": job.ID,
"instance": job.InstanceType,
},
})
}Each heartbeat deducts 1 credit from the customer's wallet. When the wallet hits zero, your application checks the entitlement and either queues the job for later or triggers an auto top-up.
Under the hood, credit deductions follow the same event pipeline as usage billing. Temporal workflows ensure that credit deductions are durable — if a worker crashes mid-deduction, Temporal replays the workflow to completion. No credits are lost or double-deducted.
The wallet transaction log provides a full audit trail: every grant, deduction, expiration, and top-up is recorded with timestamps and metadata. This is critical for enterprise customers who need to reconcile credit usage against their contracts.
- Wallet Documentation: deep dive into wallet configuration
- Credits & Wallets Feature: see the credits feature overview