***

title: Change subscription plan
subtitle: Move items to different billing intervals or contract terms with automatic subscription splitting, proration handling, and support for immediate or scheduled execution.
---------------------

For clean Markdown of any page, append .md to the page URL. For a complete documentation index, see https://docs.paymentkit.com/guides/billing/subscriptions/llms.txt. For full documentation content, see https://docs.paymentkit.com/guides/billing/subscriptions/llms-full.txt.

<Warning>
  **Deprecation planned:** This endpoint will be deprecated in a future API version in favor of the [subscription change requests](/billing/subscription-change-request) workflow, which provides better preview support, payment control, and idempotent retry handling. New integrations should use change requests instead.
</Warning>

Use the change-plan endpoint when you need to move items to different billing intervals or contract terms. This is the recommended approach for upgrades and downgrades that change the billing cycle.

```bash
curl -X POST https://api.paymentkit.com/api/{account_id}/subscriptions/{subscription_id}/change-plan \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
  "items": [
    {"action": "update", "subscription_item_id": "si_basic_monthly", "new_price_id": "price_pro_annual"}
  ],
  "proration_behavior": "always_invoice"
}'
```

<Tip>
  For changes within the same billing interval (quantity updates, add-ons, price swaps), use the [update subscription items](/billing/subscription-update#update-subscription-items) endpoint instead.
</Tip>

# Request fields

| Field                | Type    | Required | Description                                                                                                                                                                                                                                                                                                                                        |
| -------------------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `items`              | array   | Yes      | List of item changes (min 1 item required)                                                                                                                                                                                                                                                                                                         |
| `proration_behavior` | string  | Yes      | How to handle proration: `always_invoice`, `create_prorations`, or `none`                                                                                                                                                                                                                                                                          |
| `effective_at`       | string  | No       | When the plan change takes effect: `immediate` (default) or `period_end`. Use `period_end` to schedule the change for the end of the current billing period.                                                                                                                                                                                       |
| `reason`             | string  | No       | Reason for the plan change (default: `"change_plan"`)                                                                                                                                                                                                                                                                                              |
| `pay_before_change`  | boolean | No       | If true (default), payment must succeed before the plan change commits. On payment failure (including 3D Secure), the original subscription remains unchanged. If false, the plan change commits regardless of payment status, and new subscriptions may be left in `incomplete` state. Requires `proration_behavior: "always_invoice"` when true. |
| `metadata`           | object  | No       | Additional metadata to add to new subscription(s)                                                                                                                                                                                                                                                                                                  |

## Proration behavior options

* **`always_invoice`**: Create invoice immediately and attempt payment now. Use when you want the customer to pay for the upgrade immediately.
* **`create_prorations`**: Create floating items (`invoice_id=NULL`) that will be collected at the next renewal. Use for deferred billing.
* **`none`**: No proration charges or credits are created. Items are updated immediately but no billing adjustment is made for the mid-cycle change.

# How term-based splitting works

When items change to prices with different billing terms, PaymentKit automatically creates new subscriptions grouped by their terms:

```mermaid
flowchart LR
    A[Monthly Subscription<br />Items A, B, C] --> B{Change Plan}
    B --> C[Monthly Subscription<br />Item C unchanged]
    B --> D[Annual Subscription<br />Item A 12-month contract]
    B --> E[Annual Subscription<br />Item B no contract]
```

Items are grouped by their subscription terms:

* **Billing interval** (month, year, etc.)
* **Billing interval count** (every 1 month, every 3 months, etc.)
* **Contract length** (12 billing cycles, indefinite, etc.)
* **Auto-renew setting** (whether contract renews at end of term)

# Item actions

Each item mapping requires an `action` field and additional fields depending on the action:

| Action   | Required fields                        | Forbidden fields       | Description                                                                                                   |
| -------- | -------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------- |
| `add`    | `new_price_id`                         | `subscription_item_id` | Add a new item to the subscription. `quantity` is optional and defaults to 1 if not specified.                |
| `update` | `subscription_item_id`, `new_price_id` | -                      | Change an existing item's price and/or quantity. `quantity` is optional; if omitted, keeps existing quantity. |
| `delete` | `subscription_item_id`                 | `new_price_id`         | Remove an item from the subscription.                                                                         |

Items NOT included in the request stay unchanged on the original subscription.

# Response fields

| Field                              | Type     | Description                                                                                          |
| ---------------------------------- | -------- | ---------------------------------------------------------------------------------------------------- |
| `original_subscription_id`         | string   | ID of the original subscription                                                                      |
| `original_cancelled`               | boolean  | True if original was cancelled (all items moved/deleted)                                             |
| `original_items_remaining`         | integer  | Number of active items still on original subscription                                                |
| `original_subscription_updated_at` | datetime | Last update timestamp of the original subscription                                                   |
| `created_subscriptions`            | array    | New subscriptions created (one per unique billing terms)                                             |
| `items_added`                      | integer  | Number of new items created via ADD action                                                           |
| `proration_credit_atom`            | integer  | Credit for unused time on moved/deleted items (negative, in atomic units)                            |
| `proration_charge_atom`            | integer  | Charge for new subscription periods (positive, in atomic units)                                      |
| `net_amount_atom`                  | integer  | Net proration amount (credit + charge, in atomic units)                                              |
| `invoice_id`                       | string   | ID of proration invoice (if `always_invoice` behavior)                                               |
| `payment_status`                   | string   | Payment status: `paid`, `processing`, `failed`, `requires_action`, `no_payment_method`, or `pending` |
| `voided_invoice_ids`               | array    | IDs of voided pending renewal invoices                                                               |

## Created subscription fields

Each subscription in `created_subscriptions` includes:

| Field                    | Type    | Description                                                                                                                   |
| ------------------------ | ------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `subscription_id`        | string  | ID of the new subscription                                                                                                    |
| `state`                  | string  | State of the new subscription (`active`, `incomplete`). Incomplete means payment failed or requires action (e.g., 3D Secure). |
| `billing_interval`       | string  | Billing interval (month, year, etc)                                                                                           |
| `billing_interval_count` | integer | Interval count (1, 3, etc)                                                                                                    |
| `items_count`            | integer | Number of items on this subscription                                                                                          |
| `total_billing_cycles`   | integer | Contract length in billing cycles (null = indefinite)                                                                         |
| `contract_auto_renew`    | boolean | Whether contract auto-renews at end of term                                                                                   |

# Examples

## Monthly to annual upgrade

Upgrade a customer from monthly ($100/mo) to annual ($1,000/yr) billing with immediate payment:

<Tabs>
  <Tab title="API">
    ```bash
    curl -X POST https://api.paymentkit.com/api/{account_id}/subscriptions/{subscription_id}/change-plan \
    -H "Authorization: Bearer sk_live_..." \
    -H "Content-Type: application/json" \
    -d '{
      "items": [
        {
          "action": "update",
          "subscription_item_id": "si_monthly_plan",
          "new_price_id": "price_annual_plan"
        }
      ],
      "proration_behavior": "always_invoice"
    }'
    ```
  </Tab>

  <Tab title="Python SDK">
    ```python
    result = client.subscriptions.change_plan(
        account_id="acc_abc123",
        subscription_id="sub_abc123",
        items=[{
            "action": "update",
            "subscription_item_id": "si_monthly_plan",
            "new_price_id": "price_annual_plan"
        }],
        proration_behavior="always_invoice"
    )
    ```
  </Tab>
</Tabs>

The response includes details about the plan change:

```json
{
  "original_subscription_id": "sub_abc123",
  "original_cancelled": true,
  "original_items_remaining": 0,
  "original_subscription_updated_at": "2026-03-10T15:30:00Z",
  "created_subscriptions": [
    {
      "subscription_id": "sub_xyz789",
      "state": "active",
      "billing_interval": "year",
      "billing_interval_count": 1,
      "items_count": 1,
      "total_billing_cycles": null,
      "contract_auto_renew": false
    }
  ],
  "items_added": 0,
  "proration_credit_atom": -5000,
  "proration_charge_atom": 100000,
  "net_amount_atom": 95000,
  "invoice_id": "in_change123",
  "payment_status": "paid",
  "voided_invoice_ids": []
}
```

In this example:

* The customer receives a \$50 credit for unused time on the monthly plan
* The customer is charged \$1,000 for the annual plan
* The net invoice is \$950
* The original subscription is cancelled (all items moved)
* A new annual subscription is created

### Payment error responses

This endpoint uses the **Charge First pattern** by default — payment is collected BEFORE any subscription changes are made. The behavior differs based on the `pay_before_change` setting:

**With `pay_before_change: false` (default) — Automatic Charge First:**

Payment is attempted immediately. If payment fails, the API raises an exception and **no subscription changes are made**:

```python
# Python SDK - exception raised on payment failure
from payment_kit.exceptions import ChangePlanPaymentFailedError

try:
    result = client.subscriptions.change_plan(
        account_id="acc_abc123",
        subscription_id="sub_abc123",
        items=[{
            "action": "update",
            "subscription_item_id": "si_monthly_plan",
            "new_price_id": "price_annual_plan"
        }],
        proration_behavior="always_invoice"
    )
except ChangePlanPaymentFailedError as e:
    # Subscription unchanged - safe to retry
    print(f"Payment failed: {e.payment_error}")
    print(f"Status: {e.payment_status}")
    print(f"Invoice: {e.invoice_id}")
```

The subscription items remain unchanged. The customer must update their payment method and retry.

**Error response structure:**

The exception includes all payment failure details:

* `payment_error` — User-friendly error message
* `payment_status` — Status code (e.g., `"failed"`, `"no_payment_method"`)
* `invoice_id` — Invoice external ID for reference
* `orchestrator_summary` — Technical details about the failure

**With `pay_before_change: true` — Deferred Charge First:**

The API returns a **402 Payment Required** error. No subscription changes are made until payment succeeds.

```json
{
  "error": "payment_required",
  "message": "Payment is required before applying plan changes",
  "invoice_id": "in_change123",
  "payment_status": "failed",
  "payment_error": "Your card was declined. Please try a different payment method.",
  "original_subscription_id": "sub_abc123",
  "proration_amount_atom": 95000
}
```

The customer must update their payment method and retry. The original subscription remains active with no changes.

**3D Secure handling:**

**With `pay_before_change: false` (default):**
The MIT payment orchestrator handles 3D Secure authentication automatically. If authentication fails or cannot be completed automatically, the payment fails and the exception is raised (no changes applied).

**With `pay_before_change: true`:**
If 3D Secure is required, you'll receive a 402 response with authentication details:

```json
{
  "error": "payment_required",
  "message": "Payment requires customer authentication",
  "invoice_id": "in_change123",
  "payment_status": "requires_action",
  "payment_intent_id": "pi_xyz789",
  "client_secret": "pi_xyz789_secret_abc123",
  "original_subscription_id": "sub_abc123",
  "proration_amount_atom": 95000
}
```

Use the `client_secret` with PaymentKit.js to complete authentication:

```javascript
const { error } = await paymentKit.confirmCardPayment(clientSecret);
if (error) {
  // Handle authentication failure
} else {
  // Payment succeeded - plan change will be applied automatically
}
```

**No payment method:**

```json
{
  "original_subscription_id": "sub_abc123",
  "original_cancelled": false,
  "original_items_remaining": 1,
  "created_subscriptions": [],
  "invoice_id": "in_change123",
  "payment_status": "no_payment_method",
  "payment_error": "No payment method available for customer",
  "voided_invoice_ids": []
}
```

<Info>
  **Charge First guarantees:** Both `pay_before_change` modes use the Charge First pattern:

  * ✅ Payment collected BEFORE subscription changes
  * ✅ If payment fails, subscription remains unchanged
  * ✅ No `incomplete` subscriptions on payment failure
  * ✅ Safe to retry without double-charging

  The difference is **when** payment happens:

  * `pay_before_change: false` (default) — Payment attempted immediately in the same API call
  * `pay_before_change: true` — Payment deferred to separate invoice collection call (402 response)

  For multi-step workflow with explicit preview, use [subscription change requests](/billing/subscription-change-request) instead.
</Info>

## Add items during plan change

Add a new item while upgrading to a different plan:

```bash
curl -X POST https://api.paymentkit.com/api/{account_id}/subscriptions/{subscription_id}/change-plan \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
  "items": [
    {"action": "update", "subscription_item_id": "si_basic", "new_price_id": "price_pro_annual"},
    {"action": "add", "new_price_id": "price_addon_support", "quantity": 1}
  ],
  "proration_behavior": "always_invoice",
  "pay_before_change": false
}'
```

## Partial plan change

Change only some items while keeping others unchanged:

```bash
curl -X POST https://api.paymentkit.com/api/{account_id}/subscriptions/{subscription_id}/change-plan \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
  "items": [
    {"action": "update", "subscription_item_id": "si_storage", "new_price_id": "price_storage_annual"}
  ],
  "proration_behavior": "always_invoice",
  "pay_before_change": false
}'
```

Items not included in the request remain on the original subscription with their current billing terms.

## Deferred billing with create\_prorations

Upgrade a plan but defer billing to the next renewal:

```bash
curl -X POST https://api.paymentkit.com/api/{account_id}/subscriptions/{subscription_id}/change-plan \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
  "items": [
    {"action": "update", "subscription_item_id": "si_basic", "new_price_id": "price_pro"}
  ],
  "proration_behavior": "create_prorations",
  "pay_before_change": false
}'
```

With `create_prorations`, proration amounts are calculated but stored as floating line items (`invoice_id=NULL`). They will be included in the next renewal invoice rather than creating an immediate invoice.

# Pay-before-change flow

By default, the change plan endpoint uses **Charge First** pattern with automatic payment collection in the same API call. Set `pay_before_change: true` to defer payment to a separate step:

```bash
curl -X POST https://api.paymentkit.com/api/{account_id}/subscriptions/{subscription_id}/change-plan \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
  "items": [
    {"action": "update", "subscription_item_id": "si_basic", "new_price_id": "price_enterprise"}
  ],
  "proration_behavior": "always_invoice",
  "pay_before_change": true
}'
```

With `pay_before_change: true`:

1. **New subscriptions are created in `incomplete` state** - items are added but not yet active
2. **Original subscription remains unchanged** - items are NOT deactivated and the subscription is NOT cancelled until payment succeeds
3. **A 402 Payment Required response is returned** with the `invoice_id` to collect
4. **Collect the invoice separately** to complete the plan change:
   * If payment succeeds: Items on the original subscription are deactivated, the original subscription is cancelled (if empty), and new subscriptions are activated automatically
   * If payment fails or requires action (e.g., 3D Secure): The customer stays on their original subscription with no disruption

See [Payment error responses](#payment-error-responses) above for detailed examples of failed payment, 3D Secure, and missing payment method scenarios.

<Note>
  `pay_before_change: true` requires `proration_behavior: "always_invoice"`. Without an invoice, there is nothing to collect payment on.
</Note>

# Scheduled plan changes

Use `effective_at: "period_end"` to schedule a plan change for the end of the current billing period instead of applying it immediately. This is useful when you want to honor a customer's remaining time on their current plan before switching.

```bash
curl -X POST https://api.paymentkit.com/api/{account_id}/subscriptions/{subscription_id}/change-plan \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
  "items": [
    {"action": "update", "subscription_item_id": "si_monthly_plan", "new_price_id": "price_annual_plan"}
  ],
  "proration_behavior": "always_invoice",
  "effective_at": "period_end"
}'
```

When `effective_at` is set to `"period_end"`, the plan change is stored as a pending change on the subscription and executed automatically at the end of the current billing period. No items are moved, no invoices are created, and no subscriptions are cancelled until that time.

The response confirms the pending change was stored:

```json
{
  "original_subscription_id": "sub_abc123",
  "original_cancelled": false,
  "original_items_remaining": 1,
  "original_subscription_updated_at": "2026-03-10T12:00:00Z",
  "created_subscriptions": [],
  "items_added": 0,
  "proration_credit_atom": 0,
  "proration_charge_atom": 0,
  "net_amount_atom": 0,
  "invoice_id": null,
  "payment_status": null,
  "voided_invoice_ids": [],
  "effective_at": "period_end",
  "scheduled_for": "2026-04-10T00:00:00Z",
  "pending_change_id": "ppc_live_a1b2c3d4e5f6g7h8"
}
```

For deferred changes, `original_items_remaining` reflects the current item count on the subscription. Items are not moved or cancelled until the change executes at `scheduled_for`. Fields like `created_subscriptions`, `invoice_id`, and proration amounts are empty because the actual plan change has not executed yet.

## Deferred change response fields

| Field               | Description                                                                      |
| ------------------- | -------------------------------------------------------------------------------- |
| `effective_at`      | `"period_end"` — confirms the change is deferred                                 |
| `scheduled_for`     | Timestamp when the change will execute (the subscription's `current_period_end`) |
| `pending_change_id` | Unique ID for the pending change, used to cancel it later                        |

## Cancel a pending plan change

Cancel a scheduled plan change before it executes using the cancel endpoint:

```bash
curl -X DELETE https://api.paymentkit.com/api/{account_id}/subscriptions/{subscription_id}/pending-change \
-H "Authorization: Bearer sk_live_..."
```

Response when a pending change exists:

```json
{
  "status": "cancelled",
  "subscription_id": "sub_abc123",
  "previous_pending_change": {
    "id": "ppc_live_a1b2c3d4e5f6g7h8",
    "created_at": "2026-03-10T12:00:00Z",
    "scheduled_for": "2026-04-10T00:00:00Z",
    "items": [
      {"action": "update", "subscription_item_id": "123", "new_price_id": "456", "quantity": null}
    ],
    "reason": "change_plan",
    "proration_behavior": "always_invoice",
    "metadata": null
  }
}
```

<Note>
  The `subscription_item_id` and `new_price_id` values in the `items` array are internal identifiers, not the external IDs used in other API endpoints.
</Note>

Response when no pending change exists:

```json
{
  "status": "not_found",
  "subscription_id": "sub_abc123",
  "previous_pending_change": null
}
```

## Constraints and limitations

* **One pending change at a time.** A subscription can only have one pending plan change. To schedule a different change, cancel the existing one first, then schedule the new change.
* **`pay_before_change` is not supported.** `effective_at: "period_end"` cannot be combined with `pay_before_change: true`. Deferred changes don't create an invoice until execution time, so there is nothing to collect payment on in advance.
* **Default `proration_behavior` is `always_invoice`.** When the deferred change executes at period end, an invoice is created immediately for the first period of the new subscription and payment is attempted. If payment fails, the new subscription is created in `incomplete` state and enters dunning. You can override this by specifying a different `proration_behavior` when scheduling the change.
* **Deferred execution vs. deferred billing.** `effective_at: "period_end"` defers *when* the plan change happens — items stay on the current plan until period end. This is different from `proration_behavior: "create_prorations"`, which executes the change immediately but defers billing to the next renewal invoice. You can use `effective_at: "period_end"` with any `proration_behavior` value — the proration behavior applies when the change executes at period end.

# Webhooks

## Webhook sequence for immediate changes

When a plan change executes immediately (default behavior), the webhook sequence depends on whether items move to different billing intervals:

**Same-interval changes** (no subscription split):

1. `customer.subscription.updated` — subscription items updated

**Cross-interval changes** (subscription split):

1. `customer.subscription.created` — new subscription(s) created with the target billing interval
2. `customer.subscription.cancelled` — original subscription cancelled (if all items moved)
3. `invoice.created` — proration invoice for the new subscription period
4. `invoice.paid` — payment collected on the proration invoice

## Webhook sequence for scheduled changes

When a scheduled plan change executes at period end, the same webhook sequence as immediate changes applies at execution time.

## Identifying plan-change cancellations

When a subscription is cancelled due to a plan change (all items moved to new subscriptions), the `customer.subscription.cancelled` webhook includes:

* `cancellation_reason: "change_plan"`
* `cancellation_details: {"reason": "change_plan"}`

Use these fields to distinguish plan-change cancellations from customer-initiated cancellations.

<Warning>
  **Delivery order is NOT guaranteed.** Webhooks may arrive in any order. Design your integration to handle events idempotently — do not assume `subscription.created` arrives before `subscription.cancelled`.
</Warning>

## Lineage tracking with split\_from\_subscription\_id

New subscriptions created by a plan change include lineage metadata that links them back to the original subscription. In webhook payloads, this value is located at `data.object.metadata.split_from_subscription_id`:

```json
{
  "type": "customer.subscription.created",
  "data": {
    "object": {
      "id": "sub_xyz789",
      "state": "active",
      "metadata": {
        "split_from_subscription_id": "sub_abc123"
      }
    }
  }
}
```

Use `data.object.metadata.split_from_subscription_id` from the webhook payload to trace which original subscription a new subscription was split from. The same `metadata.split_from_subscription_id` field is also available in API responses when retrieving the subscription directly.