FIM One ships a complete Stripe billing pipeline behind an admin-controlled feature flag. Private deployments without payment needs leave it off and never see the UI. SaaS operators flip one toggle and get hosted Checkout, Customer Portal, webhook-driven subscription lifecycle, and quota enforcement out of the box.Documentation Index
Fetch the complete documentation index at: https://docs.fim.ai/llms.txt
Use this file to discover all available pages before exploring further.
Billing is disabled by default. Fresh installs and existing self-hosts both start with
system_settings.billing_enabled = FALSE. No billing UI surfaces until an admin explicitly activates it.What you get
- Free + Pro tiers with monthly token quotas (defaults: Free 1M / Pro 5M; both tunable post-activation)
- Stripe-hosted Checkout — users upgrade without your code ever touching card data
- Customer Portal — users update payment methods, download invoices, cancel — all on Stripe’s UI
- Webhook-driven lifecycle — subscriptions provision and renew automatically; canceled subs demote to Free at period end
- Quota enforcement — token usage tracked per period; mid-stream cutoff with a structured upgrade prompt
- Admin pages for plan CRUD and subscription monitoring
Prerequisites
- Stripe account with Live mode activated. Singapore-incorporated companies must complete KYC (business UEN, director ID, bank account). Approval typically takes 1-3 days.
- Stripe Live API key of type Restricted (recommended over Standard
sk_live_***— easier to revoke, scoped permissions). - Webhook endpoint publicly reachable at
<your-domain>/api/webhooks/stripe. - Bank account for payouts. Multi-currency settlement (e.g. USD payout to a USD account) is recommended for non-USD-default Stripe accounts to avoid 1.5-2% FX leakage per transaction.
Setup
1. Stripe Dashboard
Create the Pro product
- Catalog → Products → + Add product
- Name:
Pro, description:5M tokens / month, priority support - Pricing: Recurring, monthly, $20.00 USD (adjust to your pricing strategy)
- Save → copy the resulting
price_***ID (you will UPDATE the localbilling_planstable with this value after activation)
Create a Restricted API key
- Developers → API keys → + Create restricted key
- Name:
fim-one production - Permissions (minimum):
- Customers: Write
- Subscriptions: Write
- Checkout Sessions: Write
- Customer portal: Write
- Prices: Read
- Products: Read
- Save → copy
rk_live_***
Register the webhook endpoint
- Developers → Webhooks → + Add endpoint
- URL:
https://<your-domain>/api/webhooks/stripe - Events to receive:
checkout.session.completedcustomer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedinvoice.payment_succeededinvoice.payment_failed
- After save, click “Reveal signing secret” → copy
whsec_***
Configure multi-currency settlement (recommended)
If your Stripe account default currency differs from the price currency you charge in (common case: SGD account charging USD):- Settings → Bank accounts and currencies → Add a settlement currency
- Pick the price currency (e.g. USD)
- Attach the matching bank account (e.g. an Aspire USD virtual account)
- Save — Stripe routes USD charges directly to USD payouts, no FX conversion
2. Backend .env
Set these three keys in your production .env:
.env so the keys are picked up:
3. Activate in Admin
- Log in as an admin
- Admin → System Settings → Billing
- Toggle Enable Stripe Billing ON
- The backend validates that both
STRIPE_SECRET_KEYandSTRIPE_WEBHOOK_SECRETare present — if either is missing it returns 400 and the toggle stays OFF - On first activation only, the backend runs an idempotent setup:
- Seeds Free + Pro plans (skipped if already present)
- Sets
system_settings.default_plan_idto the Free plan id - Backfills
users.plan_id = free.idfor any user without a plan - Syncs
default_token_quota→ Free planmonthly_token_quotaso the existing admin-controlled global quota carries over
- Subsequent toggle off/on is a pure flag flip with no data side effects
4. Update the Pro plan with your live price
After activation, update the seeded Pro plan to point at your live Stripe price:- Admin → Billing → Plans → Pro → Edit
- Paste your
price_1***(from step 1) intoStripe Price ID - Save
5. Smoke test
- Open
/settings?tab=billingas a regular user - Click Switch to Pro
- Stripe Checkout opens; complete with a low-amount real card (refund afterwards)
- Webhook should fire — verify in Stripe Dashboard → Webhooks → recent events show 2xx responses
- Subscription row appears in
subscriptionstable;users.plan_idflips topro - UI now shows Pro plan + “Manage subscription” button
Disabling billing
Toggle the Enable Stripe Billing switch OFF in Admin → System Settings → Billing. When disabled:- All
/api/billing/*endpoints return 503 - The webhook endpoint returns 503 (Stripe will retry, then surface in Dashboard as failing — that’s fine, you can disable the webhook in Stripe Dashboard instead if billing is permanently off)
- The
Plan & Billinguser-facing tab disappears - The Admin → Billing nav group is hidden
- The quota chain skips the plan tier and falls back to
default_token_quotadirectly
subscriptions, billing_plans, and users.plan_id rows are untouched. Re-enabling resumes from the same state with no migration.
Calculation reference — quota & token math
This is the authoritative reference for every numeric rule that decides what a user is allowed to consume, when their counter resets, and how the resolution chain composes. Read this before changing pricing, adjusting quotas, building usage dashboards, or planning v2/v3 work. Future-but-not-yet-shipped rules are documented in their reserved slot so contributors know where new logic plugs in.Glossary
| Variable | Storage | Semantics | Range |
|---|---|---|---|
users.token_quota | per-user (override) | Three-state override; see semantics below | NULL, 0, or positive int |
users.tokens_used_this_period | per-user (counter) | Cumulative tokens since last reset | non-negative int |
users.quota_reset_at | per-user (anchor) | Mirrors Subscription.current_period_end for paid users | timestamp |
users.plan_id | per-user (FK) | Active plan | FK billing_plans.id |
billing_plans.monthly_token_quota | per-plan | Hard cap for users on this plan | non-negative int |
system_settings.default_token_quota | singleton | Defensive fallback when no plan applies | non-negative int |
system_settings.default_plan_id | singleton | Free-plan pointer for new/unassigned users | FK or NULL |
system_settings.billing_enabled | singleton | Master switch — gates step 2 of the chain | boolean |
What counts as a token
Token consumption is accounted at the LLM call layer, sourced from LiteLLM’susage object on every completion.
- Counted: prompt tokens + completion tokens on every model call
- Counted: every round-trip in a multi-step / tool-use agent flow (each model call is its own debit)
- Counted: embedding requests (KB ingestion, retrieval scoring)
- Not counted: input staged but never sent to a model (e.g. uploaded files that the user discards)
- Not counted: requests that fail before reaching the provider (auth error, rate-limit pre-check)
- Cached input: counted at full price in v1 (no provider cache discount is surfaced). v2 may credit cached prompt tokens separately.
Three-state override semantics
users.token_quota is the per-user administrative override. It carries three meanings in one column:
| Value | Meaning | Use case |
|---|---|---|
NULL | Not set — defer to plan / default | Default state for all normal users |
0 | Unlimited | Admin / internal accounts; “VIP gift” |
N > 0 | Hard cap at N | Block an abuser without canceling their paid subscription; pre-paid enterprise allocation |
Quota resolution chain — v1 (current)
For any authenticated request, the cap is computed top-down — first match wins:users.plan_id for every user. It exists as a defense-in-depth so a misconfigured plan never silently uncaps a user.
Period reset
- For paid users,
quota_reset_atmirrorsSubscription.current_period_end. Theinvoice.payment_succeededwebhook handler setstokens_used_this_period = 0and advancesquota_reset_atto the new period end on each successful renewal. - For Free users (no Stripe subscription), an hourly cron rolls
tokens_used_this_periodto 0 on a calendar-month boundary anchored to plan-assignment date. - Plan changes mid-period do not reset the counter — only renewals do. This prevents quota-cycling exploits (“subscribe → use Pro quota → cancel → subscribe again”).
Mid-stream enforcement
- Pre-flight check at chat-call entry: cheapest path, blocks requests the user can’t afford to start.
- During streaming, the running token count is re-evaluated on every chunk. Crossing the cap closes the stream with a structured terminator frame, not a network error.
- The frontend interprets the terminator and surfaces
<QuotaExceededDialog>with a deep link to/settings?tab=billing. - Non-streaming responses return HTTP
402with body{ code: "QUOTA_EXCEEDED", reset_at, upgrade_url }.
Billing-disabled fallback
Whensystem_settings.billing_enabled = FALSE:
- Step 2 of the chain is skipped — the chain collapses to
override → default → unlimited. /api/billing/*and/api/webhooks/stripereturn503.Plan & Billinguser tab and Admin → Billing nav group are hidden.- All billing data (subscriptions, plans,
users.plan_id) is preserved — re-enabling resumes from the same state with no migration.
Reserved: quota chain v2 — Team seats
Not yet shipped. Documented here so v2 work has a known landing spot.
Subscription.quantitycarries seat count (Stripe-native).- A user’s effective plan resolves through Team membership before falling back to their personal plan:
- Quota is per seat (each Team member gets a full
monthly_token_quota), not a pooled bucket. Pooled buckets create first-come-first-served exhaustion and are anti-customer. - Override semantics are unchanged — Team admins can still hard-cap individual members via
users.token_quota = N, which sits above the team plan in the chain.
Reserved: quota chain v3 — native Org allocation (no Stripe)
Not yet shipped. Reserved for on-prem / enterprise deployments that allocate quota internally without paying Stripe per user.
- New table
org_quota_allocations(user_id, monthly_token_quota, org_id)distributes a parent budget across members. - Allocations are per user, not a shared pool — every member has a clear individual SLA.
- Updated chain:
max(), notsum(). A paid Pro user never gets less than they paid for, even if their Org admin sets a low allocation. Stripe-paid quota is sacrosanct.
Reserved: pay-per-use credit balance (v3 separate dimension)
Not yet shipped. A separate axis from the chain above — credits are a one-time top-up, not a subscription tier.
- New table
user_credits(user_id, balance_cents, currency)— funded via Stripe Checkoutmode='payment'. - Consumption order: subscription quota first, then credit balance (only after subscription is exhausted does the credit decrement begin).
- Credit balance is non-refundable (industry standard for prepaid).
- UI exposes both bars:
Subscription quota: 4.2M / 5M used+Credits: $7.40 remaining.
Default values
Ship-time defaults — all tunable post-install except where noted.| Variable | Default | Tunable via |
|---|---|---|
billing_plans.monthly_token_quota (Free) | 1,000,000 | Admin → Billing → Plans → Free → Edit |
billing_plans.monthly_token_quota (Pro) | 5,000,000 | Admin → Billing → Plans → Pro → Edit |
system_settings.default_token_quota | 1,000,000 (synced to Free at activation) | Admin → System Settings → Quotas |
system_settings.billing_enabled | FALSE | Admin → System Settings → Billing |
| Pro list price | $20.00 USD / month | Stripe Dashboard (price object) |
| Stripe webhook events subscribed | 6 | Stripe Dashboard → Webhooks |
| Stripe price cache TTL | 5 minutes | hardcoded in stripe_client.py |
| Subscription lifecycle cron | hourly | APScheduler in web/main.py |
| Free-tier reset cron | hourly (calendar-month boundary) | APScheduler in web/main.py |
Pricing model
V1 is a flat subscription. Free + Pro, monthly billing, USD-only. Out of scope for v1 (intentional, deferred to roadmap):- Team plan (Stripe seats /
subscription.quantity) - Annual billing
- Multi-currency presentment
- Coupons / promo codes
- Tax handling (Stripe Tax integration — needs separate compliance review)
- Usage-based metering / overage charges
- Pay-per-use credit balance (one-time top-up)
Troubleshooting
Toggle won't activate — 400 error
Toggle won't activate — 400 error
The activation endpoint requires both
STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET to be set. Confirm they’re present in .env and that the backend was restarted after editing.Webhook returns 503
Webhook returns 503
Either billing is disabled (toggle is OFF), or the request signature failed verification (mismatched
STRIPE_WEBHOOK_SECRET). Check Stripe Dashboard → Webhooks → recent events for the actual error body.User subscribed but still sees Free plan
User subscribed but still sees Free plan
The
checkout.session.completed webhook didn’t reach your backend. Verify the endpoint URL in Stripe Dashboard matches <your-domain>/api/webhooks/stripe exactly, including the trailing path. Check Webhook recent deliveries for failures.Pro user sees the test-mode price ID
Pro user sees the test-mode price ID
The seed migration writes a test-mode price ID. After activating production billing, update the Pro plan to use your live
price_1*** via Admin → Billing → Plans → Pro → Edit, or via direct SQL UPDATE.Receipts are branded with Stripe defaults
Receipts are branded with Stripe defaults
Configure your business branding in Stripe Dashboard → Settings → Branding. Add your logo, business name (e.g. “FIM Labs Pte. Ltd.”), and address. Stripe applies these to all auto-generated receipts and invoices.
FX losses on payouts
FX losses on payouts
If your Stripe account default currency differs from your charge currency, Stripe converts at every payout (1.5-2% spread). Add a matching settlement currency under Settings → Bank accounts and currencies, attach a same-currency bank account, and Stripe will route same-currency payments without conversion.