End-to-End Billing Setup: From Zero to Working Credits in 10 Steps
Complete walkthrough building a working billing system with Acme Chat — metric, metering rule, plan, variant, customer, subscription, and usage in one linear sequence.
COMPLETE BILLING SETUP · 10 STEPS
Quick take
- Every billing setup follows the same sequence: metric → rule → plan → variant → grant template → customer → subscribe → check → use → verify
- Metering rules price the metric; credit grants fund the customer; entitlement checks gate access
- Usage events are fire-and-forget (
202 Accepted); balance updates are eventually consistent within seconds - Idempotency keys on every mutating call protect against network retries
End-to-End Billing Setup
Pattern: the full billing pipeline, one step at a time.
The goal
Go from an empty QuotaStack tenant to a working billing system where a customer has a subscription, credit grants fire automatically, usage debits credits, and entitlement checks gate access.
We’ll use Acme Chat (a chat companion app) as the running example. By the end, Alice (a customer) will have a weekly subscription that grants 50 credits, and each chat message will cost 1 credit.
Phase 1: Define what you charge for
Step 1: Create a billable metric
A billable metric is a named action you charge for. Acme Chat charges per message:
POST /v1/billable-metrics
Idempotency-Key: metric:chat_message
{ "key": "chat_message", "name": "Chat Message" }
The key is the stable identifier you’ll reference everywhere — metering rules, usage events, entitlement checks. It defaults to type: metered (credit-based).
Step 2: Set the price with a metering rule
POST /v1/metering-rules
Idempotency-Key: rule:chat_message
{ "billable_metric_key": "chat_message", "cost_type": "per_unit", "unit_cost": 1000 }
1,000mc per unit = 1 credit per message. Now QuotaStack knows: when a usage event arrives for chat_message, debit 1,000mc from the customer’s balance.
Phase 2: Build the plan
Step 3: Create a plan and variant
The plan is the product (“Acme Chat”). The variant is the specific pricing tier (“Weekly Standard” — prepaid, weekly billing cycle):
POST /v1/plans
Idempotency-Key: plan:acme-chat
{ "name": "Acme Chat", "description": "Companion chat subscription plans" }
POST /v1/plans/{plan_id}/variants
Idempotency-Key: variant:weekly-standard
{ "name": "Weekly Standard", "billing_cycle": "weekly", "billing_mode": "prepaid" }
Step 4: Attach a credit grant template
Tell QuotaStack to automatically grant 50,000mc (50 credits) every week:
POST /v1/plans/{plan_id}/variants/{variant_id}/credit-grants
Idempotency-Key: grant-template:weekly-50k
{
"credits": 50000,
"grant_interval": "billing_cycle",
"grant_type": "recurring",
"expires_after_seconds": 604800,
"rollover_percentage": 0,
"priority": 0
}
expires_after_seconds: 604800 = 7 days. Each grant expires when the next one arrives — no hoarding. rollover_percentage: 0 means unused credits don’t carry over.
At this point you have a complete plan: metric → pricing → variant → automatic grants. Nothing has happened yet because no customer is subscribed.
Phase 3: Onboard a customer
Step 5: Create a customer
POST /v1/customers
Idempotency-Key: customer:user_alice
{ "external_id": "user_alice", "display_name": "Alice" }
Step 6: Subscribe them
For a manual flow, start the subscription after your provider confirms Alice paid:
POST /v1/subscriptions
Idempotency-Key: sub:alice-weekly-001
{ "external_customer_id": "user_alice", "plan_variant_id": "{variant_id}" }
On activation, QuotaStack immediately fires the credit grant template — Alice now has 50,000mc (50 credits). The subscription status is active, with current_period_end set to one week from now.
With a payment connector, use hosted checkout for the mapped prepaid plan. QuotaStack starts the subscription after it can prove payment. Do not also call this manual route.
Phase 4: Use it
Step 7: Check entitlement before each message
GET /v1/customer-by-external-id/user_alice/entitlements/chat_message
{
"allowed": true,
"balance": 50000,
"estimated_cost": 1000,
"balance_after": 49000
}
allowed: true means Alice can send a message. balance_after: 49000 is what her balance will be afterward. If allowed were false, your UI shows “you’ve used all your messages this week.”
Step 8: Record usage
Alice sends 5 messages. Your backend fires a usage event for each (or batches them):
POST /v1/usage
Idempotency-Key: usage:msg_001
{
"external_customer_id": "user_alice",
"billable_metric_key": "chat_message",
"units": 5,
"idempotency_key": "msg_batch_001"
}
Returns 202 Accepted — usage events process asynchronously. 5 × 1,000mc = 5,000mc will be debited within seconds.
Step 9: Verify the balance
GET /v1/customer-by-external-id/user_alice/credits
{
"balance": 45000,
"reserved_balance": 0,
"pending_balance": 0,
"effective_balance": 45000,
"lifetime_earned": 50000
}
lifetime_earned still reads 50,000 — it counts every credit ever granted and never goes down. The balance is what dropped: 50,000 − 5,000 = 45,000mc. Alice has 45 messages left this week.
To see what she has consumed, read the ledger at GET /v1/customer-by-external-id/user_alice/credits/history?type=consumption. There is no lifetime_consumed field on the account.
What you’ve built
chat_message metric → "what we charge for"
↓ priced by
per_unit metering rule → "1,000mc per message"
↓ used by
Weekly Standard variant → "50,000mc/week, expires in 7 days"
↓ subscribed by
Alice → "active, 45,000mc remaining"
↓ gated by
entitlement check → "allowed: true"
↓ consumed by
usage events → "5 messages = 5,000mc debited"
The metric defines WHAT you track. The metering rule defines HOW MUCH it costs. The plan variant defines WHO gets credits and when. The subscription binds a customer to a variant. Entitlements gate access. Usage events consume credits.
Next steps
- Add variable companion pricing — charge premium companions more by varying the
unitsfield - Set up webhooks to react to
subscription.renewal_dueandcredit.consumedevents - Add a wallet for pay-as-you-go overflow when weekly credits run out
- Use reservations for long-running AI calls that might fail mid-generation
Concepts used
Loading…