Subscription change requests
Multi-step workflow for previewing and applying subscription changes with explicit payment control, conflict resolution, and idempotent retry support.
The change request API provides a multi-step workflow for making complex subscription changes with preview, payment control, and retry safety. Use this when you need to:
- Preview changes before committing — show customers the exact proration amounts before applying
- Require payment before changes — guarantee payment succeeds before modifying the subscription (Charge First pattern)
- Handle retries safely — built-in idempotency prevents double-charging on retry
- Batch multiple changes — combine item updates, coupon changes, and balance adjustments in one atomic operation
For simpler immediate changes within the same billing interval, consider the update subscription items endpoint instead.
How it works
The change request workflow has four steps:
- Create — Initialize a draft change request for a subscription
- Add changes — Specify item changes (add/update/delete), coupon changes, and balance adjustments
- Preview — Compute execution plan, detect conflicts, calculate proration (transitions to READY)
- Apply — Collect payment (if needed) then execute changes (transitions to APPLIED)
Each change request is an independent entity with its own ID and status, allowing you to create, preview, and apply changes over multiple API calls.
Create a change request
Create a new draft change request for a subscription. Only one active (DRAFT or READY) change request can exist per subscription.
API
Python SDK
Request fields
Response
If the subscription already has an active change request, this endpoint returns a 409 Conflict error. Cancel the existing request first or use its ID to continue the workflow.
Add changes
Add item changes, coupon changes, or balance adjustments to a draft change request. You can call this endpoint multiple times to build up a complex change plan.
API
Python SDK
Item change actions
Immediate vs. scheduled changes:
apply_at_end: false(default) — Changes apply immediately with prorated billingapply_at_end: true— Changes are scheduled for the end of the current billing period (no proration)
Scheduled changes use different execution actions internally (add_scheduled, update_scheduled, drop_scheduled) but are transparently handled by the API.
Coupon changes
Balance changes
Balance changes (adding credits or debits to customer account balance) are defined in the API schema but not yet implemented. Attempting to add balance changes will result in a 501 Not Implemented error during the Apply step.
Response
You can call this endpoint multiple times to incrementally build your change plan. Each call appends to the existing changes.
Preview a change request
Preview the execution plan and proration amounts for a change request. This is a pure computation — no changes are made to the subscription, but the change request transitions from DRAFT to READY status and caches the preview result.
API
Python SDK
Response
Preview fields
Execution plan
The execution plan shows the exact sequence of operations that will be performed when you call Apply. Steps are executed in phase order:
- Phase 1 — Item changes (ADD, UPDATE, DROP, and their
_scheduledvariants) - Phase 2 — Coupon changes (COUPON_ADD, COUPON_REMOVE)
- Phase 3 — Balance changes (BALANCE_CREDIT_ADD, etc.) [not yet implemented]
Each step includes:
action— The operation type (e.g.,add,update_scheduled,COUPON_ADD)item_external_id— ID of the item being modified (for item changes)price_external_id— New price ID (for ADD/UPDATE)quantity— New quantity (for ADD/UPDATE, null means keep existing)coupon_external_id— Coupon ID (for coupon operations)
Conflict detection: If the preview detects unresolvable conflicts (e.g., trying to update and delete the same item), it returns a 409 Conflict error with details about the conflicting changes. You must modify the change request to resolve these conflicts before calling Apply.
Apply a change request
Execute the change plan with the Charge First pattern: payment is collected BEFORE any subscription changes are made. If payment fails, the subscription remains unchanged and the change request stays in READY status for retry.
API
Python SDK
Request fields
Charge First pattern
The Apply operation follows a strict three-phase pattern to ensure payment succeeds before making any changes:
Phase 1: Validate & Prepare
- Verify change request is in READY status
- Load subscription, items, and prices
- Build execution plan and verify no conflicts
- Calculate invoice total from cached preview
Phase 2: Payment (idempotent)
If invoice total > 0:
- Mark payment as being attempted (checkpoint)
- Create proration invoice with line items
- Finalize invoice to OPEN status
- Create payment intent
- Attempt payment via MIT orchestrator
- If payment fails: Mark payment_attempted=true, raise error, stay in READY
- If payment succeeds: Mark payment_recorded=true (checkpoint), proceed to Phase 3
If invoice total ≤ 0:
- Zero invoice: Mark as no_payment_required, proceed to Phase 3
- Negative invoice (downgrade): Issue credit note, proceed to Phase 3
Idempotency: On retry, if payment_recorded=true, skip payment collection entirely and proceed directly to Phase 3
Phase 3: Execute Changes
- Execute all steps from execution plan in phase order
- Handle item ADD/UPDATE/DROP operations (immediate and scheduled)
- Apply coupon changes
- Cancel original subscription if all items removed
- Transition change request to APPLIED status
Why Charge First?
- Prevents orphaned state — customer never has new items without payment
- Atomic success — either payment succeeds AND changes apply, or nothing happens
- Retry safety — if network fails after payment but before database commit, retry won’t double-charge
Response (success)
Response (payment failed)
When payment fails, the API returns a 402 Payment Required error:
The change request remains in READY status. You can retry the Apply operation:
- With a different payment method
- After the customer updates their payment method
- Same request ID works — idempotency prevents double-charging
Payment status values
3D Secure and requires_action: If payment requires customer action (3D Secure authentication), the API returns payment_status: "requires_action" with a payment_intent_id. You must redirect the customer to complete authentication, then retry the Apply call after authentication succeeds. The idempotency mechanism ensures the customer isn’t double-charged.
Cancel a change request
Cancel a draft or ready change request. Once applied, a change request cannot be cancelled.
API
Python SDK
Response
Cancelled change requests are retained for audit purposes. The subscription can now have a new change request created.
Status transitions
Change requests follow a strict state machine:
Adding changes to READY: If you call Add Changes on a READY change request, it transitions back to DRAFT. You must call Preview again before Apply.
Proration calculation
Proration is calculated for immediate changes only (not scheduled changes). The calculation depends on the change type:
DROP (remove item)
Credit for unused time at current price
Example: Remove a $100/month item with 15 days remaining in a 30-day period
- Credit: (15/30) × 50
UPDATE (change price or quantity)
Credit for old price + Charge for new price
Example: Upgrade from 200/month with 15 days remaining
- Credit: (15/30) × 50
- Charge: (15/30) × 100
- Net: 50 = $50
ADD (new item)
Charge for remaining time at new price
Example: Add a $50/month add-on with 15 days remaining
- Charge: (15/30) × 25
Scheduled changes (apply_at_end: true)
No proration — scheduled changes don’t affect the current period billing. They execute at the next renewal with no mid-cycle adjustments.
Downgrade handling
When the net proration is negative (more credits than charges), PaymentKit handles it automatically:
- Negative net < 0: Issue a credit note to customer balance immediately
- Zero net = 0: No invoice created, changes apply without payment
- Positive net > 0: Create invoice and collect payment
Example: Downgrade from 100/month with 15 days remaining
- Credit: (15/30) × 100
- Charge: (15/30) × 50
- Net: 100 = -50 credit note
Response fields for downgrades:
Common workflows
Preview before showing to customer
Build a change request, preview it, show the preview to the customer, then apply only after they confirm:
Handle payment failure with retry
Batch multiple changes atomically
All changes are applied atomically — either all succeed or none are applied.
Schedule changes for end of period
The change executes automatically at the subscription’s current_period_end.
Comparison with other endpoints
When to use:
- Change Request (recommended) — When you need explicit preview before applying changes, multi-step workflow with user confirmation, or idempotent retry safety
- Update Items — For simple same-interval changes when preview and multi-step workflow aren’t needed
- Change Plan (deprecated) — Legacy endpoint for cross-interval changes. Use Change Request instead for new integrations.
Note: All three endpoints use Charge First pattern to guarantee payment succeeds before modifying subscriptions. The key differences are preview capability, workflow complexity, and retry safety.
Webhooks
Immediate changes
When a change request is applied with immediate changes:
subscription.change_request.created— Change request created (DRAFT)subscription.change_request.previewed— Preview completed (READY)invoice.created— Proration invoice created (if invoice_total > 0)invoice.paid— Payment succeeded (if invoice_total > 0)customer.subscription.updated— Subscription items modifiedsubscription.change_request.applied— Change request applied (APPLIED)
Scheduled changes
When a change request includes scheduled changes (apply_at_end: true):
- Change request webhooks fire as normal (created, previewed, applied)
customer.subscription.updated— Pending changes stored on subscription- At period end, when the scheduled change executes:
customer.subscription.updated— Items activated/deactivated/updated
Failed payment
When payment fails during Apply:
subscription.change_request.payment_failed— Payment failed, still in READY- Customer updates payment method
- Retry Apply → same sequence as successful payment
Delivery order is NOT guaranteed. Design webhook handlers to be idempotent and don’t assume ordering.