> 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/update-a-subscription/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.paymentkit.com/_mcp/server. # Update a subscription # Update subscription fields Update metadata, payment method, trial period, or discount on an active subscription without changing line items. #### API ```bash curl -X PATCH https://app.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 SDK ```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. | > **Tip** > > **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. ## Field clearing on lifecycle actions When subscriptions transition between states, certain fields are automatically cleared to maintain data consistency. Understanding this behavior is important when building integrations that depend on these fields. | Action | Fields Cleared | Fields Preserved | | --------------------------------------------- | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | | **Pause** | `cancel_at_period_end`, `cancel_at`, `cancellation_refund_option` | — | | **Resume** | `paused_at`, `resumes_at`, `pause_at_end`, `pause_for_cycles` | — | | **Cancel** | `paused_at`, `resumes_at`, `pause_at_end`, `pause_for_cycles`, `cancel_at_period_end` | — | | **Activate** (from paused/past\_due/trialing) | `paused_at`, `resumes_at`, `pause_at_end`, `pause_for_cycles` | `cancel_at_period_end`, `cancel_at`, `cancellation_refund_option` | > **Info** > > **Why activate preserves scheduled cancellation:** A customer may schedule a cancellation during their trial or while recovering from a failed payment. When the subscription activates (trial converts or payment succeeds), the scheduled cancellation should persist—it's a deliberate user action that shouldn't be silently discarded. > > **Why pause clears scheduled cancellation:** Pausing supersedes any scheduled cancellation. The customer explicitly chose to pause instead, and can re-schedule cancellation after resuming if desired. This prevents edge cases where a `cancel_at` date passes while paused, causing unexpected immediate cancellation upon resume. # 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://app.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. You can optionally specify `proration_date` to set a custom date for proration calculation (defaults to now). ## Response fields The response includes: * `subscription_id`: The updated subscription's external ID * `invoice_id`: Created invoice ID (if `proration_behavior=always_invoice` with upgrade) * `credit_note_id`: Created credit note ID (if `proration_behavior=always_invoice` with downgrade) * `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 > **Warning** > > Items must have the same billing interval as the subscription. To change a customer from monthly to annual billing, use the [subscription change requests](#change-subscription-plan) workflow instead (recommended) or the legacy [change plan](#change-subscription-plan) endpoint (deprecated). ## Concurrent modification If another operation is modifying one of this subscription's invoices when your request arrives — most often a concurrent plan change or item update, and briefly at the start of a payment collection — the request is refused with `409` and `error_code` `invoice_locked`. **Nothing is modified**: no items change, no invoice is voided, and no partial state is left behind. ```json { "title": "Invoice Locked", "status": 409, "detail": "Cannot change subscription sub_abc123 right now: invoice in_xyz789 is being modified by another operation. Retry in a few seconds.", "error_code": "invoice_locked", "invoice_id": "in_xyz789", "subscription_id": "sub_abc123", "retryable": true } ``` Retry the identical request after a few seconds. > **Note** > > Only changes that re-bill can hit this: item changes, plan changes, and adding or > removing a coupon on `PATCH /subscriptions/{subscription_id}`. Requests that never > touch a pending invoice — updating description or metadata, or scheduling a change for > period end — are not affected. See the Retryable errors section of the [API reference](/api-reference). # 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 charges or credits are created. Items are updated immediately but no billing adjustment is made for the mid-cycle change. | ## 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://app.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": "inv_proration456", "payment_status": "paid", "payment_error": null, "floating_items_created": 0, "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/BACS) submitted, awaiting webhook 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 **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`. ### Payment error responses When payment fails with `proration_behavior: "always_invoice"`, the update uses the **Charge First pattern** — payment is attempted **before** applying any item changes. If payment fails, the API returns a **402 Payment Required** error and the subscription remains unchanged: **Card declined (402 response):** ```json { "error": "Payment failed for subscription update", "error_code": "PAYMENT_FAILED", "payment_status": "failed", "payment_error": "Your card was declined. Please try a different payment method.", "orchestrator_summary": "Card declined by issuer (insufficient_funds)" } ``` The subscription items are **NOT updated**. The customer must update their payment method and retry the request. **No payment method (402 response):** ```json { "error": "Cannot charge subscription without a payment method. Please add a payment method before making changes.", "error_code": "PAYMENT_FAILED", "payment_status": "no_payment_method", "payment_error": "no_payment_method" } ``` > **Info** > > **Charge First guarantees:** With `proration_behavior: "always_invoice"`, payment is collected BEFORE any subscription changes are applied. If payment fails: > > * ✅ Subscription items remain unchanged > * ✅ Original subscription state preserved > * ✅ No `incomplete` subscriptions created > * ✅ Safe to retry without double-charging > > This is the same Charge First pattern used by the [subscription change requests](/guides/billing/subscriptions/subscription-change-requests) workflow. **3D Secure handling:** If payment requires 3D Secure authentication, the MIT orchestrator will attempt to handle it automatically. If the orchestrator returns `requires_action`, you'll need to collect the payment separately using the invoice collection endpoint. ## Example: Deferred billing Add an add-on that will be billed on the next renewal: ```bash curl -X PATCH https://app.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, "payment_error": 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 To move items to different billing intervals or contract terms (e.g., monthly to annual), use the **subscription change requests** workflow: #### [Subscription change requests (recommended)](/guides/billing/subscriptions/subscription-change-requests) Multi-step workflow with preview, payment control, and retry safety The [subscription change requests](/guides/billing/subscriptions/subscription-change-requests) workflow provides: * **Preview before applying** — show customers exact proration amounts before committing * **Charge First pattern** — guarantee payment succeeds before modifying the subscription * **Idempotent retry** — built-in retry safety prevents double-charging * **Status tracking** — DRAFT → READY → APPLIED state machine * **Cross-interval support** — handles monthly to annual and other billing term changes * **Term-based splitting** — items automatically group into new subscriptions by billing terms > **Note** > > Both **Update Items** (this endpoint) and **Change Requests** use the Charge First pattern with `proration_behavior: "always_invoice"`. The key difference is that Change Requests provide explicit preview and multi-step workflow for user confirmation. ## Legacy endpoint (deprecated) The [change subscription plan](/guides/billing/subscriptions/change-subscription-plan) endpoint is a single-call approach that is **planned for deprecation**. New integrations should use subscription change requests instead.
View legacy change plan endpoint details The change plan endpoint provides: * **Term-based subscription splitting** — items automatically group into new subscriptions by billing interval and contract terms * **Proration** — credits for unused time, charges for new periods * **Scheduled changes** — defer execution to period end with `effective_at: "period_end"` * **Pay-before-change** — require payment success before committing changes * **Lineage tracking** — trace new subscriptions back to the original See [change subscription plan documentation](/guides/billing/subscriptions/change-subscription-plan) for full details (deprecated).
# Pending invoice handling When you modify a subscription with immediate changes, 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 **When voiding occurs:** Pending invoices are only voided for immediate changes that affect current-period billing: * Adding items (without `start_at_end: true`) * Updating items (quantity or price changes) * Deleting items Scheduled changes do not trigger voiding: * Adding items with `start_at_end: true` * Scheduling item removal with `drop_at_end: true` 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://app.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" } ] } } ``` # Bulk update subscription items Update items across up to 100 subscriptions in a single request. Each subscription processes independently — one failure does not block others. ## Bulk update constraints * Items must have the same billing interval as each subscription. Cross-interval changes require the [subscription change requests](/guides/billing/subscriptions/subscription-change-requests) workflow. * Updates work on ACTIVE, TRIALING, PAST\_DUE, and PAUSED subscriptions. CANCELLED, INCOMPLETE, and SCHEDULED subscriptions return errors. * Proration is automatically skipped for PAUSED and TRIALING subscriptions since no active billing period exists. * Maximum **100 subscriptions** per request. ## Bulk update request #### API ```bash curl -X POST https://app.paymentkit.com/api/{account_id}/subscriptions/bulk-update-items \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "subscriptions": [ { "subscription_id": "sub_abc123", "items": [ {"id": "si_item1", "price_id": "price_pro_59"} ] }, { "subscription_id": "sub_def456", "items": [ {"id": "si_item2", "price_id": "price_pro_59"} ] } ], "proration_behavior": "create_prorations" }' ``` #### Python SDK ```python result = client.subscriptions.bulk_update_items( account_id="acc_xxx", subscriptions=[ { "subscription_id": "sub_abc123", "items": [{"id": "si_item1", "price_id": "price_pro_59"}] }, { "subscription_id": "sub_def456", "items": [{"id": "si_item2", "price_id": "price_pro_59"}] } ], proration_behavior="create_prorations" ) print(f"Updated: {result.total_succeeded}, Failed: {result.total_failed}") ``` Each subscription entry specifies which items to change. Item operations are the same as [single-subscription updates](#update-subscription-items): | Operation | Payload | | --------------------- | ------------------------------------------------- | | **Add item** | `{"price_id": "price_xxx", "quantity": 2}` | | **Update quantity** | `{"id": "si_xxx", "quantity": 10}` | | **Swap price** | `{"id": "si_xxx", "price_id": "price_yyy"}` | | **Remove item** | `{"id": "si_xxx", "deleted": true}` | | **Schedule removal** | `{"id": "si_xxx", "drop_at_end": true}` | | **Add at period end** | `{"price_id": "price_xxx", "start_at_end": true}` | The `proration_behavior` field controls how mid-cycle changes are billed. Defaults to `create_prorations` if not specified. See [proration behavior](#proration-behavior) for details on each option. > **Tip** > > PAUSED and TRIALING subscriptions skip proration automatically regardless of the behavior specified. Item changes apply immediately but billing adjustments wait until the subscription is actively billing. ## Bulk update response The response returns HTTP 200 even for partial failures. Check the `failed` list for errors: ```json { "succeeded": [ { "subscription_id": "sub_abc123", "proration_amount_atom": 1500, "invoice_id": "inv_xyz789", "floating_items_created": 0 } ], "failed": [ { "subscription_id": "sub_def456", "error_code": "invalid_state", "error_message": "Cannot update cancelled subscription" } ], "total_processed": 2, "total_succeeded": 1, "total_failed": 1, "total_proration_amount_atom": 1500, "invoices_created": 1, "floating_items_created": 0 } ``` ### Bulk response fields | Field | Description | | ----------------------------- | ------------------------------------------------------------------ | | `succeeded` | List of successfully updated subscriptions with proration details | | `failed` | List of failed subscriptions with error codes and messages | | `total_processed` | Total subscriptions in the request | | `total_succeeded` | Count of successful updates | | `total_failed` | Count of failed updates | | `total_proration_amount_atom` | Sum of proration amounts across all succeeded subscriptions | | `invoices_created` | Count of invoices created (for `always_invoice` behavior) | | `floating_items_created` | Count of floating items created (for `create_prorations` behavior) | ### Bulk error codes | Code | Description | | ------------------------ | --------------------------------------------------------- | | `subscription_not_found` | Subscription does not exist or belongs to another account | | `invalid_state` | Subscription in CANCELLED, INCOMPLETE, or SCHEDULED state | | `interval_mismatch` | Price interval does not match subscription interval | | `validation_error` | Invalid item change (e.g., missing required fields) | | `payment_failed` | Payment failed for `always_invoice` behavior | | `internal_error` | Unexpected server error | ## Example: Roll out a price increase Update all customers from a $49 price to a $59 price, with proration collected at their next renewal: ```bash curl -X POST https://app.paymentkit.com/api/{account_id}/subscriptions/bulk-update-items \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "subscriptions": [ {"subscription_id": "sub_001", "items": [{"id": "si_a", "price_id": "price_pro_59"}]}, {"subscription_id": "sub_002", "items": [{"id": "si_b", "price_id": "price_pro_59"}]}, {"subscription_id": "sub_003", "items": [{"id": "si_c", "price_id": "price_pro_59"}]} ], "proration_behavior": "create_prorations" }' ``` For paused subscriptions in this batch: * Item changes apply immediately * No floating items are created (no active billing period) * When the subscription resumes, the next invoice reflects the new price ## Handling partial failures Bulk updates process each subscription independently. A failure in one subscription does not affect others. ```python result = client.subscriptions.bulk_update_items(...) if result.total_failed > 0: for failure in result.failed: if failure.error_code == "payment_failed": # Queue for retry after payment method update queue_for_retry(failure.subscription_id) elif failure.error_code == "invalid_state": # Log and skip cancelled subscriptions log_skipped(failure.subscription_id, failure.error_message) elif failure.error_code == "interval_mismatch": # Use subscription change requests workflow instead queue_for_change_request(failure.subscription_id) ``` > **Warning** > > Each subscription update commits independently. If subscription 2 fails after subscription 1 succeeds, subscription 1's changes cannot be rolled back. Design your error handling to accommodate partial success. > Update subscription-level settings, manage items (add/remove/modify), adjust quantities and prices, with control over proration timing and behavior for mid-cycle changes.