> ## 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.

# Stripe Billing

> Enable per-user Stripe subscriptions (Free + Pro) with token quotas and self-service plan management.

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.

<Note>
  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.
</Note>

***

## 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

1. **Stripe account** with Live mode activated. Singapore-incorporated companies must complete KYC (business UEN, director ID, bank account). Approval typically takes 1-3 days.
2. **Stripe Live API key** of type Restricted (recommended over Standard `sk_live_***` — easier to revoke, scoped permissions).
3. **Webhook endpoint** publicly reachable at `<your-domain>/api/webhooks/stripe`.
4. **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

1. **Catalog → Products → + Add product**
2. Name: `Pro`, description: `5M tokens / month, priority support`
3. Pricing: Recurring, monthly, \$20.00 USD (adjust to your pricing strategy)
4. Save → copy the resulting `price_***` ID (you will UPDATE the local `billing_plans` table with this value after activation)

#### Create a Restricted API key

1. **Developers → API keys → + Create restricted key**
2. Name: `fim-one production`
3. Permissions (minimum):
   * **Customers**: Write
   * **Subscriptions**: Write
   * **Checkout Sessions**: Write
   * **Customer portal**: Write
   * **Prices**: Read
   * **Products**: Read
4. Save → copy `rk_live_***`

#### Register the webhook endpoint

1. **Developers → Webhooks → + Add endpoint**
2. URL: `https://<your-domain>/api/webhooks/stripe`
3. Events to receive:
   * `checkout.session.completed`
   * `customer.subscription.created`
   * `customer.subscription.updated`
   * `customer.subscription.deleted`
   * `invoice.payment_succeeded`
   * `invoice.payment_failed`
4. 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):

1. **Settings → Bank accounts and currencies → Add a settlement currency**
2. Pick the price currency (e.g. USD)
3. Attach the matching bank account (e.g. an Aspire USD virtual account)
4. Save — Stripe routes USD charges directly to USD payouts, no FX conversion

### 2. Backend `.env`

Set these three keys in your production `.env`:

```bash theme={null}
STRIPE_SECRET_KEY=rk_live_***
STRIPE_WEBHOOK_SECRET=whsec_***
STRIPE_BILLING_RETURN_URL=https://<your-domain>/settings?tab=billing
```

See [Environment Variables](/configuration/environment-variables) for full reference.

Restart the backend after editing `.env` so the keys are picked up:

```bash theme={null}
./deploy.sh   # or: docker compose restart fim-one
```

### 3. Activate in Admin

1. Log in as an admin
2. **Admin → System Settings → Billing**
3. Toggle **Enable Stripe Billing** ON
4. The backend validates that both `STRIPE_SECRET_KEY` and `STRIPE_WEBHOOK_SECRET` are present — if either is missing it returns 400 and the toggle stays OFF
5. On first activation only, the backend runs an **idempotent setup**:
   * Seeds Free + Pro plans (skipped if already present)
   * Sets `system_settings.default_plan_id` to the Free plan id
   * Backfills `users.plan_id = free.id` for any user without a plan
   * Syncs `default_token_quota` → Free plan `monthly_token_quota` so the existing admin-controlled global quota carries over
6. 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) into `Stripe Price ID`
* Save

Or via SQL (if you prefer direct DB access):

```sql theme={null}
UPDATE billing_plans
SET stripe_price_id = 'price_1***'
WHERE slug = 'pro';
```

### 5. Smoke test

1. Open `/settings?tab=billing` as a regular user
2. Click **Switch to Pro**
3. Stripe Checkout opens; complete with a low-amount real card (refund afterwards)
4. Webhook should fire — verify in Stripe Dashboard → Webhooks → recent events show 2xx responses
5. Subscription row appears in `subscriptions` table; `users.plan_id` flips to `pro`
6. 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 & Billing` user-facing tab disappears
* The Admin → Billing nav group is hidden
* The quota chain skips the plan tier and falls back to `default_token_quota` directly

**Data is preserved**: existing `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's `usage` 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 |

The override always wins over plan and default. It exists so admins can pin individual users above or below their plan tier without touching Stripe.

### Quota resolution chain — v1 (current)

For any authenticated request, the cap is computed top-down — **first match wins**:

```
1. users.token_quota        ── NULL? skip. 0? unlimited. N>0? cap at N.
2. users.plan.monthly_token_quota   ── only when billing_enabled = TRUE
3. system_settings.default_token_quota  ── defensive fallback
4. unlimited                ── last resort if everything above is NULL
```

Step 3 is rarely reached when billing is on, because activation backfills `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_at` mirrors `Subscription.current_period_end`. The `invoice.payment_succeeded` webhook handler sets `tokens_used_this_period = 0` and advances `quota_reset_at` to the new period end on each successful renewal.
* For Free users (no Stripe subscription), an hourly cron rolls `tokens_used_this_period` to 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 `402` with body `{ code: "QUOTA_EXCEEDED", reset_at, upgrade_url }`.

### Billing-disabled fallback

When `system_settings.billing_enabled = FALSE`:

* Step 2 of the chain is skipped — the chain collapses to `override → default → unlimited`.
* `/api/billing/*` and `/api/webhooks/stripe` return `503`.
* `Plan & Billing` user 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

<Note>Not yet shipped. Documented here so v2 work has a known landing spot.</Note>

When the Team plan ships:

* `Subscription.quantity` carries seat count (Stripe-native).
* A user's effective plan resolves through Team membership before falling back to their personal plan:
  ```
  effective_plan = team.plan if team_member(user) else user.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)

<Note>Not yet shipped. Reserved for on-prem / enterprise deployments that allocate quota internally without paying Stripe per user.</Note>

* 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:
  ```
  override → max(plan_quota, org_allocation) → default → unlimited
  ```
* `max()`, not `sum()`. 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)

<Note>Not yet shipped. A separate axis from the chain above — credits are a one-time top-up, not a subscription tier.</Note>

* New table `user_credits(user_id, balance_cents, currency)` — funded via Stripe Checkout `mode='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)

See the [roadmap](/roadmap) for what's planned next.

***

## Troubleshooting

<Accordion title="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.
</Accordion>

<Accordion title="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.
</Accordion>

<Accordion title="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.
</Accordion>

<Accordion title="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.
</Accordion>

<Accordion title="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.
</Accordion>

<Accordion title="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.
</Accordion>
