A plan is a reusable pricing template that defines what a customer gets and how they're charged. Plans combine one or more products with pricing rules and are attached to subscriptions.
Plans support versioning - when you update a plan's pricing or product lineup, existing subscribers stay on their current version until explicitly migrated. This lets you iterate on pricing without disrupting active subscriptions.
Key concepts:
- Products - the items included in this plan, each with optional included quantities and rollover rules
- Versions - immutable snapshots of the plan configuration at a point in time
- Publishing - makes a draft plan version available for new subscriptions
- Migration - moves existing subscribers from one plan version to another, with configurable proration
List plans
Returns a paginated list of plans with optional status and custom field filters.
query Parameters
created_atFilter on created_at (date-time). Operators: eq, gt, gte, lt, lte — dot grammar, e.g. created_at.gt=value; a bare created_at=value means eq.
external_idFilter on external_id (string). Operators: eq, in — dot grammar, e.g. external_id.in=value; a bare external_id=value means eq. A bare comma-separated value is in-sugar: external_id=a,b means external_id.in=a,b.
nameFilter on name (string). Operators: eq, in, contains — dot grammar, e.g. name.in=value; a bare name=value means eq. A bare comma-separated value is in-sugar: name=a,b means name.in=a,b.
published_atFilter on published_at (date-time). Operators: eq, gt, gte, lt, lte — dot grammar, e.g. published_at.gt=value; a bare published_at=value means eq.
updated_atFilter on updated_at (date-time). Operators: eq, gt, gte, lt, lte — dot grammar, e.g. updated_at.gt=value; a bare updated_at=value means eq.
versionFilter on version (number). Operators: eq, gt, gte, lt, lte — dot grammar, e.g. version.gt=value; a bare version=value means eq.
limitPage size. Values above the server-side cap are clamped (standard default 50, cap 200; a few document-heavy lists use larger windows). Invalid values fall back to the default.
cursorOpaque continuation token from the previous response's pagination.cursor. Omit for the first page. Cursors are stateless and do not expire, but are only valid for the list and filters that produced them.
searchCase-insensitive substring match over name and external_id; relevance-ranked (exact > prefix > substring) unless an explicit sort is given
statusFilter by status. Filterable fields (status, name, external_id, version, published_at, created_at, updated_at) accept apifilter operator suffixes; custom_fields.
tagFilter by tag name (repeatable, containment semantics)
countsComma-separated countable fields (status) to include per-value counts for
has_scheduled_versionFilter by whether the entity has a version scheduled to publish (true or false). A scheduled version is a FROZEN version row a pending catalog.publish_version Change is bound to; the entity itself stays draft, published or archived and its working draft stays free for immediate changes.
sortOrdering: created_at, name, status or description, each with an optional :asc/:desc suffix (default created_at:desc). Plans with no description sort last in both directions. version is deliberately not sortable - the list renders the joined plan_versions.version, and a joined column cannot be keyset-ordered. An unknown value is rejected. Cursors are bound to the ordering that issued them, so changing sort mid-walk needs a fresh first page. Sorting suppresses relevance ranking when search is also given.
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
List plans › Responses
OK
Create a plan
Creates a new plan in draft status.
Headers
Idempotency-KeyUnique key that makes this POST safe to retry: repeats with the same key replay the first response instead of re-executing. Replays are scoped to the retrying principal (same API key / user) and kept for 24 hours. Required on every POST.
Client-generated idempotency key (e.g. a UUID).
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Create a plan › Request Body
nameInternal operator-facing plan name.
Org-defined custom field values for this plan.
descriptionInternal operator-facing description; not shown to buyers.
external_idCaller-supplied external identifier for reconciliation with an upstream system.
Free-form operator key/value metadata.
price_cadence_unitThe billing period this plan's prices are authored for (week, month or year); defaults to month. Subscriptions, quotes and checkouts against this plan must bill on exactly this cadence.
public_descriptionBuyer-facing description shown in checkout and the customer portal.
Locale-keyed overrides for name, description and public_description.
Create a plan › Responses
Created
Bulk republish plans
Republishes each listed plan so its 'latest'-mode products re-snapshot at the product head. The manual fallback for plans the product→plan cascade did not update automatically. Best-effort per plan.
Headers
Idempotency-KeyUnique key that makes this POST safe to retry: repeats with the same key replay the first response instead of re-executing. Replays are scoped to the retrying principal (same API key / user) and kept for 24 hours. Required on every POST.
Client-generated idempotency key (e.g. a UUID).
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Bulk republish plans › Request Body
plan_idsPlans to republish so their latest-mode products re-snapshot at the product head; at least one, at most 100 per request.
Bulk republish plans › Responses
OK
Get a plan
Returns a plan with its associated products.
path Parameters
idPlan UUID
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Get a plan › Responses
OK
Delete a plan
Soft-deletes a plan by UUID.
path Parameters
idPlan UUID
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Delete a plan › Responses
No Content
Update a plan
Merge-patch update: only supplied fields change, and none of them publish. Display fields (name, description, price cadence, metadata, …) land live on the plan immediately; the composition — attached products, their terms and prices — is edited through the plan's own sub-routes and stages on the plan's draft head, which you can read with GET /v1/plans/{id}/draft and discard with DELETE. Taking the draft live, now or on a date, is POST /v1/changes with operation catalog.publish_version and subject_kind=plan, whose pins hold named products at a chosen version. status="active" here is refused, and there is no effective_at, save_as_draft, publish_now or product_version_pins. status can still archive the plan, which is terminal. expected_version is the optimistic-concurrency token: when supplied and stale, the update returns 409.
path Parameters
idPlan UUID
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Update a plan › Request Body
Org-defined custom field values for this plan.
descriptionNew internal description; null leaves it unchanged.
expected_versionOptimistic-concurrency token: send the version you last read and a stale value answers 409 CONFLICT. Omitted: the update applies unconditionally (last write wins).
external_idNew external identifier; null leaves it unchanged.
Free-form operator key/value metadata.
nameNew internal plan name; null leaves it unchanged.
price_cadence_unitNew price cadence (week, month or year); null leaves it unchanged. Changing it re-scopes which billing intervals existing and future subscriptions may use — it does NOT restate any price amount.
public_descriptionNew buyer-facing description; null leaves it unchanged.
statusNew catalog lifecycle status (draft, scheduled, active, archived); null leaves it unchanged.
Locale-keyed overrides for name, description and public_description.
Update a plan › Responses
OK
Copy a plan
Creates a new draft plan copying the source plan's name (with " (Copy)" suffix unless overridden), description, metadata, and every plan_products row. Versions are NOT copied - the new plan starts at version 1, status 'draft'. external_id is intentionally NOT copied.
path Parameters
idSource plan UUID
Headers
Idempotency-KeyUnique key that makes this POST safe to retry: repeats with the same key replay the first response instead of re-executing. Replays are scoped to the retrying principal (same API key / user) and kept for 24 hours. Required on every POST.
Client-generated idempotency key (e.g. a UUID).
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Copy a plan › Responses
Created
Get the staged draft
The entity's open draft: its row id (the same id catalog.publish_version binds as staged_version_id), the version number it will publish as, when it was opened and last touched, the head version it forks from, and a counted summary of what is staged. 404 when nothing is staged — which is a state, not an error.
path Parameters
idThe entity's UUID
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Get the staged draft › Responses
OK
Open the plan's working draft on a chosen base
Opens the working draft if none is open, and records which release it is authored on top of: the live head (omit base_version_id) or one of the plan's scheduled releases. Re-basing an open draft keeps its staged rail edits only while it has none; a draft that already holds edits is refused with 409. A publish of a draft based on a scheduled release cannot be scheduled to fire before that release does.
path Parameters
idPlan UUID
Headers
Idempotency-KeyUnique key that makes this POST safe to retry: repeats with the same key replay the first response instead of re-executing. Replays are scoped to the retrying principal (same API key / user) and kept for 24 hours. Required on every POST.
Client-generated idempotency key (e.g. a UUID).
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Open the plan's working draft on a chosen base › Request Body
base_version_idThe release the working draft is authored on top of — the live head, or one of the plan's scheduled releases (its scheduled_versions[].version_id). Omit for the head. Re-basing an open draft keeps its staged rail edits.
Open the plan's working draft on a chosen base › Responses
OK
Discard the staged draft
Throws the staged draft away. The live entity is untouched: nothing pointed at the draft and it was never on the billing timeline. 404 when nothing is staged, which is a different answer from a successful discard and is reported as one.
path Parameters
idThe entity's UUID
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Discard the staged draft › Responses
No Content
Migrate plan subscribers
Previews or executes subscriber migration to a new plan version.
path Parameters
idPlan UUID
Headers
Idempotency-KeyUnique key that makes this POST safe to retry: repeats with the same key replay the first response instead of re-executing. Replays are scoped to the retrying principal (same API key / user) and kept for 24 hours. Required on every POST.
Client-generated idempotency key (e.g. a UUID).
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Migrate plan subscribers › Request Body
modeMigration mode: PREVIEW (dry run, no changes), IMMEDIATE (migrate now) or SCHEDULED (migrate at scheduled_at). Matched case-insensitively: the server also accepts the all-uppercase and all-lowercase form of each value. The form published here is the canonical one and is what reads back.
target_versionPlan version to migrate subscribers onto; must be greater than 0.
proration_strategyOptional proration strategy applied to the version change.
scheduled_atWhen to run the migration; required for and only valid with SCHEDULED mode, and must be in the future.
Migrate plan subscribers › Responses
OK
Add a product to a plan
Adds a product to the plan's working membership together with its allowance configuration (included quantity, rollover, refresh cadence, REFRESHING/POOL kind, limits). No product version is pinned at attach time: the membership's product_track_mode (defaulting to the org setting, else 'latest') decides at each plan publish whether the snapshot follows the product head or holds a pinned version. The membership reaches subscribers on the plan's next publish.
path Parameters
idPlan UUID
Headers
Idempotency-KeyUnique key that makes this POST safe to retry: repeats with the same key replay the first response instead of re-executing. Replays are scoped to the retrying principal (same API key / user) and kept for 24 hours. Required on every POST.
Client-generated idempotency key (e.g. a UUID).
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Add a product to a plan › Request Body
product_idProduct to attach to the plan.
default_quantityQuantity a new subscription seeds for this product when the caller passes no explicit item; omit to seed 1.
included_quantity^-?\d+(\.\d+)?$Quantity bundled into the base price before per-unit billing applies (the included allowance/seat floor); null means none.
max_quantityHighest quantity a buyer may pick for this product; omit for no plan-level ceiling.
min_quantityLowest quantity a buyer may pick for this product; omit for 1.
product_track_modeHow this membership tracks the product version: latest (auto-follow head) or pinned; omitted seeds from the org default.
refresh_countPlan-level default allowance-refresh cadence count; null (with refresh_unit) means one whole-period pool.
refresh_unitUnit for the refresh cadence: day, week, month, year or billing_cycle.
rollover_enabledWhen true, unused included allowance carries forward into later periods.
rollover_expiry_periodsNumber of periods after which rolled-over allowance expires; null means it never expires.
rollover_max^-?\d+(\.\d+)?$Cap on accumulated rolled-over allowance; null means uncapped.
unit_limit^-?\d+(\.\d+)?$Ceiling on a POOL's unit balance; POOL only.
Add a product to a plan › Responses
Created
Remove a product from a plan
Deletes the plan-product membership row. Already-published plan versions keep their snapshot — the removal takes effect on the plan's next publish. Responds 404 when the product is not on the plan.
path Parameters
idPlan UUID
productIdProduct UUID
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Remove a product from a plan › Responses
No Content
Update a plan-product membership
Updates a product's membership on a plan: the version track mode, the purchasable quantity band, and what the price includes. Band and allowance are each opt-in (quantity_band_set / allowance_set) and rewritten whole. Terms edit the membership's DRAFT revision and reach customers at the next publish; the track mode applies immediately.
path Parameters
idPlan UUID
productIdProduct UUID
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Update a plan-product membership › Request Body
allowance_setSet true to rewrite the included allowance from the seven fields below; false leaves the stored allowance untouched.
default_quantityQuantity a new subscription seeds for this product; null clears it (seeds 1). Only applied when quantity_band_set is true.
included_quantity^-?\d+(\.\d+)?$Quantity bundled into the base price before per-unit billing applies; null clears it. Only applied when allowance_set is true.
max_quantityHighest quantity a buyer may pick; null clears it (no plan-level ceiling). Only applied when quantity_band_set is true.
min_quantityLowest quantity a buyer may pick; null clears it (1). Only applied when quantity_band_set is true.
product_track_modeNew version-tracking mode for the membership: latest (auto-follow the product head) or pinned.
quantity_band_setSet true to rewrite the quantity band from the three fields below; false leaves the stored band untouched.
refresh_countHow often the included quantity comes back, paired with refresh_unit; null (with refresh_unit) means one whole-period pool. Only applied when allowance_set is true.
refresh_unitUnit for the refresh cadence: day, week, month, year, billing_cycle, or never (granted once). Only applied when allowance_set is true.
rollover_enabledWhen true, unused included allowance carries forward. Only applied when allowance_set is true.
rollover_expiry_periodsPeriods after which carried-forward allowance expires; null means it never expires. Only applied when allowance_set is true.
rollover_max^-?\d+(\.\d+)?$Cap on the accumulated carried-forward bank; null means uncapped, 0 means nothing carries. Only applied when allowance_set is true.
unit_limit^-?\d+(\.\d+)?$Lifetime ceiling on units drawn; only valid beside a never-refreshing included quantity. Only applied when allowance_set is true.
Update a plan-product membership › Responses
OK
Preview the impact of publishing a plan version
Read-only dry-run: how many ACTIVE subscriptions on this plan track the latest version and would migrate, how many hold a version pin and would NOT, and the split of the movers by version change strategy (i.e. when the new prices actually bill).
path Parameters
idPlan UUID
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Preview the impact of publishing a plan version › Responses
OK
Would publishing this plan succeed?
Read-only dry-run of the publish preconditions: runs the real publish inside a transaction and rolls it back, so the answer comes from the code that would refuse rather than a paraphrase of it. Returns ok=true, or the refusal code and the operator-facing message — which for the override-pin refusals names the remedy, because the fix is on a different entity than the plan being published.
path Parameters
idPlan UUID
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Would publishing this plan succeed? › Responses
OK
List plan versions (with lifecycle filter)
Returns version rows for the entity, keyset-paginated (default 100, max 200 per page). The status filter accepts draft, scheduled, published, archived; the default excludes draft (ADR-0007). Supports ?include=usage_count and ?archived_reason=.
path Parameters
idEntity UUID
query Parameters
statusLifecycle status filter (repeatable / comma-separated): draft, scheduled, published, archived. Default excludes draft.
archived_reasonNarrow archived rows by reason (repeatable / comma-separated): canceled, discarded, errored, superseded
includeComma-separated includes. Currently supports usage_count.
limitPage size. Values above the server-side cap are clamped (standard default 50, cap 200; a few document-heavy lists use larger windows). Invalid values fall back to the default.
cursorOpaque continuation token from the previous response's pagination.cursor. Omit for the first page. Cursors are stateless and do not expire, but are only valid for the list and filters that produced them.
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
List plan versions (with lifecycle filter) › Responses
OK
Get a plan version snapshot
Returns the historical snapshot of the given plan version (same shape as GET /plans/{id}).
path Parameters
idEntity UUID
versionVersion number
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Get a plan version snapshot › Responses
OK
Name or annotate a plan version
Sets the version's release_name and/or release_note. These are display metadata, not content: editing them never creates a new version and is allowed on published versions. An absent field is left unchanged; an empty string clears it.
path Parameters
idEntity UUID
versionThe version number
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Name or annotate a plan version › Request Body
release_nameNew name; empty string clears it. Absent leaves it unchanged.
release_noteNew note; empty string clears it. Absent leaves it unchanged.
Name or annotate a plan version › Responses
OK
created_ateffective_fromeffective_toidoriginWho made this version: manual (an operator published it), scheduled_release (a scheduled publish fired), cascade (republished automatically because something it depends on published).
published_atstatusversionarchived_reasonFor origin=cascade: the version whose publish caused this one, resolved to its entity and number.
created_byrank1-based position in the entity's queue of scheduled versions — the order they will fire. Present only on scheduled rows.
release_nameOperator-given name of this version, shown beside its number.
release_noteFree-text note about this version.
scheduled_change_idThe pending catalog.publish_version Change that will publish this version — reschedule or cancel it there.
scheduled_forWhen this scheduled version is due to publish. Present only on the row a pending catalog.publish_version Change is bound to.
usage_countForecast the impact of superseding this plan version
Who is using the version in force today, and what a publish over it would move. pinnable reports whether holding a version is possible for this entity at all — it is false for tax rules, which are dated rather than pinned, so a caller must not render a pinned count for them. republishes_consumers is the cross-entity fan-out (publishing a price formula republishes every ACTIVE product whose head tiers reference it), and live_readers counts consumers billing off the live head, who move as soon as the publish lands. Read-only, and uncached: this is read at the moment money changes.
path Parameters
idEntity UUID
versionVersion number (positive integer)
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Forecast the impact of superseding this plan version › Responses
OK
List plan version referrers
Offset-paginated drill-down of the referrers of one type for one version of the entity.
path Parameters
idEntity UUID
versionVersion number (positive integer)
query Parameters
typeReferrer type to list (required)
limitPage size. Values above the server-side cap are clamped (standard default 50, cap 200; a few document-heavy lists use larger windows). Invalid values fall back to the default.
cursorOpaque continuation token from the previous response's pagination.cursor. Omit for the first page. Cursors are stateless and do not expire, but are only valid for the list and filters that produced them.
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
List plan version referrers › Responses
OK
Get plan version usage summary
Referrer counts for one version of the entity, grouped by referrer type. Powers the version-switcher hover card.
path Parameters
idEntity UUID
versionVersion number (positive integer)
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Get plan version usage summary › Responses
OK