Skip to main content

Documentation

Subscriptions & billing

List plans, start and change subscriptions, open the billing portal, and read invoices from your app with @egav/billing — the subscription plumbing you do not build.

Sell and manage subscriptions

Your app manifest declares only the features (definitions — a type, a default, a quota policy). Plans are defined in the Control Plane: a plan binds per-plan feature values and a price to that vocabulary (see Plans, features & quotas). This page is the other half: how your app sells and manages those subscriptions at runtime, with @egav/billing.

createSubscriptionsClient({ baseUrl, getToken }) wraps the Control Plane's tenant billing surface — the portal-tier /v1/control-plane/portal/* endpoints — so a subscription page is typed method calls, not fetch. It is the same client that already runs the platform's own customer portal.

These endpoints are portal-tier: they require the customer's bearer. Inject a getToken that returns the current access token; the client attaches it as Authorization: Bearer. An optional orgGuid adds the X-Org-Guid header — note this does not scope the request: portal-tier endpoints take the organization from the org_guid claim on the bearer token. The header is a service-to-service convention and is ignored here.

Install

Published as @djokodonev/egav-billing-frontend on GitHub Packages, consumed under the alias @egav/billing. Route the @djokodonev scope to the registry with a token, then alias the package:

.npmrcini
@djokodonev:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_PACKAGES_TOKEN}
package.jsonjson
{
  "dependencies": {
    "@egav/billing": "npm:@djokodonev/egav-billing-frontend@^0.2.0"
  }
}
In a monorepo working copy, a file:../egav-billing-frontend dependency works too — no registry auth needed.

Plans and starting a subscription

List the purchasable plans (also available unauthenticated at sign-up — see Sign-up & onboarding), then start the org's first subscription. A paid plan returns a checkout_url to redirect to; a free or no-provider plan sets activated: true and needs no redirect.

subscribe.tsts
import { createSubscriptionsClient } from '@egav/billing';

const billing = createSubscriptionsClient({
  baseUrl: 'https://cp-api.example.com',
  getToken: () => session.accessToken,   // the customer's portal bearer
});

const plans = await billing.getPlans();

const result = await billing.createCheckout('pro', 'monthly');
if (result.checkout_url) window.location.assign(result.checkout_url);

startTrial() is the no-card alternative — it activates a trial for the current org without a provider round-trip.

Show and change the current subscription

listSubscriptions() returns the org's subscriptions with pagination meta. An org can hold several — a primary plan plus additional aliased workspaces (ADR-010). changeSubscription / cancelSubscription act on the primary; the *ById variants address one by GUID.

manage.tsts
const { data: subscriptions } = await billing.listSubscriptions();

// Change plan or interval on the primary subscription.
await billing.changeSubscription('scale', 'annual');

// Cancel the primary subscription, with a reason.
await billing.cancelSubscription('too_expensive');

// Address a specific subscription by GUID (multi-workspace orgs).
await billing.cancelSubscriptionById(subscriptions[0].guid);

To add an additional aliased subscription (a new workspace) without touching the existing ones — alias is required so the org can tell them apart:

add-workspace.tsts
const added = await billing.addSubscription('pro', { interval: 'monthly', alias: 'client-a' });
if (added.checkout_url) window.location.assign(added.checkout_url);

The billing portal, invoices and profile

For card changes, cancellations and receipts, hand the customer to the payment provider's own hosted billing portal — one call returns the URL:

billing-portal.tsts
const { portal_url } = await billing.openBillingPortal();
window.location.assign(portal_url);

Read invoices and the billing profile directly:

invoices.tsts
const { data: invoices } = await billing.listInvoices({ status: 'paid' });
const { url } = await billing.getInvoicePdfUrl(invoices[0].guid);   // our PDF or provider-hosted

const profile = await billing.getBillingProfile();
await billing.updateBillingProfile({ ...profile, company_name: 'Acme, Inc.' });

One-off products and credits

The same package also sells one-off products — credit packs, seat add-ons — separate from recurring subscriptions. createBillingClient lists active products and opens a one-time checkout; a drop-in BuyCredits widget renders the catalog for you. The client never sends an amount or an org — the Control Plane resolves the price and reads the org from the JWT.

credits.tsts
import { createBillingClient } from '@egav/billing';

const products = createBillingClient({ baseUrl, getToken: () => session.accessToken });

const catalog = await products.listProducts('ai');
const { checkout_url } = await products.createCheckout('ai', 'credits_100', {
  successUrl: 'https://app.example.com/billing?ok=1',
  cancelUrl: 'https://app.example.com/billing',
});

Errors

Every failure throws a BillingError with the HTTP status and the platform errorCode when present — so you branch on a specific failure. The gate a lapsed subscription trips is 402; a plan missing a feature is 403; a breached quota is 429. What puts a subscription into a lapsed state in the first place — cancellation, failed renewals, trial end — and what each state does to the tenant is the Billing lifecycle page.

errors.tsts
import { BillingError } from '@egav/billing';

try {
  await billing.changeSubscription('scale', 'annual');
} catch (err) {
  if (err instanceof BillingError && err.status === 402) redirectToBilling();
}

Endpoints

MethodEndpoint
getPlansGET /v1/control-plane/public/plans
listSubscriptionsGET /v1/control-plane/portal/subscriptions
createCheckout / startTrialPOST /v1/control-plane/portal/onboarding/{checkout,start-trial}
addSubscriptionPOST /v1/control-plane/portal/subscriptions/checkout
changeSubscription / cancelSubscriptionPOST /v1/control-plane/portal/subscription/{change,cancel}
changeSubscriptionById / cancelSubscriptionByIdPOST /v1/control-plane/portal/subscriptions/{guid}/{change,cancel}
openBillingPortalPOST /v1/control-plane/portal/billing/portal
listInvoices / getInvoice / getInvoicePdfUrlGET /v1/control-plane/portal/invoices[/{guid}[/pdf]]
getBillingProfile / updateBillingProfileGET / PUT /v1/control-plane/portal/billing-profile

The subscription, feature and quota a plan grants are enforced on every request by the five gates — see Auth & the five gates.