Change subscription plan

Move items to different billing intervals or contract terms with automatic subscription splitting, proration handling, and support for immediate or scheduled execution.
View as Markdown

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.

$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"
>}'

For changes within the same billing interval (quantity updates, add-ons, price swaps), use the update subscription items endpoint instead.

Request fields

FieldTypeRequiredDescription
itemsarrayYesList of item changes (min 1 item required)
proration_behaviorstringYesHow to handle proration: always_invoice, create_prorations, or none
effective_atstringNoWhen 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.
reasonstringNoReason for the plan change (default: "change_plan")
pay_before_changebooleanNoIf 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.
metadataobjectNoAdditional 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:

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:

ActionRequired fieldsForbidden fieldsDescription
addnew_price_idsubscription_item_idAdd a new item to the subscription. quantity is optional and defaults to 1 if not specified.
updatesubscription_item_id, new_price_id-Change an existing item’s price and/or quantity. quantity is optional; if omitted, keeps existing quantity.
deletesubscription_item_idnew_price_idRemove an item from the subscription.

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

Response fields

FieldTypeDescription
original_subscription_idstringID of the original subscription
original_cancelledbooleanTrue if original was cancelled (all items moved/deleted)
original_items_remainingintegerNumber of active items still on original subscription
original_subscription_updated_atdatetimeLast update timestamp of the original subscription
created_subscriptionsarrayNew subscriptions created (one per unique billing terms)
items_addedintegerNumber of new items created via ADD action
proration_credit_atomintegerCredit for unused time on moved/deleted items (negative, in atomic units)
proration_charge_atomintegerCharge for new subscription periods (positive, in atomic units)
net_amount_atomintegerNet proration amount (credit + charge, in atomic units)
invoice_idstringID of proration invoice (if always_invoice behavior)
payment_statusstringPayment status: paid, processing, failed, requires_action, no_payment_method, or pending
voided_invoice_idsarrayIDs of voided pending renewal invoices

Created subscription fields

Each subscription in created_subscriptions includes:

FieldTypeDescription
subscription_idstringID of the new subscription
statestringState of the new subscription (active, incomplete). Incomplete means payment failed or requires action (e.g., 3D Secure).
billing_intervalstringBilling interval (month, year, etc)
billing_interval_countintegerInterval count (1, 3, etc)
items_countintegerNumber of items on this subscription
total_billing_cyclesintegerContract length in billing cycles (null = indefinite)
contract_auto_renewbooleanWhether contract auto-renews at end of term

Examples

Monthly to annual upgrade

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

$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"
>}'

The response includes details about the plan change:

1{
2 "original_subscription_id": "sub_abc123",
3 "original_cancelled": true,
4 "original_items_remaining": 0,
5 "original_subscription_updated_at": "2026-03-10T15:30:00Z",
6 "created_subscriptions": [
7 {
8 "subscription_id": "sub_xyz789",
9 "state": "active",
10 "billing_interval": "year",
11 "billing_interval_count": 1,
12 "items_count": 1,
13 "total_billing_cycles": null,
14 "contract_auto_renew": false
15 }
16 ],
17 "items_added": 0,
18 "proration_credit_atom": -5000,
19 "proration_charge_atom": 100000,
20 "net_amount_atom": 95000,
21 "invoice_id": "in_change123",
22 "payment_status": "paid",
23 "voided_invoice_ids": []
24}

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

Add items during plan change

Add a new item while upgrading to a different plan:

$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:

$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:

$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

Require payment before applying the plan change. This ensures the customer stays on their original plan if payment fails:

$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

pay_before_change: true requires proration_behavior: "always_invoice". Without an invoice, there is nothing to collect payment on.

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.

$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:

1{
2 "original_subscription_id": "sub_abc123",
3 "original_cancelled": false,
4 "original_items_remaining": 1,
5 "original_subscription_updated_at": "2026-03-10T12:00:00Z",
6 "created_subscriptions": [],
7 "items_added": 0,
8 "proration_credit_atom": 0,
9 "proration_charge_atom": 0,
10 "net_amount_atom": 0,
11 "invoice_id": null,
12 "payment_status": null,
13 "voided_invoice_ids": [],
14 "effective_at": "period_end",
15 "scheduled_for": "2026-04-10T00:00:00Z",
16 "pending_change_id": "ppc_live_a1b2c3d4e5f6g7h8"
17}

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

FieldDescription
effective_at"period_end" — confirms the change is deferred
scheduled_forTimestamp when the change will execute (the subscription’s current_period_end)
pending_change_idUnique 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:

$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:

1{
2 "status": "cancelled",
3 "subscription_id": "sub_abc123",
4 "previous_pending_change": {
5 "id": "ppc_live_a1b2c3d4e5f6g7h8",
6 "created_at": "2026-03-10T12:00:00Z",
7 "scheduled_for": "2026-04-10T00:00:00Z",
8 "items": [
9 {"action": "update", "subscription_item_id": "123", "new_price_id": "456", "quantity": null}
10 ],
11 "reason": "change_plan",
12 "proration_behavior": "always_invoice",
13 "metadata": null
14 }
15}

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.

Response when no pending change exists:

1{
2 "status": "not_found",
3 "subscription_id": "sub_abc123",
4 "previous_pending_change": null
5}

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.

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.

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:

1{
2 "type": "customer.subscription.created",
3 "data": {
4 "object": {
5 "id": "sub_xyz789",
6 "state": "active",
7 "metadata": {
8 "split_from_subscription_id": "sub_abc123"
9 }
10 }
11 }
12}

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.