Skip to main content
Plans define pricing tiers for your app. The active plan and subscription window drive overage in the billing summary. Published plans are synced to OpenMeter, which handles authoritative usage metering and retail rate cards.

Authentication

Plan routes use provider dashboard session auth only: a logged-in session whose user is the app owner, a platform admin, or a providerAdmins team member. Confidential M2M / Basic auth is not accepted on plan mutation routes. GET /plans also accepts M2M Basic auth for integrators who need to read plan configuration. POST, PUT, and DELETE additionally require canEditProviderApp (same session, with edit rights).

Base path

{clientId} is the public app_… OAuth client id.

List plans

Returns every plan for the app, each with nested capabilities (pipeline/model bundles).

Query parameters

Default response shape (no apiVersion)

Each plan includes: id, clientId, name, type (free | subscription | usage), priceAmount, priceCurrency, status, isNetworkDefault, isStarterDefault, includedUnits, includedUsdMicros, overageRateWei, billingCycle, openmeterPlanId, lastSyncedAt, syncError, and capabilities (array).

Billing API v2 response (apiVersion=2)

Returns { apiVersion: 2, products: [BillingProduct…] }.

Create a plan

201 Created body: { "id": "<new-plan-uuid>" }.
is_network_default cannot be set on custom plans. The Network Price plan is managed separately via the Plans UI.

Update a plan

Body must include id (plan UUID). Omitted fields keep existing values. If capabilities is present, it replaces all bundles for that plan; omit the key to leave bundles unchanged. PUT on the Network Price plan id returns 400 — edit exclusions via the Plans UI. 200 OK: { "success": true }.

Delete a plan

Removes the plan and its capability bundles. Prefer status: inactive via PUT if subscribers might still reference the plan. Deleting the Network Price default plan returns 409. 200 OK: { "success": true }. 404 if the plan is missing or not owned by this app.

Sync a plan to OpenMeter

Auth: provider dashboard session with edit rights. Triggers an explicit OpenMeter plan sync for the specified plan. Use this when a plan’s rate cards or included allowance have drifted from OpenMeter, or after changing pricing configuration outside the normal publish flow. 200 OK: { "success": true, "openmeterPlanId": "om_plan_...", "syncedAt": "..." }.

Starter plan

Every app has a Starter plan (isStarterDefault: true) separate from custom billing plans. It carries an includedUsdMicros allowance and is automatically subscribed to by new end-users. Providers update the Starter allowance separately from the plans list:
PUT body: { "includedUsdMicros": "5000000" }. This triggers an immediate OpenMeter plan sync. The Starter plan also appears in the GET /plans list (with isStarterDefault: true) but cannot be deleted or replaced by a custom plan.

Errors


OpenMeter sync behavior

When a plan is published with status: active and OPENMETER_URL is configured:
  1. A plan keyed {clientId}:{planId} is created or updated in OpenMeter.
  2. Flat subscription fee, included allowance on network_fee_usd_micros, and usage rate cards are provisioned.
  3. Per-capability retailRateUsd entries create filtered OpenMeter features and rate cards.
  4. Stale openmeterPlanId values are recreated automatically when OpenMeter returns plan-not-found.
Stripe Connect is for invoicing/checkout, not for provisioning plans in OpenMeter.

Examples

Call GET/POST/PUT/DELETE from a trusted context that holds the provider dashboard session cookie.
Do not expose plan mutation to end users; keep session cookies server-side or use the in-app UI.

Implementation reference

Canonical contract: pymthouse docs/builder-api.md (Billing API → Plans). Code: src/app/api/v1/apps/[id]/plans/route.ts, src/app/api/v1/apps/[id]/plans/[planId]/sync/route.ts, src/app/api/v1/apps/[id]/starter-plan/route.ts.