Plan changes in a pricing plan group
Move a subscription from one plan to another in the same pricing plan group in a single call, with the timing decided by the group rather than by the caller.
Why this matters
An upgrade from Starter to Pro is one commercial decision. As a billing operation it is several: end the running schedule on the right date, open a new one on the target plan, keep the currency and billing period, and get the pro-ration right. Doing that by hand means the same sequence of calls every time. The date it lands on is then whatever the caller passed, rather than what the plan family says it should be.
A pricing plan group already holds that policy: which plans belong together, in what order, and when a move between them takes effect. change-plan executes it. An upgrade becomes one request, and the timing rules live with the plans instead of in your integration.
How it works
A group carries an ordered list of member plans and three transition policies:
The move itself is a new schedule on the same subscription, pointing at the target plan. The schedule that was running is closed at the moment the new one starts, so the subscription still has exactly one schedule in effect.
Direction is inferred, never passed. A target with a higher order than the current plan is an upgrade and follows upgrade_type; a lower one is a downgrade and follows downgrade_type. That policy then resolves the date:
IMMEDIATE and IMMEDIATE_PRO_RATA resolve to the same start moment. What follows from that start is the subscription’s own pro-ration behaviour: whether the closed period is charged in full or pro-rated. The two policies describe that behaviour rather than change it.
Change the plan
Send the target plan. Everything else is either inferred or carried over:
The response is the new schedule, returned with 201 Created. See Change the pricing plan of a subscription in the API reference. The call needs both PRICING_PLAN_SUBSCRIPTION.UPDATE and PRICING_PLAN_SCHEDULE.CREATE, since it updates a subscription by creating a schedule on it.
What the new schedule takes with it
Anything negotiated onto the schedule being replaced is customer-specific pricing on a plan the customer is leaving. It is not copied onto the plan they are moving to. Configure it again on the new schedule if it still applies.
A target plan that is a rate card works the same way. The version applicable at the new schedule’s start is resolved, and the schedule keeps following the rate card rather than being pinned to that version.
Change only the billing period
A plan sold in several billing periods, monthly and yearly against the same pricing, is moved between them by sending the plan the subscription is already on together with a different billing_period:
There is no order difference to read the direction from here, so it comes from the length of the period instead. A longer billing period is an upgrade, a shorter one a downgrade. The group’s policy for that direction then decides the timing as usual. Two periods of the same length, MONTH/12 against YEAR/1, carry no direction and are rejected.
Preview the invoice of the change
An upgrade screen has to show what the move costs before the customer commits to it. POST /v1/invoices/change-plan/preview returns the invoice the change would produce, and leaves the subscription exactly as it is.
The body is the one change-plan takes, with the subscription named in it rather than in the path:
The change is resolved exactly as change-plan resolves it. The group’s policy decides the start, the start decides which version of the target plan applies, and the invoice is dated on that start. A NEXT_BILLING_PERIOD policy therefore previews the invoice of the next period, and an immediate policy previews the invoice of the change itself.
From there, the same billing logic runs as on the real invoice. The schedule being replaced is shortened to the moment the new one starts and billed as a schedule that pays afterwards, with periods that were already billed in advance corrected. The new schedule is billed in advance for its first period. A discount or a running coupon that carries over to the new schedule is applied in the preview as well, so the amount matches what the customer would actually be charged.
Everything change-plan refuses, the preview refuses in the same way and on the same field: a subscription that is not ACTIVE, a target outside the group, a NOT_ALLOWED transition, a start_at on a NEXT_BILLING_PERIOD policy. Checking the preview first is therefore also a way to find out whether the move is allowed at all. The call needs the INVOICE.PREVIEW permission, not the two the change itself needs.
The response is a full invoice object, not the InvoicePreviewActionResponse that POST /v1/invoices/preview returns. Nothing is persisted: the invoice has no counterpart in the database, and neither do the schedules it was billed from.
From the customer portal
POST /v1/portal/invoices/change-plan/preview takes the same body and returns the same invoice, for the subscription the session is scoped to. It checks that the subscription belongs to the customer the portal token resolves to, and needs no permission beyond that.
Let the customer change their own plan
An upgrade screen in the customer portal needs two things: the plans it may offer, and a way to make the move. The portal serves both for the subscription the session is scoped to, so the whole flow runs without your backend in the middle.
Read the group behind the subscription
The response is the group itself. It carries the members with their order, plus the upgrade_type, downgrade_type and cancellation_type. Those tell the screen which moves are possible, and when each would take effect. The group is resolved from the schedule running right now, so a subscription whose plan is in no group answers 404.
Perform the change
The body, the response and every rule above are the same as on the platform endpoint: the same pricing_plan_id, start_at and billing_period, the same group policy deciding the timing, the same 201 Created with the new schedule. The portal adds one check on top, that the subscription belongs to the customer the session is scoped to, so a customer can only ever move their own subscription. There is no permission to grant: the portal session is the authorisation. Call the portal preview first to show the amount on the confirmation step.
The hosted portal does not render an upgrade screen on top of this yet. The endpoint is there for the screen you build with the SDK, which is why the group read and the change are separate calls: your screen decides how the options are presented.
Portal endpoints authenticate with the short-lived access token the SDK exchanges the portal token for, not with an API key. Never call them with X-API-KEY, and never put an API key in a browser. For the changes a customer can make inside their current plan, see Self-serve plan changes.
Expand the group into a pricing table
A member of a group is a plan id and an order, which is not enough to render a screen: an upgrade page has to show what each plan is called and what it costs. expand[] fills that in on the same response, on GET /v1/pricing-plan-groups, on a single group, and on the portal’s pricing-plan-group route.
Only ACTIVE versions are returned, so a plan whose next version is still DRAFT shows the one that is sellable today. Pricing categories are the only thing that can be expanded further inside a version; a deeper path is ignored rather than refused.
Expanding is per request, not a setting. Ask for the versions and categories on the screen that renders the price table, and for the plain group everywhere you only need the member list and the policies.
What is rejected
Via Desk
You manage the group in Desk under Pricing → Plans, on the Plan groups tab. There you set its member plans and their order, the timing of upgrades, downgrades and cancellations, and its status. Desk has no action yet that moves a subscription between plans in a group. An agent making the same move edits the subscription’s schedules directly, which the group’s policies do not bind. The policies exist to keep an automated or self-service change on the terms the plan family was sold on.
Edge cases
- The move is not a migration. Migrating a schedule moves it to a different version of the plan it is already on.
change-planmoves the subscription to a different plan. That is why it builds a new schedule instead of remapping the existing one. start_atcannot be in the past. The subscription also has to be active at that moment, so a date before its start is rejected.- Only billing subscriptions. A quote subscription is rejected: agreed terms change through a new quote version, not through this action.
- The group is resolved from the current plan, not from the request. If the subscription’s plan was removed from its group, the call fails. It does not fall back to the target’s group.