> ## Documentation Index
> Fetch the complete documentation index at: https://docs.one.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 instance access model. Private deployments leave it on **No subscriptions** and never see payment UI. Operators who want a catalogue pick **Included + paid** or **Paid only** and get hosted Checkout, Customer Portal, webhook-driven subscription lifecycle, and quota enforcement.

<Note>
  The software default is **No subscriptions** (`access_model = off`, `default_token_quota = 0` meaning unlimited). Fresh installs and existing self-hosts start there. No billing UI surfaces until an admin picks a Stripe posture.
</Note>

***

## What you get

* **Three access postures**: no subscriptions, included + paid, or paid only
* **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 paid users return to the included tier (or become unentitled in paid-only) 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 → Access and billing**
3. Pick one posture (the instance is always in exactly one):
   * **No subscriptions** (software default) — Stripe unused. Token access is the default monthly quota (`0` = unlimited).
   * **Included + paid** — Stripe required. New users land on the included tier; they can upgrade. Canceled paid users return to the included tier at period end.
   * **Paid only** — Stripe required. No included tier. Users must subscribe before they can call models. Existing included-tier users keep their quota if you switch from Included + paid.
4. The backend validates that both `STRIPE_SECRET_KEY` and `STRIPE_WEBHOOK_SECRET` are present when you pick a Stripe posture — if either is missing it returns 400
5. On first activation of a Stripe posture the backend runs an **idempotent setup**:
   * Included + paid: seeds the included plan (`slug=free`) + a Pro template with an empty Price ID; sets `default_plan_id`; backfills users without a plan
   * Paid only: seeds the Pro template only; does not backfill
   * Copies `default_token_quota` onto the included plan when that plan is seeded (`0` stays `0`, unlimited)
6. Switching back to **No subscriptions** is a **pure flag flip** with no data side effects. The included default plan cannot be deleted while Included + paid is active.

### 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 access_model is freemium or paid_only
3. paid_only + no plan      ── unentitled (stop; do not fall through)
4. system_settings.default_token_quota  ── used when access_model is off
5. unlimited                ── last resort if everything above is unset
```

Step 4 is the instance-wide cap for **No subscriptions**. Under **Paid only**, a user with no plan is unentitled (chat returns 402) even if `default_token_quota` is 0.

### 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                                           |
| -------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------- |
| `system_settings.access_model`                     | `off`                                                       | Admin → System Settings → Access and billing          |
| `billing_plans.monthly_token_quota` (included)     | copied from `default_token_quota` (`0` = unlimited)         | Admin → System Settings → Default Monthly Token Quota |
| `billing_plans.monthly_token_quota` (Pro template) | `5,000,000`                                                 | Admin → Billing → Plans → Pro → Edit                  |
| `system_settings.default_token_quota`              | `0` (unlimited; synced to the included plan under freemium) | Admin → System Settings → Default Monthly Token Quota |
| `system_settings.billing_enabled`                  | `FALSE` (derived from `access_model != off`)                | Admin → System Settings → Access and 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>
