> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lupin.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Billing setup and management

> Configure hosted Stripe checkout, customer portal and signed subscription updates.

# Billing

Billing uses Stripe-hosted Checkout and the customer portal. Organization keys
with `api_key_create` permission can read billing status and start management
sessions; resource-scoped keys are rejected. Missing setup keeps online billing
unavailable and the console provides a support path.

## Operator prerequisites

Apply all API migrations (including billing tables), bind D1, and configure:

| Name | Purpose |
| - | - |
| `STRIPE_SECRET_KEY` | Server key for the correct Stripe account; production activation accepts live restricted (`rk_live_`) or standard secret (`sk_live_`) keys |
| `STRIPE_WEBHOOK_SECRET` | Signing secret for this endpoint in the same mode |
| `STRIPE_DEVELOPER_PRICE_ID` | Recurring price matching the Developer plan |
| `STRIPE_GROWTH_PRICE_ID` | Recurring price matching the Growth plan |
| `STRIPE_STARTUP_PRICE_ID` | Recurring price matching the Startup plan |
| `CONSOLE_URL` | HTTPS console origin; checkout and portal return to `/billing` |
| `STRIPE_PORTAL_CONFIGURATION_ID` | Optional dedicated customer portal configuration |

For a live restricted key, grant **Write** to Customers, Checkout Sessions and
Customer portal, and **Read** to Prices and Invoices. Grant Subscriptions **Write** if overage consent must attach metered items. The production
activation check additionally reads Balance and Webhook Endpoints, so grant
**Read** to those resources as well. Leave other resources and connected-account
permissions at None. This reflects the endpoints used by Lupin; see
[Stripe restricted API keys](https://docs.stripe.com/keys/restricted-api-keys).
The webhook signing secret (`whsec_`) is separate from the API key.

If the Stripe account has no Lupin catalog or webhook yet, automatic initial
setup additionally needs temporary **Products: Write**, **Prices: Write** and
**Webhook Endpoints / Event Destinations: Write**. After creating the three monthly
prices and signed webhook, remove Products access and return Prices and Webhook
Endpoints to Read. Subscription reconciliation requires read access; enabling metered items on an existing subscription additionally requires Write.

The production account uses Stripe Managed Payments. Lupin subscription products use the
operator-confirmed **Platform as a service (PaaS) — business use** tax code
`txcd_10102000`. Set the code on each product before testing Checkout; leaving it
empty causes Stripe to reject session creation. The bootstrap includes this code;
`infra/configure-stripe-tax.mjs` can add it to verified, previously created Lupin
products without changing prices or the account's Managed Payments setting.
The category appears in Stripe's
[eligible Managed Payments tax codes](https://docs.stripe.com/payments/managed-payments/eligibility#eligible-tax-codes).

Configure the Stripe customer portal to expose the intended plan changes,
cancellation, payment methods and invoice history. Portal features depend on
[Stripe portal settings](https://docs.stripe.com/customer-management/configure-portal).

Register the API's `POST /v0/billing/webhooks` in Stripe for
`customer.subscription.created`, `customer.subscription.updated`, and
`customer.subscription.deleted`.
[Subscription webhooks](https://docs.stripe.com/billing/subscriptions/webhooks)
carry asynchronous status changes; Lupin verifies the raw body using the
`stripe-signature` HMAC and a five-minute timestamp window. The webhook endpoint
uses its signing secret rather than the customer API-key gate. Preserve the raw
body at any reverse proxy. A checkout success return URL does not grant a plan;
the signed subscription event updates durable account state and entitlements.

## Customer workflow

```bash theme={null}
lupin billing status
lupin billing checkout --plan Developer
# Open the returned URL, complete Checkout, then refresh status.
lupin billing portal
# Open the returned portal URL to manage an existing subscription.
```

The SDK exposes `billing.status()`, `billing.checkout("Developer" | "Growth" | "Startup")`
and `billing.portal()`. An active subscription blocks duplicate checkout; use
the portal to change it. An organization without a Stripe customer receives
`billing_account_missing` when opening the portal. Provider failures return an
explicit error; no plan change is inferred from redirect navigation.

`billing.status().usage` reads the permanent usage ledger. Sent email counts
recipients; deleting a message or resource does not remove recorded usage.
`usage_summary` separates provider-confirmed USD costs, configured estimates and
unpriced activity, and reports the coverage start. Historical usage before ledger
activation is not backfilled, so these remain partial totals rather than an invoice
counter. Billing period timestamps come from subscription state, or the current UTC
calendar month for a free workspace. Inspect provider invoices for actual charges.
See [budgets](https://docs.lupin.sh/platform/budgets.md) for admission limits and coverage boundaries.

Test checkout, plan change, cancellation, webhook retry and portal access in
Stripe test mode before enabling live keys. Deployment configuration alone does
not verify a payment flow. No real payment or subscription was created by the
local regression tests.

## Published catalog and usage controls

Catalog `2026-10-25` has five plans: Free ($0), Developer ($20/month), Growth
($79/month), Startup ($200/month), and Enterprise (contracted). The canonical
allowances, limits, and rational micro-USD retail rates live in
`packages/shared/src/plans.ts` and `packages/shared/src/billing.ts`; the console,
landing pricing, and API use that catalog. Existing Developer subscriptions with
catalog `legacy-developer-10000` retain their 10,000-recipient allowance when enabling metered overages and across renewals. New Developer subscriptions include 5,000 recipients. Use the account entitlement catalog version for consent; the public catalog does not replace a grandfathered allowance.

Use `GET /v0/billing/catalog` for published plans and rates,
`GET /v0/billing/status` for the billing account's plan, payment and cancellation
state, and `GET /v0/billing/usage` for the retail accounting overview. The overview
separates included limits, settled quantities, reserved quantities, remaining
allowances, billable overage quantities, retail estimates, and reserved retail
estimates. Accounting starts at `coverage_start`; earlier usage is not backfilled.
These estimates exclude subscription charges, taxes and provider adjustments.
Provider-confirmed costs and configured provider estimates in `usage_summary`
remain separate from customer retail charges. They are not the customer invoice.

Free has hard limits and cannot enable paid overages. Paid overages are disabled
by default. An active paid account must explicitly accept the catalog, authorize
overages, and set a positive monthly USD cap. The cap covers metered retail
overages, not the subscription or separately quoted purchases. The settings API
accepts an integer number of micro-USD (`1 USD = 1,000,000 micro-USD`):

```bash theme={null}
lupin billing catalog
lupin billing usage
lupin billing settings
# This command authorizes real paid overages for the current catalog:
lupin billing settings --overage-enabled true --confirm-overages \
  --spend-limit-usd 25.00 --catalog-version 2026-10-25
# Disable future overages while retaining the stated cap:
lupin billing settings --overage-enabled false \
  --spend-limit-usd 25.00 --catalog-version 2026-10-25
lupin billing checkout --plan Growth
```

The SDK exposes `billing.catalog()`, `billing.usage()`, `billing.settings()`,
`billing.updateSettings({overage_enabled, spend_limit_micros, catalog_version})`,
and `billing.checkout("Developer" | "Growth" | "Startup")`. The settings write
returns verified persisted settings and the updated metering overview. The
console retains edits and shows an error if subscription reconciliation or
settings storage fails; it does not infer success from a provider redirect.
Already incurred charges remain payable after overages are disabled.

`enforcement_enabled: false` means this overview does **not** establish that
quotas, retention, or overage admission are active. Provider capability and
retention/storage readiness must also be configured before operators advertise
those controls as operational. Phone usage needs destination-specific quotes;
proxy allocation is separately purchased. No phone rate is inferred from the
email or browser catalog. Enterprise allowances require an explicit contract.

## Invoices and recovery

```bash theme={null}
lupin billing invoices --limit 20
lupin billing invoices --limit 20 --starting-after in_previous
lupin billing upcoming
lupin billing portal
```

The SDK equivalents are `billing.invoices({limit, starting_after})` and
`billing.upcoming()`. Invoice list results provide `has_more` and `next_cursor`.
Amounts are in the invoice currency's minor units (USD cents); dates are ISO
strings or null. The upcoming invoice is an **estimated** provider preview and
can report `no_subscription` or `no_upcoming_invoice`. Permission or provider
failures remain errors rather than empty successful history.

Hosted invoice URLs are restricted to HTTPS `invoice.stripe.com` and
`pay.stripe.com`; PDF URLs may additionally use `files.stripe.com`. Credentials,
custom ports, and lookalike hosts are rejected. Links open with `noopener` and
`noreferrer`. Payment recovery and scheduled cancellation management use the
Stripe customer portal; a payment issue does not silently switch plans or enable
overages. Configure **Invoices: Read** for invoice history and previews, and
**Subscriptions: Write** when consent attaches metered items to an existing
subscription. Configure `STRIPE_GROWTH_PRICE_ID` alongside the Developer and
Startup recurring prices. Full metered-catalog configuration and
`BILLING_ENFORCEMENT=1` are required before activating retail metering.

All local console, SDK, and CLI billing regression tests use mocked provider
responses. They do not create invoices, charge a card, or modify subscriptions.

## Explicit enrollment and plan changes

Older workspaces without an explicit billing entitlement are not assigned a paid
plan by matching their resource limits. An owner may enroll them in Free by
accepting the current catalog:

```bash theme={null}
lupin billing enroll --plan Free --catalog-version 2026-10-25
```

`POST /v0/billing/enroll` accepts exactly
`{ "plan": "Free", "catalog_version": "2026-10-25" }`. The SDK equivalent is
`billing.enroll(...)`. Existing paid or already enrolled accounts cannot be
silently replaced by Free. Online paid checkout and explicit Free enrollment are
separate operations.

Existing paid subscriptions can preview a change before accepting proration:

```bash theme={null}
lupin billing preview --plan Growth
# Review the returned invoice preview, effective date, and intent_id first.
lupin billing change --intent-id bci_saved_preview --confirm-plan-change
# Retry with the SAME intent_id if the response is uncertain.
```

Use `billing.previewPlanChange("Growth")` and
`billing.confirmPlanChange(intentId)` in the SDK. Preview responses include the
provider invoice estimate, expiry, and `allowance_effective_at`. The current
cycle's included usage allowance remains unchanged, including on upgrades;
subscription proration and resource capacity are separate from included usage.

`billing.status().pending_change` survives a console reload and reports the
original intent, target plan and pending/payment state. Recovery uses
`POST /v0/billing/change-plan` with the same `intent_id`, never a new plan request.
A verified Stripe invoice payment URL may be returned when payment is required.
An uncertain result, expired preview, or unresolved payment does not grant new
allowances. Use the subscription portal for payment-method recovery.

## Retained content and expiry

`GET /v0/billing/storage` (SDK `billing.storage()`, CLI `billing storage`)
separates stored, reserved, deleting, and available bytes from the storage limit.
Reservations and unfinished deletions continue consuming capacity. It reports
activation time, current retention days, and whether historical-content expiry
was explicitly activated. `enforced: false` does not establish automatic expiry.
Storage overages are currently unavailable: retained storage has a hard cap.
Additional browser concurrency and nondefault sandbox sizes also have no
self-service purchase flow; confirm availability with support before relying on
those optional add-ons.

Changing the retention window defaults to future content. Applying it to
existing retained content requires a distinct confirmation; it assigns a full
new retention window rather than deleting old content immediately:

```bash theme={null}
lupin billing retention --days 20
# Explicitly assign expiry to existing content as well:
lupin billing retention --days 20 --apply-to-existing --confirm-expiry
```

The SDK method is
`billing.updateRetention({retention_days, apply_to_existing, confirmed})`.
`POST /v0/billing/storage/retention` rejects values above the plan maximum and
rejects `apply_to_existing: true` without `confirmed: true`. Expired content can
be permanently deleted; extending retention cannot restore data already deleted.

## Retained content and bounded reads

The workspace storage limit covers retained message and draft content, raw
messages, call transcripts, event payloads and binary attachments. Usage is
measured in UTF-8 or binary bytes. Database indexes and metadata overhead are
excluded. Provider operation journals keep reconciliation and idempotency
information, including some response or message copies; they are retained
separately and are **not covered by content expiry**. A storage total is not a
measurement of your provider's invoice.

New email and draft bodies use immutable R2 versions. Exact reads verify the
stored content; a missing or unavailable body returns `503
content_body_unavailable`, rather than a successful empty message. Earlier inline
bodies remain readable. Attachment downloads and complete outbound attachment
sets are limited to 30 MiB. A retained message body is limited to 10 MiB; `/v0`
request bodies are limited to 32 MiB (`413 request_too_large`). Use attachment
URLs when base64 encoding would exceed the request limit.

Message lists, searches and batch reads are paginated metadata responses. They
include `body_loaded: false` and a `body_url`. Fetch that URL for the complete
message (`body_loaded: true`); append `/raw` for the original RFC822 content.
Draft lists use the same metadata/detail distinction. Lists do not download all
workspace bodies into a worker's memory.

Thread details keep their existing routes, including
`/v0/emails/threads/:thread_id`,
`/v0/emails/inboxes/:inbox_id/emails/threads/:thread_id`, and
`/v0/projects/:project_id/emails/threads/:thread_id`. The response contains
paginated message metadata, `messages_limit` and, when another page exists,
`messages_next_page_token`. Pass that token as `message_page_token`; choose
`message_limit` up to 50. Fetch each message's `body_url` when its body is needed.

Search covers message metadata and the first 8,192 Unicode characters of plain
text. Responses expose this limit through `search_coverage`. HTML-only content
and text beyond the indexed excerpt are not promised as searchable. Search
never downloads every R2 body.

`GET /v0/billing/storage` shows stored, reserved and deletion-pending bytes,
available capacity, retention days and activation dates. Unknown writes keep
their reservation. Deleting an R2 object frees capacity only after deletion is
confirmed. Storage overage billing is currently unavailable: the included byte
limit is a hard cap even when other product overages are enabled.

Newly stored content follows the active retention policy. Existing content has
no new expiry by default. An owner or administrator can use
`POST /v0/billing/storage/retention` with `retention_days` within the current
plan's maximum. Applying it to existing content additionally requires
`apply_to_existing: true` and `confirmed: true`. The effective date starts a full
retention window; it does not immediately remove old content. Expired bodies
are removed while message metadata and permanent usage records remain.

Legacy Custom workspaces without an explicit billing enrollment keep their
existing resource limits. A separate compatibility storage policy captures their
known retained bytes and grants one-time 250 MB of additional headroom. It does
not enroll a plan, reset monthly, authorize paid actions or create usage charges.
Content under that legacy policy receives no automatic expiry. The storage
response identifies it as `policy_kind: legacy_grandfathered`. After explicit
plan enrollment, the plan's byte cap and retention for new content apply;
historical expiry still requires the separate confirmation above.

## Complimentary plans

An operator can explicitly grant a complimentary plan independently of Stripe.
Billing responses identify it through `entitlements.source: complimentary`;
the console shows **\$0 / month · Complimentary**. Included allowances renew on
calendar months. Usage stays metered against those allowances, while paid
overages, Stripe meter exports and automatic subscription charges remain disabled.

The `complimentary-startup` workflow is restricted to the designated verified
administrator's owned workspace. It expires any unused paid checkout, refuses
an existing Stripe subscription, and records the grant in the console audit log.
Replaying it does not reset consumed allowances. A subsequently purchased
subscription returns the workspace to verified Stripe billing periods.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.