Billing
Billing status (subscription or credit balance)
Billing status (subscription or credit balance)
Usage/charge history (prepaid), most recent first
Usage/charge history (prepaid), most recent first
Start a Stripe Checkout session for an existing instance (subscription upgrade or one-time bundle top-up)
For a prepaid top-up, any existing Stripe Customer Balance credit on this instance's account (see docs/billing.md's 'Self-serve billing management' section) is applied first — `appliedCreditCents` in the response reports how much. If it fully covers the bundle price, `checkoutUrl` is null (credits were already granted synchronously, nothing to pay); if it only partially covers it, the Checkout Session charges just the discounted remainder and the credit is consumed once that payment is confirmed by the webhook.
Preview of the next scheduled charge for an active fixed subscription
`upcoming` is null when there's nothing scheduled (no subscription, or prepaid mode). Backed by Stripe's invoice preview — never a locally-computed estimate.
Billing history — real Stripe invoices (fixed) or credit-purchase history (prepaid)
Both billing modes are mapped to the same InvoiceEntry shape so the dashboard renders either one identically. Prepaid entries never have a hostedUrl (no Stripe customer is ever created for a one-time guest payment).
Read-only preview of what POST .../change-cycle would actually charge/credit right now
Applies the exact same validation as the real change-cycle route (active subscription required, target cycle must differ, a real Stripe Price must exist) and never changes anything — backed by Stripe's own invoice-preview machinery, so the number can't drift from what actually happens on confirm. Meant to back a confirmation step before the customer commits to a switch.
Switch an already-active fixed subscription between monthly and yearly
Requires an active/trialing/past_due/unpaid subscription and a real cycle change (not the current one). Any amount owed for the switch is charged immediately (Stripe `proration_behavior: always_invoice`) — never deferred to the next invoice.
Read-only preview of what POST .../switch-to-prepaid would actually credit right now
`creditCents` is 0 (not an error) when there's no active subscription to cancel — nothing to preview. Meant to back a confirmation step before the customer commits to canceling.
Switch a fixed instance to pay-as-you-go
Cancels the Stripe subscription immediately and flips Instance.billingMode to prepaid right away — no Checkout is needed to stop paying. The unused portion of the current billing period is credited to the customer's Stripe balance (Stripe's own Customer Balance feature — `creditCents` in the response is what was granted), applied automatically to whatever this customer is charged next, rather than forfeited.
Start a Stripe Billing Portal session for this instance's customer
Returns a URL to Stripe's own hosted Billing Portal, where the customer can update their payment method, view/download past invoices, and manage or cancel their subscription directly with Stripe — this app never handles card details itself. Requires this instance to already have a Stripe customer (`Instance.stripeCustomerId`, set the first time one is needed — a bundle purchase, a fixed-plan checkout, or a credit conversion — independent of whether a subscription exists); a `400` here means neither has happened yet. A `400` can also mean the Stripe account has no Customer Portal configuration yet (test-mode accounts get a default one automatically; live accounts need one created once — see docs/billing.md) — Stripe's own rejection message is passed through in that case.
Stripe Customer Balance credit available on this instance's account
Reads Stripe's own Customer Balance (docs/billing.md's 'Self-serve billing management' section) — never a balance tracked in our own database. This credit is applied automatically to whatever this customer is charged next (a subscription renewal, a cycle switch's prorated invoice, etc). Always `{ creditCents: 0, currency: <platform currency> }` (not an error) for an instance that has never had a Stripe customer at all (`Instance.stripeCustomerId` unset — no bundle purchase, checkout, or credit conversion has happened for it yet).
Start a Stripe Checkout session for a brand-new instance (no instance is created until Stripe confirms payment)
Used by the dashboard's 'Add Instance' wizard once the tenant's free trial has ended. Unlike /instances/{instanceId}/billing/checkout, no Instance row exists yet — the desired instance's shape travels as Checkout Session metadata, and is only created by the Stripe webhook handler once `checkout.session.completed` actually arrives. See docs/billing.md.
One-time reveal of the instance+apiKey a pending-instance Checkout just created
Poll this after a POST /billing/checkout redirect completes — the webhook that creates the instance may not have run yet. The row is deleted as part of being read, so it can be fetched successfully at most once; a 404 means either 'not created yet, keep polling' or 'already revealed' (indistinguishable by design).