***
title: Update a subscription
subtitle: >-
Update subscription-level settings, manage items (add/remove/modify), adjust
quantities and prices, with control over proration timing and behavior for
mid-cycle changes.
------------------
# Update subscription fields
Update metadata, payment method, trial period, or discount on an active subscription without changing line items.
```bash
curl -X PATCH https://api.paymentkit.com/api/{account_id}/subscriptions/{subscription_id} \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"description": "Enterprise plan - annual",
"default_payment_method_id": "pm_newcard456"
}'
```
```python
subscription = client.subscriptions.update(
account_id="acc_abc123",
subscription_id="sub_abc123",
description="Enterprise plan - annual",
default_payment_method_id="pm_newcard456"
)
```
## Updatable fields
| Field | Description |
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `description` | Free-text description of the subscription. |
| `default_payment_method_id` | Payment method used for future collections. Must belong to the subscription's customer. |
| `cancel_at_period_end` | Set to `true` to cancel at the end of the current period instead of renewing. |
| `trial_end` | Extend or shorten the trial period. Mutually exclusive with `trial_period_days`. **Only valid when subscription is in TRIALING state.** |
| `trial_period_days` | Set trial length in days from now. Mutually exclusive with `trial_end`. **Only valid when subscription is in TRIALING state.** |
| `coupon_id` | Attach a coupon. Pass `""` to remove an existing coupon. Mutually exclusive with `promotion_code`. |
| `promotion_code` | Apply a promotion code. Mutually exclusive with `coupon_id`. |
| `collection_method` | Set to `charge_automatically` or `send_invoice`. Requires a payment method when set to `charge_automatically`. |
| `net_d` | Payment terms in days (Net X). Must be 0 or greater. Set to `0` for due immediately. |
| `metadata` | Key-value pairs. Replaces existing metadata entirely. Pass an empty object `{}` to clear all metadata. |
**Lifecycle impact:** Changing `cancel_at_period_end`, `trial_end`, or `trial_period_days` triggers a re-evaluation of the subscription lifecycle and may reschedule automated actions.
**Invoice impact:** Changing `coupon_id` or `promotion_code` voids any pending renewal invoices to ensure correct pricing on the next billing cycle.
# Update subscription items
Add, remove, or change items on a subscription. This is how you handle quantity changes, add-ons, and price swaps within the same billing interval.
```bash
curl -X PATCH https://api.paymentkit.com/api/{account_id}/subscriptions/{subscription_id}/items \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"items": [
{"id": "si_existing123", "quantity": 10},
{"price_id": "price_addon_storage", "quantity": 1},
{"id": "si_old_addon", "deleted": true}
],
"proration_behavior": "always_invoice"
}'
```
## Item operations
| Operation | Payload | Notes |
| ------------------------------ | ---------------------------------------------------------------- | ------------------------------------------- |
| **Add new item** | `{"price_id": "price_xxx", "quantity": 2}` | Quantity defaults to 1 if omitted |
| **Add new item at period end** | `{"price_id": "price_xxx", "quantity": 2, "start_at_end": true}` | Created inactive, activates at next renewal |
| **Update quantity** | `{"id": "si_xxx", "quantity": 10}` | |
| **Swap price** | `{"id": "si_xxx", "price_id": "price_yyy"}` | Keeps existing quantity if not specified |
| **Update price and quantity** | `{"id": "si_xxx", "price_id": "price_yyy", "quantity": 5}` | |
| **Remove item now** | `{"id": "si_xxx", "deleted": true}` | |
| **Schedule removal** | `{"id": "si_xxx", "drop_at_end": true}` | Item removed at next renewal |
| **Cancel scheduled removal** | `{"id": "si_xxx", "drop_at_end": false}` | |
Combine multiple operations in a single request. All changes are applied atomically.
## Proration behavior
The `proration_behavior` field is **required** and controls how mid-cycle changes are billed:
* **`always_invoice`**: Creates an invoice immediately and attempts payment. Use when you want the customer to pay for the upgrade right away.
* **`create_prorations`**: Creates floating items (invoice\_id=NULL) that are collected on the next renewal invoice. 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.
## Response fields
The response includes:
* `subscription_id`: The updated subscription's external ID
* `invoice_id`: Created invoice ID (if `proration_behavior=always_invoice`)
* `payment_status`: Payment result (`"paid"`, `"processing"`, `"requires_action"`, `"failed"`, `"no_payment_method"`, `"skipped"`)
* `payment_error`: Error message if payment failed
* `floating_items_created`: Count of floating items (if `proration_behavior=create_prorations`)
* `proration_amount_atom`: Net proration amount in atoms
* `voided_invoice_ids`: External IDs of voided pending renewal invoices
* `new_renewal_invoice_id`: External ID of replacement renewal invoice (if a voided invoice had active dunning)
* `new_invoice_payment_status`: Payment status of the replacement renewal invoice
Items must have the same billing interval as the subscription. To change a customer from monthly to annual billing, use the [change plan](#change-subscription-plan) endpoint instead.
# Proration behavior
When items change mid-cycle, PaymentKit calculates the prorated difference. Control how proration is handled with the `proration_behavior` field:
| Behavior | Description |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `always_invoice` | Create a proration invoice immediately and attempt payment. Use this for instant upgrades where the customer should be charged right away. |
| `create_prorations` | Create prorated line items that are included on the next renewal invoice. |
| `none` | No proration. Changes take effect at the next renewal with no credit or charge for the remainder of the current period. |
## Example: Immediate upgrade
Upgrade a customer from a basic plan to a pro plan and charge the prorated difference immediately:
```bash
curl -X PATCH https://api.paymentkit.com/api/{account_id}/subscriptions/{subscription_id}/items \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"items": [
{"id": "si_basic_plan", "price_id": "price_pro_plan"}
],
"proration_behavior": "always_invoice"
}'
```
The response includes the created invoice ID and payment status:
```json
{
"subscription_id": "sub_abc123",
"invoice_id": "in_proration456",
"payment_status": "paid",
"proration_amount_atom": 1500,
"voided_invoice_ids": [],
"new_renewal_invoice_id": null,
"new_invoice_payment_status": null
}
```
**Payment status values:**
* `paid`: Payment succeeded
* `processing`: Async payment (ACH/SEPA) submitted, awaiting confirmation
* `requires_action`: Customer action required (e.g., 3D Secure authentication)
* `failed`: Payment failed
* `no_payment_method`: No payment method available
* `skipped`: Payment processing disabled for this account
If payment fails, the `payment_error` field contains the error message.
**Invoice voiding:** When immediate updates are made, any pending renewal invoices (DRAFT, OPEN, or PAST\_DUE) are automatically voided to ensure correct billing. The `voided_invoice_ids` field contains a list of the voided invoices. If the voided invoice had active dunning, a new renewal invoice is created with updated items, and its ID is returned in `new_renewal_invoice_id`.
## Example: Deferred billing
Add an add-on that will be billed on the next renewal:
```bash
curl -X PATCH https://api.paymentkit.com/api/{account_id}/subscriptions/{subscription_id}/items \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"items": [
{"price_id": "price_addon_seats", "quantity": 3}
],
"proration_behavior": "create_prorations"
}'
```
The response includes the count of floating items created:
```json
{
"subscription_id": "sub_abc123",
"invoice_id": null,
"payment_status": null,
"floating_items_created": 1,
"proration_amount_atom": 4500,
"voided_invoice_ids": [],
"new_renewal_invoice_id": null,
"new_invoice_payment_status": null
}
```
Prorated line items are stored as floating items (with `invoice_id` set to null) and automatically included in the next renewal invoice.
# Change subscription plan
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"
}'
```
## 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` |
| `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 operation rolls back. If false, the plan change commits regardless of payment status, and new subscriptions may be left in 'incomplete' state. |
| `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, changes apply at next renewal only. Items are updated but no proration charges/credits are created.
## 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
Items A, B, C] --> B{Change Plan}
B --> C[Monthly Subscription
Item C unchanged]
B --> D[Annual Subscription
Item A 12-month contract]
B --> E[Annual Subscription
Item B no contract]
```
Items are grouped by:
* Billing interval (month, year, etc.)
* Billing interval count (every 1 month, every 3 months, etc.)
* Contract length (12 cycles, indefinite, etc.)
* Auto-renew setting
## 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.
## Example: Monthly to annual upgrade
Upgrade a customer from monthly ($100/mo) to annual ($1,000/yr) billing with immediate payment:
```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"
}'
```
```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"
)
```
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": []
}
```
### 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 |
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
## Example: 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
}'
```
## Example: 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.
## Example: 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.
## Example: Pay-before-change flow
Require payment before applying the plan change. This ensures the customer stays on their original plan if payment fails:
```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
`pay_before_change: true` requires `proration_behavior: "always_invoice"`. Without an invoice, there is nothing to collect payment on.
# Pending invoice handling
When you modify a subscription, PaymentKit automatically voids any pending renewal invoices to ensure the next invoice reflects the updated configuration.
**What gets voided:**
* Only renewal invoices (`billing_reason: "subscription_cycle"`)
* With status `draft`, `open`, or `past_due`
* Proration invoices (`billing_reason: "subscription_update"`) are never voided
The response includes the voided invoice IDs:
```json
{
"subscription_id": "sub_abc123",
"voided_invoice_ids": ["in_pending456"]
}
```
**Handling active dunning:**
If a voided invoice has active dunning (failed payment with scheduled retries), PaymentKit uses a two-phase approach to ensure the replacement invoice has the correct subscription items:
1. **Phase 1 - Void old invoice:**
* Marks the old invoice as `void`
* Marks the dunning state as `invoice_voided`
2. **Phase 2 - Create replacement invoice (after item changes are applied):**
* Creates a new renewal invoice with the updated subscription items
* Finalizes the invoice to `open` status
* The dunning system automatically picks up the new invoice for collection
This ensures customers are always billed the correct amount for their current subscription configuration, even when dunning is in progress.
# Preview upcoming invoice
Before making changes, preview what the next invoice will look like:
```bash
curl -X GET https://api.paymentkit.com/api/{account_id}/subscriptions/{subscription_id}/preview \
-H "Authorization: Bearer sk_live_..."
```
This is a **read-only** operation that computes what the next renewal invoice will be without creating anything in the database. No invoice numbers are claimed, no billing cycles are decremented, and no events are emitted.
The response includes:
* **subscription**: Complete subscription details including all items and pricing
* **upcoming\_invoice**: Preview of the next billing cycle invoice with:
* Line items (descriptions, quantities, amounts, period dates)
* Amount breakdowns (subtotal, tax, total, due amount)
* Due date (calculated from `net_d` payment terms)
* Billing period (period\_start, period\_end)
* Currency and collection method
**Example response:**
```json
{
"subscription": {
"id": "sub_abc123",
"customer_id": "cus_xyz789",
"state": "active",
"currency": "usd",
"current_period_start": "2026-02-10T00:00:00Z",
"current_period_end": "2026-03-10T00:00:00Z",
"items": [
{
"id": "si_item1",
"price_id": "prc_monthly_plan",
"quantity": 1
}
]
},
"upcoming_invoice": {
"id": "preview",
"customer_id": "cus_xyz789",
"status": "draft",
"currency": "usd",
"subtotal_amount_atom": 2000,
"tax_amount_atom": 0,
"total_amount_atom": 2000,
"due_amount_atom": 2000,
"paid_amount_atom": 0,
"remaining_amount_atom": 2000,
"period_start": "2026-03-10T00:00:00Z",
"period_end": "2026-04-10T00:00:00Z",
"due_date": "2026-04-10T00:00:00Z",
"billing_reason": "subscription_cycle",
"items": [
{
"id": "preview",
"description": "Monthly Plan",
"quantity": 1,
"amount": 2000,
"period_start": "2026-03-10T00:00:00Z",
"period_end": "2026-04-10T00:00:00Z"
}
]
}
}
```