FastAPI + Stripe: Building Usage-Based Billing for an AI SaaS
How to wire Stripe's metered billing to token consumption in a FastAPI AI backend — metering strategy, webhook handling, and per-key usage tracking.
Subscription billing is a solved problem. Usage-based billing for an AI product — where cost tracks token consumption, not a flat monthly fee — is where most teams get stuck. This post covers the actual mechanics: what to meter, how to report it to Stripe, and where the sharp edges are.
Why per-seat billing doesn't work for AI features
A customer running 50 requests a day costs you a fraction of a cent in LLM API calls. A customer running 50,000 requests a day on the same plan costs real money. Flat per-seat pricing either underprices your heavy users or overprices your light ones. Usage-based billing — reporting actual token consumption to Stripe's metered billing API — fixes this, but it means every API call needs to produce an accurate, attributable usage record.
Choosing your metering unit
Three common choices, each with tradeoffs:
- Tokens — most accurate reflection of your actual LLM cost, but customers find "tokens" an unintuitive unit to budget against.
- Requests — simplest to explain, but a one-word query and a 10-page document analysis cost the same.
- Credits — an abstracted unit you define (e.g., 1 credit = 1,000 tokens), giving you pricing flexibility without exposing raw token counts.
Most AI SaaS products land on credits: customers understand "100 credits included" more easily than "150,000 tokens included," and it insulates your pricing from LLM provider price changes.
Setting up the Stripe metered price
Metered prices in Stripe are tied to a subscription item, not charged directly. Create the product and metered price once in the Stripe Dashboard, then report usage against it per billing period.
# app/billing/stripe_client.py
import stripe
stripe.api_key = settings.STRIPE_SECRET_KEY
async def record_usage(subscription_item_id: str, quantity: int):
stripe.billing.MeterEvent.create(
event_name="api_credits",
payload={
"stripe_customer_id": subscription_item_id,
"value": str(quantity),
},
)
Wiring metering into the request path
The usage record has to be created in the same code path that serves the LLM response — not in a background job that might not run, and not as an afterthought. FastAPI AI Kit's metered endpoints follow this pattern:
@router.post("/v1/chat")
@require_api_key(tier=["basic", "pro"])
@rate_limit(per_minute=60)
async def chat(
body: ChatRequest,
key: APIKey = Depends(get_api_key),
):
response = await llm.chat(
messages=body.messages,
track_tokens=True,
)
credits = tokens_to_credits(response.tokens)
await meter.record(key.stripe_subscription_item_id, credits)
return {"reply": response.content}
The track_tokens=True flag is what makes response.tokens available — without it, you're guessing at cost from response length, which is unreliable across models.
Handling the webhook side
Stripe fires webhooks for subscription lifecycle events — upgrades, downgrades, payment failures, cancellations. Your API needs to react to these, particularly customer.subscription.deleted, or you'll keep serving a customer who's stopped paying.
@router.post("/webhooks/stripe")
async def stripe_webhook(request: Request):
payload = await request.body()
sig = request.headers.get("stripe-signature")
event = stripe.Webhook.construct_event(payload, sig, settings.STRIPE_WEBHOOK_SECRET)
if event["type"] == "customer.subscription.deleted":
await revoke_api_key(event["data"]["object"]["customer"])
elif event["type"] == "invoice.payment_failed":
await flag_account_past_due(event["data"]["object"]["customer"])
return {"status": "ok"}
Always verify the webhook signature. An unverified webhook endpoint is a direct path for someone to fake a "subscription created" event and get free access.
Common mistakes
Metering after the response is sent. If your metering call happens in a BackgroundTasks callback that can silently fail, you'll under-bill without ever finding out. Meter synchronously, or at minimum, meter in a Celery task with retries — not fire-and-forget.
Not handling partial failures. If the LLM call succeeds but the metering call throws, you've given away a free request. Wrap metering in its own try/except with logging, so a Stripe API hiccup doesn't take down your chat endpoint, but also doesn't vanish silently.
Rounding credits too aggressively. If you round every request down to the nearest credit, small requests become free at scale. Track fractional credits internally and only round at the invoice line-item level.
What FastAPI AI Kit includes
The kit ships this entire flow pre-wired: token tracking on every llm.chat() call, a meter.record() helper, and a Stripe webhook handler for subscription lifecycle events. You configure your metered price ID and credit-per-token ratio — the reporting and webhook plumbing is already built and tested.
Build your AI backend with FastAPI AI Kit.
Clone, configure, and ship — everything is already wired up.
