Skip to main content
All billing endpoints live under /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 catalogGET /api/billing/plans returns marketing-friendly cards with { priceId, planId, name, currency, monthlyPrice, discount, headline, subheading, features[] }. Use it to populate plan selectors in your UI.
  • Balances & historyGET /api/billing/overview bundles:
    • 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 from globalPrices.

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 priceId values surfaced by /api/billing/plans or your custom Stripe catalog loader.
Once Stripe confirms the subscription, the webhook handler (see below) activates the plan and records any promotional bonuses.

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.
curl http://localhost:3000/api/billing/top-up \
  -X POST \
  -H "Content-Type: application/json" \
  -H "Cookie: token=<session>" \
  -d '{ "amount": "25", "currency": "USD" }'
Notes:
  • amount is 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 through reportContainerUsage 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.