/api/billing/* and require an authenticated, email-verified user unless stated otherwise. Prices are stored in minor currency units and returned as strings to avoid floating-point drift.
Read-only views
- Plan catalog –
GET /api/billing/plansreturns marketing-friendly cards with{ priceId, planId, name, currency, monthlyPrice, discount, headline, subheading, features[] }. Use it to populate plan selectors in your UI. - Balances & history –
GET /api/billing/overviewbundles:balances: available and pending funds per currency.activeSubscription: current plan metadata plus any included bonuses (network, CPU, token pools, etc.).transactions: up to 50 most recent usage records (unit, billed amount, discounts, pending remainder) interleaved with top-ups.usagePricing: canonical per-unit prices derived fromglobalPrices.
Starting or changing a subscription
POST /api/billing/subscription expects { "priceId": "<stripe price id>" } and returns { url }, the Stripe Checkout page for the desired plan.
- The backend prevents downgrades; you must cancel or wait for the current period to expire.
- Upgrades automatically calculate prorated credits by comparing the remaining time on the current plan with the target price. The endpoint returns a checkout URL even when a discount applies.
- Supply
priceIdvalues surfaced by/api/billing/plansor your custom Stripe catalog loader.
Billing portal
POST /api/billing/portal opens the customer portal so users can manage payment methods or cancel subscriptions themselves. The response structure is { url }. Only email-verified users can request a portal link.
Top-ups
Use top-ups when you want prepaid balances without a recurring plan.amountis a decimal string; the server converts it to minor units before creating the Stripe Checkout session.- Only the default currency is supported today.
- The response is
{ url }, just like the subscription endpoint.
Webhooks
POST /api/stripe/webhook ingests Stripe events and requires the raw request body plus the stripe-signature header. Configure Stripe to send checkout.session.completed, customer.subscription.updated, and customer.subscription.deleted events to this endpoint.
For local testing, run stripe listen --forward-to localhost:3000/api/stripe/webhook.
Usage metering
Quartz-level usage data flows throughreportContainerUsage and shows up as billing_usage_records. When the coordinator reports resourceId, amount, and unit, the server returns { action: "continue" } or { action: "shutdown" } to signal whether the workload can keep running. Review the raw usage rows via /api/billing/overview to reconcile your own ledgers.