> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.paymentkit.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.paymentkit.com/_mcp/server.

# Plan change events

When you change a subscription's plan — swapping a price, moving to a different billing interval, or changing it during a trial — PaymentKit emits a set of webhook events. Which events fire depends on three things:

* **Whether the billing interval or contract terms change.** A change that keeps the same terms is applied in place; a change to a different interval, interval count, contract length, or auto-renew setting **splits** the subscription (the original is cancelled and a new one is created).
* **Whether the subscription still has trial time remaining.**
* **The net proration amount** across the whole change (an "upgrade" nets to a charge, a "downgrade" nets to a credit).

There is no single "plan changed" event that fires for every case. This page lists exactly what fires for each one.

> **Warning**
>
> **Do not detect plan changes from `invoice.paid`.** A downgrade nets to a credit and produces a [`credit_note.created`](#the-event-matrix) event, not a payment — so `invoice.paid` never fires. `invoice.paid` also fires for ordinary renewals, so it cannot tell a plan change apart from a normal billing cycle. Detect plan changes from the subscription and credit-note events below, then read the new price from the subscription's `items`.

# Detecting a plan change

> **Tip**
>
> Treat any of these events as "this subscription's plan may have changed — re-check it," then read the current price from the subscription's `items` in the payload (`data.object.items`) or by re-fetching the subscription:
>
> * `customer.subscription.plan_changed` — a plan change while trialing (see [In-trial changes](#in-trial-changes))
> * `customer.subscription.created` and `customer.subscription.cancelled` — a change that crossed billing intervals (a [split](#cross-interval-changes))
> * `customer.subscription.updated` — a scheduled change was stored, or items were modified
> * `credit_note.created` — a downgrade issued a credit
>
> Use `invoice.paid` only to confirm that a charge was collected, and `credit_note.created` for credits.

Two cases need care:

* **In-trial changes** emit a single authoritative event, [`customer.subscription.plan_changed`](#in-trial-changes), which **replaces** `customer.subscription.updated` for that change.
* **Same-interval changes** keep the same subscription and emit `customer.subscription.updated` on it (alongside the billing events), so you detect them exactly like any other update — read the new price from the subscription's `items`. The only exception is a change that removes the subscription's last item: that cancels it and emits `customer.subscription.cancelled` instead.

# The event matrix

The tables below list the events each kind of change emits, beyond the baseline `product`/`price`/`customer` events for any objects you created. "Upgrade" and "downgrade" mean the sign of the **net proration across the whole change**, not the direction of a single item. How the new charge is calculated depends on whether the subscription splits: a [same-interval change](#same-billing-interval-and-terms) prorates the new charge for the time left in the current period, while a [cross-interval change](#cross-interval-changes) starts a fresh billing period on the new subscription and charges its **full** price, crediting the original subscription for its unused time.

## Immediate changes

For immediate changes (`effective_at: "immediate"`, the default), PaymentKit always creates and collects a proration invoice — see [proration and timing](#proration-and-timing-options).

### Same billing interval and terms

The plan is changed on the same subscription; no subscription is created or cancelled.

| Net result                 | Events emitted                                                                                                        |
| -------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| **Upgrade** (net charge)   | `invoice.created`, `invoice.finalized`, `invoice.paid`, `customer.subscription.updated`                               |
| **Downgrade** (net credit) | `credit_note.created`, `customer.updated`, `invoice.created`, `customer.subscription.updated` — **no `invoice.paid`** |
| **Zero net**               | `customer.subscription.updated` — the items change going forward with no invoice or credit note                       |

`customer.subscription.updated` fires on the subscription for every same-interval change that leaves it active, and is the last event of the change — detect the plan change from it and read the new price from the subscription's `items`. (A change made while trialing emits [`customer.subscription.plan_changed`](#in-trial-changes) instead, and a change that removes the subscription's last item cancels it — see [cross-interval changes](#cross-interval-changes) for the split case.)

### Cross-interval changes

A change to a different billing interval, interval count, contract length, or auto-renew setting **splits** the subscription: the original is cancelled (when all its items move) and one or more new subscriptions are created.

| Net result                 | Events emitted                                                                                                                                           |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Upgrade** (net charge)   | `customer.subscription.created`, `customer.subscription.cancelled`, `invoice.created`, `invoice.finalized`, `invoice.paid`, `invoice.voided`             |
| **Downgrade** (net credit) | `customer.subscription.created`, `customer.subscription.cancelled`, `credit_note.created`, `customer.updated`, `invoice.created` — **no `invoice.paid`** |

`invoice.voided` covers any pending renewal invoices voided when the original subscription is cancelled. The new subscription carries `metadata.split_from_subscription_id` linking it to the original — see [lineage tracking](/guides/billing/subscriptions/change-subscription-plan#lineage-tracking-with-split_from_subscription_id).

## In-trial changes

When the subscription still has trial remaining (its state is `trialing` and `trial_end` is in the future), the plan is changed **in place** regardless of interval — the subscription keeps its identity and its trial, and its plan is swapped. Nothing is billed at change time, because nothing has been paid yet.

| Event emitted                             |
| ----------------------------------------- |
| `customer.subscription.plan_changed` only |

No invoice, credit note, `customer.subscription.created`, `customer.subscription.cancelled`, or `customer.subscription.updated` is emitted. (If the subscription has an open setup invoice from the start of its trial, re-anchoring its period can additionally emit an `invoice.updated`.) If the trial is ended immediately (`trial_behavior: "end_now"`), the first real period is billed by the normal activation path **after** the change, which then emits the usual `invoice.created` / `invoice.paid`.

## Scheduled changes

With `effective_at: "period_end"`, the change is stored on the subscription and executed at the next renewal.

| When                   | Events emitted                                                                     |
| ---------------------- | ---------------------------------------------------------------------------------- |
| At scheduling time     | `customer.subscription.updated` (the pending change is stored on the subscription) |
| At execution (renewal) | The same events as the equivalent [immediate change](#immediate-changes)           |

> **Warning**
>
> **Delivery order is not guaranteed.** Webhooks can arrive in any order. Handle events idempotently — do not assume, for example, that `customer.subscription.created` arrives before `customer.subscription.cancelled`, or that `invoice.created` arrives before `credit_note.created`.

# `customer.subscription.plan_changed`

`customer.subscription.plan_changed` fires when a plan change is applied to a subscription that **still has trial remaining** (state `trialing` with a future `trial_end`). The subscription keeps its identity, its plan is swapped in place, and — because this event is emitted — the generic `customer.subscription.updated` is **suppressed** for that change. Treat `plan_changed` as the trial-time equivalent of `updated`.

It only fires on this in-place trial path. A non-trial plan change never emits it — you receive the [cross-interval](#cross-interval-changes) or [same-interval](#same-billing-interval-and-terms) events instead.

## Webhook payload

The event follows the standard [webhook envelope](/guides/integration/webhooks#webhook-payload). `data.object` is the updated subscription (with the new `billing_interval`, `billing_interval_count`, `items`, and `trial_end`), and `data.previous_attributes` holds the subscription's previous state, so you can see what the interval changed from.

```json
{
  "id": "evt_prod_a1b2c3d4e5f6g7h8",
  "object": "event",
  "type": "customer.subscription.plan_changed",
  "created": 1704067200,
  "livemode": true,
  "data": {
    "object": {
      "id": "sub_prod_a1b2c3d4e5f6g7h8",
      "state": "trialing",
      "billing_interval": "year",
      "billing_interval_count": 1,
      "trial_end": "2026-02-01T00:00:00Z",
      "items": [
        {
          "id": "si_prod_1a2b3c4d5e6f7g8h",
          "price_id": "price_prod_annual_plan",
          "quantity": 1
        }
      ]
    },
    "previous_attributes": {
      "billing_interval": "month",
      "billing_interval_count": 1
    }
  }
}
```

> **Note**
>
> The `strategy` and `previous_billing_interval` details below are carried on the **stored event's `metadata`** (readable through the [Events API](/guides/integration/events)), **not** in the webhook wire payload. Over webhooks, read the new interval from `data.object.billing_interval` and the previous interval from `data.previous_attributes.billing_interval`.

## Event metadata (Events API)

Fetched through the [Events API](/guides/integration/events), the event's `metadata` carries the plan-change specifics:

| Field                             | Description                                                                                                 |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `strategy`                        | How the trial was handled: `in_place_preserve` (trial kept) or `in_place_end_now` (trial ended immediately) |
| `in_place`                        | Always `true` — the change was applied on the same subscription                                             |
| `previous_billing_interval`       | Billing interval before the change (e.g. `month`)                                                           |
| `previous_billing_interval_count` | Billing interval count before the change                                                                    |
| `billing_interval`                | Billing interval after the change (e.g. `year`)                                                             |
| `billing_interval_count`          | Billing interval count after the change                                                                     |
| `trial_end`                       | The subscription's trial end (ISO 8601), or `null`                                                          |

```json
{
  "id": "evt_prod_a1b2c3d4e5f6g7h8",
  "type": "customer.subscription.plan_changed",
  "aggregate_type": "subscription",
  "aggregate_id": "sub_prod_a1b2c3d4e5f6g7h8",
  "data": { "...": "the updated subscription" },
  "previous_data": { "...": "the subscription's previous state" },
  "metadata": {
    "strategy": "in_place_preserve",
    "in_place": true,
    "previous_billing_interval": "month",
    "previous_billing_interval_count": 1,
    "billing_interval": "year",
    "billing_interval_count": 1,
    "trial_end": "2026-02-01T00:00:00Z"
  },
  "created": "2026-01-01T00:00:00Z"
}
```

## Strategy values

| `strategy`          | Meaning                                                                                                                            |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `in_place_preserve` | The trial's original end date is kept. The first charge is at trial end, at the new price.                                         |
| `in_place_end_now`  | The trial is ended immediately and the first period is billed at the new price straight away (through the normal activation path). |

# Sample payloads

All examples use the Stripe-compatible [webhook envelope](/guides/integration/webhooks#webhook-payload). Amount fields are in atomic units (for example, cents for USD) with the `_atom` suffix.

## Downgrade: `credit_note.created`

A downgrade issues a credit to the customer's balance. `credit_note.created` fires in both directions, so read `amount_atom` and `reason` rather than the event name — see [Credit notes](/guides/billing/credit-notes-credit-balance).

```json
{
  "id": "evt_prod_b2c3d4e5f6g7h8i9",
  "object": "event",
  "type": "credit_note.created",
  "created": 1704067200,
  "livemode": true,
  "data": {
    "object": {
      "id": "cn_prod_a1b2c3d4e5f6g7h8",
      "customer_id": "cus_prod_a1b2c3d4e5f6g7h8",
      "amount_atom": 2500,
      "currency": "usd",
      "type": "issued",
      "reason": "proration_excess"
    }
  }
}
```

## Downgrade: `customer.updated`

The customer's credit balance changes when the credit is applied, so a `customer.updated` event is emitted.

```json
{
  "id": "evt_prod_c3d4e5f6g7h8i9j0",
  "object": "event",
  "type": "customer.updated",
  "created": 1704067200,
  "livemode": true,
  "data": {
    "object": {
      "id": "cus_prod_a1b2c3d4e5f6g7h8",
      "credit_balance": { "usd": 2500 }
    },
    "previous_attributes": {
      "credit_balance": { "usd": 0 }
    }
  }
}
```

## Downgrade: `invoice.created`

A downgrade also creates a companion invoice that documents the credit note. It is born paid (`billing_reason: "credit_note"`, `status: "paid"`) and is **not** a collectible charge — it never emits `invoice.paid`.

```json
{
  "id": "evt_prod_d4e5f6g7h8i9j0k1",
  "object": "event",
  "type": "invoice.created",
  "created": 1704067200,
  "livemode": true,
  "data": {
    "object": {
      "id": "in_prod_a1b2c3d4e5f6g7h8",
      "status": "paid",
      "billing_reason": "credit_note",
      "currency": "usd",
      "total_amount_atom": 2500,
      "due_amount_atom": 0,
      "paid_amount_atom": 2500
    }
  }
}
```

## Upgrade: `invoice.paid`

An upgrade collects the prorated charge, which emits `invoice.paid` on success.

```json
{
  "id": "evt_prod_e5f6g7h8i9j0k1l2",
  "object": "event",
  "type": "invoice.paid",
  "created": 1704067200,
  "livemode": true,
  "data": {
    "object": {
      "id": "in_prod_a1b2c3d4e5f6g7h8",
      "status": "paid",
      "billing_reason": "subscription_update",
      "currency": "usd",
      "total_amount_atom": 5000,
      "due_amount_atom": 0,
      "paid_amount_atom": 5000
    },
    "previous_attributes": {
      "status": "open",
      "due_amount_atom": 5000,
      "paid_amount_atom": 0
    }
  }
}
```

# Proration and timing options

Both the [change-plan](/guides/billing/subscriptions/change-subscription-plan) and [change requests](/guides/billing/subscriptions/subscription-change-requests) endpoints control proration and timing with two options.

## `proration_behavior`

| Value               | Effect                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `always_invoice`    | Create a proration invoice immediately and collect it now (an upgrade), or issue a credit note (a downgrade).                                                                                                                                                                                                                                                                                                                                                                                                            |
| `create_prorations` | Create floating proration line items (`invoice_id` is null) that are billed on the next renewal invoice. No immediate invoice or charge.                                                                                                                                                                                                                                                                                                                                                                                 |
| `none`              | Make no billing adjustment for the mid-cycle change. The items change going forward.                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `charge_full`       | Charge the **full** new-plan price immediately with no proration and no credit, and reset the billing cycle to now. **A `charge_full` change never nets to a credit — even a price *decrease* collects the full new amount, so it emits the charge-shaped events (`invoice.paid`), never `credit_note.created`.** Detect it the same way as an upgrade: a same-interval change emits `customer.subscription.updated`, a cross-interval change emits `customer.subscription.created` + `customer.subscription.cancelled`. |

## `effective_at`

| Value                 | Effect                                                                  |
| --------------------- | ----------------------------------------------------------------------- |
| `immediate` (default) | Apply the change now.                                                   |
| `period_end`          | Store the change and apply it at the end of the current billing period. |

> **Note**
>
> An **immediate** change always uses `always_invoice` — `create_prorations` and `none` are rejected with `effective_at: "immediate"`. Those two behaviors only apply when the change is deferred with `effective_at: "period_end"`, where they take effect at renewal.

> **Warning**
>
> **There is no option to charge the full new-plan price immediately with no proration credit.** How much is collected depends on the change:
>
> * A **same-interval** change prorates the new charge for the time left in the current period, and credits the old price's unused time.
> * A **cross-interval** change charges the new subscription's full price for a fresh period, and credits the original subscription for its unused time — so the amount collected is the full new price minus that credit, not a prorated fraction.
> * Deferring with `effective_at: "period_end"` makes no mid-cycle adjustment: the full new price is billed at the next renewal.
>
> If you need to bill the full new price immediately with no offsetting credit for the customer's remaining time, that is not supported — use one of the options above.

# Related

* [Webhooks](/guides/integration/webhooks) — the webhook envelope, signing, and the full event-type list
* [Events](/guides/integration/events) — the Events API, where event `metadata` is readable
* [Change subscription plan](/guides/billing/subscriptions/change-subscription-plan) — the change-plan endpoint
* [Subscription change requests](/guides/billing/subscriptions/subscription-change-requests) — the recommended multi-step workflow
* [Credit notes & credit balance](/guides/billing/credit-notes-credit-balance) — how downgrade credits work