API

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 & pathPermissionWhat it does
GET /api/billing/planspublicThe published plan catalog — prices, limits, features, localized copy.
GET /api/billing/statebilling:readDashboard snapshot: plan, period, credit balance, capacity.
GET /api/billing/usagebilling:readMetered usage over the current period.
GET /api/billing/subscriptionbilling:readThe org's subscription slice — tier, status, period.
POST /api/billing/subscriptionbilling:writeStart a Stripe Checkout session for a plan change.
POST /api/billing/cancelbilling:adminEnd the subscription at the close of the paid period.
GET /api/billing/topup-packsbilling:readThe credit packs available to buy.
POST /api/billing/topupbilling:writeStart Checkout for a one-shot credit pack.
POST /api/billing/portalbilling:writeMint a Stripe billing-portal session (invoices, cards).

List plans

GET /api/billing/plans

Public 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.monthly is in cents, and null means the tier is priced per contract — pair it with contactSales and render the ui.custom label rather than a number.
  • A null limit is always "unlimited", never zero. annual.enabled is false while annual terms are unpublished, and every price.annual is null to 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.

On this page