Billing API
Read published plans without auth, then manage a subscription, usage, credit top-ups, and the Stripe portal on Openship Cloud. Self-hosted Openship has no billing.
The Billing API has two halves. GET /api/billing/plans is public and present
on every instance — it serves the published plan catalog, prices included, so a
pricing page or signup flow can read it before anyone has a session. Everything
else is Openship Cloud only: Stripe-backed subscriptions, metered usage,
credit top-ups, and the customer portal.
Base path & auth
All paths are relative to your instance, under /api — e.g.
https://your-host/api/billing/plans. GET /plans needs no credentials. The
rest take a personal access token as a bearer header
(Authorization: Bearer <token>), created with
openship token create; the dashboard uses your session
cookie instead. See the API overview for the full auth model.
Self-hosted has no billing
On a self-hosted instance only GET /plans is mounted. There's nothing to pay,
no subscription, and no usage meter — see the
billing guide.
Endpoints
| Method & path | Permission | What it does |
|---|---|---|
GET /api/billing/plans | public | The published plan catalog — prices, limits, features, localized copy. |
GET /api/billing/state | billing:read | Dashboard snapshot: plan, period, credit balance, capacity. |
GET /api/billing/usage | billing:read | Metered usage over the current period. |
GET /api/billing/subscription | billing:read | The org's subscription slice — tier, status, period. |
POST /api/billing/subscription | billing:write | Start a Stripe Checkout session for a plan change. |
POST /api/billing/cancel | billing:admin | End the subscription at the close of the paid period. |
GET /api/billing/topup-packs | billing:read | The credit packs available to buy. |
POST /api/billing/topup | billing:write | Start Checkout for a one-shot credit pack. |
POST /api/billing/portal | billing:write | Mint a Stripe billing-portal session (invoices, cards). |
List plans
GET /api/billing/plansPublic on every deploy mode. The response is the resolved pricing catalog: one entry per tier in display order, with prices in the currency's minor units (cents), the numeric limits enforcement reads, and finished localized feature strings.
Pass ?locale=<lang> — or send an Accept-Language header — to pick the
language. An unsupported tag falls back to its base language, then to English;
the locale that was actually used comes back in the response.
The envelope is { data: { locale, annual, ui, plans } }. annual reports
whether annual terms are published (enabled) and how many months an annual term
waives; ui carries the short labels a pricing UI needs — the /mo suffix, the
"Custom" and "Free" words, the "Most popular" badge — already translated, because
a marketing site may have no dictionary of its own.
Each entry in plans looks like:
Prop
Type
Two conventions matter when you render this:
price.monthlyis in cents, andnullmeans the tier is priced per contract — pair it withcontactSalesand render theui.customlabel rather than a number.- A
nulllimit is always "unlimited", never zero.annual.enabledisfalsewhile annual terms are unpublished, and everyprice.annualisnullto match — keep annual affordances hidden while it is.
Prices and limits come from one committed catalog in @repo/core, which the API,
the dashboard, and the pricing page all read, so they cannot drift
apart.
Subscription and usage
GET /state is the snapshot the dashboard's Billing screen renders in one call:
the active tier, the period you're in, the credit balance, and how much of the
plan's capacity is in use. GET /usage returns the metered series behind the
usage chart. GET /subscription returns just the subscription slice — tier,
status, and period boundaries.
POST /subscription starts a Stripe Checkout session for an upgrade or downgrade
and returns the URL to send the browser to. The local subscription row is not
written by that call — Stripe's customer.subscription.* webhooks finalize it,
so a plan change only lands once payment actually clears.
POST /cancel flips the subscription to cancel_at_period_end. Access stays
until the paid period ends, then the deletion webhook downgrades the org to the
free tier. Nothing is deleted by cancelling.
Top-ups and the portal
GET /topup-packs lists the one-shot credit packs from the same catalog;
POST /topup opens Checkout for one. Credits bought this way sit on top of the
plan's monthly allowance.
POST /portal mints a short-lived Stripe billing-portal session for invoices and
payment methods. It's a POST because each call is a Stripe-side mutation, and
it's rate-limited per organization for the same reason.