Plans
A plan is the unit a customer subscribes to. It bundles one or more products with their prices and per-product allowances ("Pro Monthly", "Enterprise Annual", etc.). Subscriptions never attach to the live, mutable plan - they pin to a specific plan version, an immutable snapshot of the plan's contents at publish time.
Plan vs plan version
| Object | Mutable? | What it carries |
|---|---|---|
Plan | Yes (name, description, status, attached products) | The long-lived identifier, edited freely while in draft |
PlanVersion | No, once published | Frozen snapshot: products, their pricing models, the available PlanProductPriceSummary rows (price_id + price_version_id + currency + list_price + optional price_key, display_name, country_code), per-product allowances |
Every subscription stores plan_version_id, so changing the live plan never silently mutates anyone's bill - only an explicit migration can move subscribers between versions.
Lifecycle
A plan's status is one of draft, scheduled, active, or archived. The allowed transitions are:
Code
draft→scheduled,active, orarchivedscheduled→draft,active, orarchivedactive→archivedarchived→ (terminal)
(draft, scheduled, and active can all move directly to archived; the arrows above show the typical forward path.)
| Status | What changes |
|---|---|
draft | Free editing - attach / detach products, change name + description, no subscriber sees this. |
scheduled | A future publish has been queued (via PUT with a future effective_at). The plan is not yet live; the move to active happens at the scheduled time. Can be pulled back to draft, brought live early, or archived. |
active | Published and live. The first move into active creates version 1; later publishes increment the version. New subscriptions can attach. |
archived | Hard stop - no new subscriptions, no further publishes. Existing subscribers continue to bill from their pinned version. |
Status transitions are made via PUT /v1/plans/{id}. To publish now, send {"status":"active"}; the draft → active transition is what freezes the version snapshot. To schedule a future publish, send PUT with effective_at set to a future timestamp (this routes the plan through scheduled). There is no separate publish endpoint.
Plan products & allowances
A plan attaches products via POST /v1/plans/{id}/products and detaches via DELETE /v1/plans/{id}/products/{productId}. Each attachment (PlanProduct) can carry per-period allowance fields:
| Field | Notes |
|---|---|
included_quantity | Quantity included in the base plan price before usage starts being charged. For keyed products, the allowance is pooled across all price_keys. |
rollover_enabled | If true, unused included_quantity from one period is added to the next. Emits allowance.rolled_over on each rollover. |
rollover_max | Hard cap on how much can accumulate from rollovers. |
rollover_expiry_periods | Rolled-over balance expires after this many periods. |
The plan version snapshot freezes each product's PlanProductPriceSummary rows at publish, including the per-price price_key, so the invoice pipeline can detect "this product was keyed when the plan was published" without re-querying the live product or its key set.
Versions: publish, clone, inspect
Code
There is no version-comparison endpoint under /plans. To inspect a structural diff for a pending change, use the scheduled-changes surface (GET /v1/scheduled-changes/{id}/diff), which diffs a scheduled change against the entity's current state.
clone is the right call when you want to ship a price-changed plan as a separate offer (so existing subscribers stay where they are by default and only opt-in customers see the new one). Publishing a new version and running migrate-subscribers is the right call when you want everyone moved to the new version.
Migrating subscribers
POST /v1/plans/{id}/migrate-subscribers previews or executes a bulk move of a plan's subscribers from their current pinned version to a target version.
Code
| Field | Behaviour |
|---|---|
mode | PREVIEW returns the planned-change report without mutating anything; IMMEDIATE executes now; SCHEDULED creates a pending migration that runs at scheduled_at and emits notice events leading up to it. |
target_version | The version subscribers are moved to. Must be ≥ 1 and exist on the plan. |
proration_strategy | Optional. Controls how the in-flight billing period is settled when the migration runs. |
scheduled_at | Required when mode = SCHEDULED (and must be in the future). Setting it on a PREVIEW or IMMEDIATE request is rejected with a validation error. |
The migration is all-or-nothing per plan - there's no per-subscription-ID filter on this endpoint. To move a single subscription, change its plan_version_id directly via the Subscriptions endpoints. On execute, the platform fires plan.subscribers_migrated.
Scheduling other plan changes
For changes that aren't subscriber migrations (e.g. status moves, attached-product edits scheduled for a future cutover), schedule the change by sending PUT /v1/plans/{id} with a future effective_at. The version-backed scheduler then exposes these routes for inspecting and managing the pending change:
Code
These operate on scheduled changes you can list, cancel, reconfirm, or query the resolved timeline of.
Per-plan dunning
Each plan can carry its own dunning policy via /v1/plans/{planId}/dunning-policy - overrides the org-wide default. See Dunning.
Endpoints
Related
- Products - the catalog units plans bundle
- Subscriptions - the entity that pins to a plan version
- Pricing models - the per-product tier-walk algorithms
- Advanced pricing - keyed prices, customer overrides
- Scheduled changes - the underlying mechanism for scheduling a plan change via
PUT /plans/{id}with a futureeffective_at - Dunning - per-plan overrides