--- name: clickfunnels-subscription-changes description: > Manage existing subscription orders programmatically - discover allowed variant/price upgrade or downgrade targets for a subscription line item, preview the upcoming invoice impact, then commit the change. Use this skill any time you need to move a customer's subscription onto a different plan or billing cadence without cancelling and re-buying. version: "1.0" author: ClickFunnels tools: - http references: - path: https://accounts.myclickfunnels.com/llms.txt description: Quick-reference index - product overview, login, funnels, broadcasts, API docs. - path: https://accounts.myclickfunnels.com/.well-known/funnels/skill.md description: Funnel workflow APIs - where a subscription was sold from in the first place. --- # ClickFunnels Subscription Changes Skill A self-serve flow for switching the variant or price on an existing subscription line item. Modeled on the change-plan flows partners are used to from other billing platforms, but with one CF2-specific constraint: legal transitions are pre-configured by the merchant on the source variant. You can only target a variant/price combination the merchant has declared "allowed" for the source price. ## The three-step flow | Step | Method | Path | Purpose | |------|--------|------|---------| | Discover | `GET` | `/api/v2/products/prices/:price_id/change_options` | List allowed upgrade and downgrade targets for a source price. | | Preview | `POST` | `/api/v2/orders/line_items/:line_item_id/changes` | Non-destructive quote of the upcoming invoice for a proposed change. | | Commit | `POST` | `/api/v2/orders/line_items/:line_item_id/changes/perform` | Apply the change. Flips the line item's variant + price. | All three accept either the integer database id or the obfuscated `public_id` string in the path; the `products_price_id` body param accepts both forms too. ## Step 1 - Discover what's allowed Pass the **current** price id of the line item you want to change. Response is grouped by **target variant**; each group lists the prices on that variant the subscription may switch to. ```bash curl -X GET \ -H "Authorization: Bearer YOUR_TOKEN" \ "https://YOUR_WORKSPACE.myclickfunnels.com/api/v2/products/prices/66/change_options" ``` ```json { "products_prices_change_options": { "upgrade": [ { "products_variant": { "id": 124, "public_id": "k7xQzR", "name": "Pro", "product_id": 88, "prices": [ { "id": 456, "public_id": "aBcDeF", "name": "Pro Monthly", "key": "pro_monthly_2024", "amount": "20.00", "currency": "USD", "duration": null, "interval": "months", "interval_count": 1 }, { "id": 457, "public_id": "gHiJkL", "name": "Pro Annual", "key": "pro_annual_2024", "amount": "200.00", "currency": "USD", "duration": null, "interval": "years", "interval_count": 1 } ] } } ], "downgrade": [] } } ``` Notes on the shape: - `upgrade` and `downgrade` are siblings; the array key is the direction. - Groups are keyed by `products_variant` because multiple billing options on a single target variant are common (monthly + annual on the same "Pro" tier). - Each price carries the minimal display payload (`id`, `public_id`, `name`, `key`, `amount`, `currency`, `duration`, `interval`, `interval_count`) - enough to render a plan selector without a second fetch. - When no options are configured, you get `{ "upgrade": [], "downgrade": [] }` with status 200 - never a 404. Empty arrays let you hide the upgrade UI cleanly when a price has no targets. ## Step 2 - Preview the change Pass `products_price_id` (the target) - wrapped under `orders_line_items_change`. The target variant is derived from the price's `variant` association. ```bash curl -X POST \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{"orders_line_items_change": {"products_price_id": 456}}' \ "https://YOUR_WORKSPACE.myclickfunnels.com/api/v2/orders/line_items/204/changes" ``` ```json { "orders_line_items_change": { "preview": true, "upcoming_invoice": { "total_amount": "35.83", "subtotal_amount": "35.83", "tax_amount": "0.00", "shipping_amount": "0.00", "discount_amount": "0.00", "currency": "USD", "next_invoice_date": "2026-06-14T03:49:45Z" }, "previous_line_item": { /* current line item state, pre-change */ }, "new_line_item": { /* proposed line item state with the new variant + price */ } } } ``` The preview is a non-destructive quote - no charge happens, the line item is not mutated. `upcoming_invoice` holds the upstream invoice fields (totals, currency, next invoice date) so you can show the customer what they'll be charged. `previous_line_item` reflects the **current** state and `new_line_item` reflects what it would become after the change, so you can diff the two directly without having to construct the comparison yourself. Optional body params: - `prorate` (boolean) - override the default proration behaviour for this call. - `effective_time` (`"now"` or `"next_renewal"`) - override default timing. Defaults if you omit them: | Direction | Default `prorate` | Default `effective_time` | |-----------|-------------------|--------------------------| | Upgrade | `true` | `"now"` | | Downgrade | `false` | `"next_renewal"` | Physical line items always force `prorate=false`, regardless of what you send. ## Step 3 - Commit the change Same body shape as preview. The response envelope is identical; only the `preview` flag flips to `false`. `upcoming_invoice` carries the upstream invoice quote snapshotted just before the commit was applied - so you don't have to call preview separately to know what the customer was charged. ```bash curl -X POST \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{"orders_line_items_change": {"products_price_id": 456, "effective_time": "now"}}' \ "https://YOUR_WORKSPACE.myclickfunnels.com/api/v2/orders/line_items/204/changes/perform" ``` ```json { "orders_line_items_change": { "preview": false, "upcoming_invoice": { "total_amount": "35.83", "subtotal_amount": "35.83", "tax_amount": "0.00", "shipping_amount": "0.00", "discount_amount": "0.00", "currency": "USD", "next_invoice_date": "2026-06-14T03:49:45Z" }, "previous_line_item": { /* line item state before the change */ }, "new_line_item": { /* updated state - variant + price flipped */ } } } ``` On success: - The persisted `line_item.variant_id` and `line_item.products_price_id` are now the values shown in `new_line_item`. - The `subscription.line_items.changed` webhook fires. If the upstream quote snapshot returns no data, the commit still succeeds - `upcoming_invoice`'s amount fields fall back to `null` and `currency`/`next_invoice_date` fall back to the order's stored values. ## Allowed transitions, in plain words A change is allowed if the merchant has configured an upgrade/downgrade option row matching `(current variant, current price -> target variant, target price)`. If no such row exists, you get **422 `target_not_allowed`**. The merchant adds options in the admin UI under **Product -> Variant -> Upgrade/Downgrade options**. If the target price you send is the **same as the current price**, the call returns **422 `same_target`** - there's nothing to change. ## Eligibility The line item's order must qualify for self-serve change. This means: - The order is a subscription order. - It has exactly **one** subscription-type line item. - It has only `subscription` and/or `one_time` payment types - no payment plans. If not, you get **422 `not_eligible`**. One-time orders, mixed payment-plan orders, and orders with multiple subscription line items can't be self-served through this API. ## Error responses | Code | When | |------|------| | 401 | Missing or invalid bearer token. | | 404 | Resource not found, including a cross-workspace resource. | | 422 | `invalid_effective_time`, `not_eligible`, `same_target` (target price matches the current price), `target_not_allowed`, `Invalid products_price_id` (target belongs to a different workspace on the same team), `preview_failed`, `commit_failed`. | ## Gotchas - **Empty discovery is not 404.** When no options are configured, you get a 200 response with empty arrays. Don't treat empty as an error. - **Public IDs work everywhere.** Path `:price_id` / `:id` and body `products_price_id` both accept the obfuscated `public_id` strings - no need to integer-cast them client-side. - **Posting the current price as the target returns 422.** Both preview and commit reject "change to the same price" as `same_target`. Skip the call client-side when the user hasn't picked a different price. - **A preview without a payment method on the order fails.** The upstream payment platform's quote endpoint requires the order to have a billing payment method - wire one up first.