Skip to main content

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: 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. 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. Configure the Stripe customer portal to expose the intended plan changes, cancellation, payment methods and invoice history. Portal features depend on Stripe portal settings. Register the API’s POST /v0/billing/webhooks in Stripe for customer.subscription.created, customer.subscription.updated, and customer.subscription.deleted. Subscription 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

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 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):
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

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