> 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/subscription-change-requests/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.paymentkit.com/_mcp/server. # Subscription change requests 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 > **Tip** > > For simpler immediate changes within the same billing interval, consider the [update subscription items](/guides/billing/subscriptions/update-a-subscription#update-subscription-items) endpoint instead. # How it works The change request workflow has four steps: ```mermaid stateDiagram-v2 [*] --> DRAFT: 1. Create change request DRAFT --> DRAFT: 2. Add changes DRAFT --> READY: 3. Preview READY --> APPLIED: 4. Apply (with payment) READY --> CANCELLED: Cancel DRAFT --> CANCELLED: Cancel APPLIED --> [*] CANCELLED --> [*] ``` 1. **Create** — Initialize a draft change request for a subscription 2. **Add changes** — Specify item changes (add/update/delete), coupon changes, and balance adjustments 3. **Preview** — Compute execution plan, detect conflicts, calculate proration (transitions to READY) 4. **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 ```bash curl -X POST https://app.paymentkit.com/api/{account_id}/change-requests \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "subscription_id": "sub_abc123", "reason": "Upgrade to annual plan", "expires_in_hours": 24 }' ``` #### Python SDK ```python change_request = client.change_requests.create( account_id="acc_abc123", subscription_id="sub_abc123", reason="Upgrade to annual plan", expires_in_hours=24 ) ``` ## Request fields | Field | Type | Required | Description | | ------------------ | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `subscription_id` | string | Yes | External ID of the subscription to modify | | `reason` | string | No | Optional reason for the change (stored as metadata) | | `expires_in_hours` | integer | No | Hours until the draft expires (default: 24). The change request automatically transitions to EXPIRED status after this time if not applied. | ## Response ```json { "id": "chg_live_a1b2c3d4", "subscription_id": "sub_abc123", "status": "draft", "reason": "Upgrade to annual plan", "created_at": "2026-04-14T10:00:00Z", "expires_at": "2026-04-15T10:00:00Z", "item_changes": [], "coupon_changes": [], "balance_changes": [] } ``` > **Warning** > > 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 ```bash curl -X POST https://app.paymentkit.com/api/{account_id}/change-requests/{change_request_id}/changes \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "item_changes": [ { "action": "update", "item_id": "si_monthly_plan", "price_id": "price_annual_plan" }, { "action": "add", "price_id": "price_addon_support", "quantity": 1, "apply_at_end": false } ], "coupon_changes": [ { "action": "add", "coupon_id": "coup_welcome20" } ] }' ``` #### Python SDK ```python result = client.change_requests.add_changes( account_id="acc_abc123", change_request_id="chg_live_a1b2c3d4", item_changes=[ { "action": "update", "item_id": "si_monthly_plan", "price_id": "price_annual_plan" }, { "action": "add", "price_id": "price_addon_support", "quantity": 1, "apply_at_end": False } ], coupon_changes=[ { "action": "add", "coupon_id": "coup_welcome20" } ] ) ``` ## Request fields | Field | Type | Required | Description | | ----------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `item_changes` | array | No | Item add/update/drop operations — see below | | `coupon_changes` | array | No | Coupon add/remove operations | | `balance_changes` | array | No | Customer balance adjustments | | `trial_behavior` | string | No | `preserve` (default) or `end_now`. How to treat an ongoing trial — see [Trials](#trials). Omitting it leaves whatever was previously set on this change request. | ## Item change actions | Action | Required fields | Optional fields | Description | | -------- | ----------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `add` | `price_id` | `quantity`, `apply_at_end` | Add a new subscription item. If `apply_at_end: true`, the item is created inactive and activates at the next renewal. Quantity defaults to 1. | | `update` | `item_id`, `price_id` OR `quantity` | `price_id`, `quantity`, `apply_at_end` | Update an existing item's price and/or quantity. If `apply_at_end: true`, the update is deferred to the next renewal. | | `drop` | `item_id` | `apply_at_end` | Remove an item. If `apply_at_end: true`, the item is marked for removal at the next renewal. | > **Info** > > **Immediate vs. scheduled changes:** > > * `apply_at_end: false` (default) — Changes apply immediately with prorated billing > * `apply_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. ## Trials If the subscription is **still in its trial** when the change is applied, the change is applied **in place**: the same subscription keeps its identity and its original trial end date, its plan is swapped, and no split subscription is created. Nothing is charged at the time of the change, because nothing has been paid yet — there is no period to prorate. The customer is first charged **when the trial ends**, at the new price. | `trial_behavior` | Effect | | -------------------- | ----------------------------------------------------------------------------------- | | `preserve` (default) | Keep the trial's original end date. First charge at trial end, at the new price. | | `end_now` | End the trial immediately and bill the first period at the new price straight away. | `trial_behavior` is set per change, on the same `PATCH` request that carries the item changes. It is ignored when the subscription has no trial time remaining — requesting `preserve` on a subscription that is not mid-trial does **not** avoid a charge. > **Warning** > > **This changed the default behaviour.** Previously, a trialing subscription that changed > across billing intervals had its trial discarded and a new subscription created, and no > charge was raised until that new period ended — so the customer received unpaid service. > Requests that do not specify `trial_behavior` now get `preserve`. > > There is no longer a way to request the old behaviour. If you relied on a trialing > cross-interval change producing a second subscription, use `end_now` (which converts the > trial immediately) or end the trial before changing the plan. > **Info** > > A change that **adds** a differently-billed item without removing the existing one still > creates a separate subscription, even mid-trial — a subscription cannot hold items on two > different billing intervals. Only a change that replaces the plan is applied in place. ## Coupon changes | Action | Required fields | Description | | -------- | --------------- | ------------------------------------------------------------------ | | `add` | `coupon_id` | Attach a coupon to the subscription. Replaces any existing coupon. | | `remove` | `coupon_id` | Remove the specified coupon from the subscription. | ## Balance changes > **Note** > > 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 ```json { "change_request": { "id": "chg_live_a1b2c3d4", "subscription_id": "sub_abc123", "status": "draft", "item_changes": [ { "action": "update", "item_id": "si_monthly_plan", "price_id": "price_annual_plan", "quantity": null, "apply_at_end": false }, { "action": "add", "price_id": "price_addon_support", "quantity": 1, "apply_at_end": false } ], "coupon_changes": [ { "action": "add", "coupon_id": "coup_welcome20" } ], "balance_changes": [] }, "changes_count": 3 } ``` > **Tip** > > 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 ```bash curl -X POST https://app.paymentkit.com/api/{account_id}/change-requests/{change_request_id}/preview \ -H "Authorization: Bearer sk_live_..." ``` #### Python SDK ```python preview = client.change_requests.preview( account_id="acc_abc123", change_request_id="chg_live_a1b2c3d4" ) ``` ## Response ```json { "change_request": { "id": "chg_live_a1b2c3d4", "status": "ready", "last_preview": { "items_to_add": [ {"price_id": "price_addon_support", "quantity": 1} ], "items_to_update": [ {"item_id": "si_monthly_plan", "price_id": "price_annual_plan", "quantity": null} ], "items_to_delete": [], "coupon_to_add": "coup_welcome20", "coupon_to_remove": null, "balance_to_apply_atom": 0, "proration_credit_atom": -5000, "proration_charge_atom": 100000, "invoice_total_atom": 95000, "execution_plan": { "steps": [ { "phase": 1, "action": "update", "item_external_id": "si_monthly_plan", "price_external_id": "price_annual_plan", "quantity": null }, { "phase": 1, "action": "add", "price_external_id": "price_addon_support", "quantity": 1 }, { "phase": 2, "action": "COUPON_ADD", "coupon_external_id": "coup_welcome20" } ], "auto_resolutions": [] } } }, "preview": { /* same as last_preview */ }, "execution_plan": { /* same as last_preview.execution_plan */ } } ``` ## Preview fields | Field | Description | | --------------------------------- | ------------------------------------------------------------ | | `items_to_add` | Items that will be added to the subscription | | `items_to_update` | Items that will be updated (price and/or quantity changes) | | `items_to_delete` | Items that will be removed | | `coupon_to_add` | Coupon ID being attached (null if none) | | `coupon_to_remove` | Coupon ID being removed (null if none) | | `balance_to_apply_atom` | Account balance credit/debit amount in atomic units | | `proration_credit_atom` | Total credits for unused time (negative value) | | `proration_charge_atom` | Total charges for new items (positive value) | | `invoice_total_atom` | Net amount customer will be charged (max of 0 and net\_atom) | | `execution_plan.steps` | Ordered list of steps that will be executed during Apply | | `execution_plan.auto_resolutions` | Conflicts that were automatically resolved | ## 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 `_scheduled` variants) * **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) > **Warning** > > **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 ```bash curl -X POST https://app.paymentkit.com/api/{account_id}/change-requests/{change_request_id}/apply \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "payment_method_id": "pm_card123" }' ``` #### Python SDK ```python result = client.change_requests.apply( account_id="acc_abc123", change_request_id="chg_live_a1b2c3d4", payment_method_id="pm_card123" # optional override ) ``` ## Request fields | Field | Type | Required | Description | | ------------------- | ------ | -------- | -------------------------------------------------------------------------------------------------- | | `payment_method_id` | string | No | Optional payment method to use instead of the subscription's default. Must belong to the customer. | ## 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:** 1. Mark payment as being attempted (checkpoint) 2. Create proration invoice with line items 3. Finalize invoice to OPEN status 4. Create payment intent 5. Attempt payment via MIT orchestrator 6. **If payment fails:** Mark payment\_attempted=true, raise error, stay in READY 7. **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 > **Info** > > **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) ```json { "change_request": { "id": "chg_live_a1b2c3d4", "status": "applied", "applied_at": "2026-04-14T10:30:00Z" }, "result": { "subscription_external_id": "sub_abc123", "new_subscriptions": [], "invoice_external_id": "inv_proration456", "payment_status": "paid", "step_results": [ { "phase": 1, "action": "update", "item_external_id": "si_monthly_plan", "result": "success" }, { "phase": 1, "action": "add", "item_external_id": null, "result": "success" }, { "phase": 2, "action": "COUPON_ADD", "result": "success" } ] } } ``` ## Response (payment failed) When payment fails, the API returns a 402 Payment Required error: ```json { "error": "payment_failed", "message": "Payment failed for change plan", "payment_status": "failed", "payment_error": "Your card was declined. Please try a different payment method.", "orchestrator_summary": "Card declined by issuer (insufficient_funds)" } ``` 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 | Status | Description | | --------------------- | --------------------------------------------------------- | | `paid` | Payment succeeded | | `processing` | Async payment (ACH/SEPA) submitted, awaiting confirmation | | `failed` | Payment failed | | `requires_action` | Customer action required (e.g., 3D Secure) | | `no_payment_method` | No payment method available | | `already_paid` | Payment was already recorded (idempotent retry) | | `no_payment_required` | Zero or negative invoice (no payment needed) | > **Warning** > > **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 ```bash curl -X DELETE https://app.paymentkit.com/api/{account_id}/change-requests/{change_request_id} \ -H "Authorization: Bearer sk_live_..." ``` #### Python SDK ```python result = client.change_requests.cancel( account_id="acc_abc123", change_request_id="chg_live_a1b2c3d4" ) ``` ## Response ```json { "id": "chg_live_a1b2c3d4", "status": "cancelled", "cancelled_at": "2026-04-14T11:00:00Z" } ``` > **Info** > > 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: ``` DRAFT → READY → APPLIED ↓ ↓ CANCELLED CANCELLED ↓ EXPIRED (after expires_at) ``` | Status | Description | Allowed operations | | ----------- | --------------------------------- | ---------------------------- | | `draft` | Initial state after creation | Add changes, Preview, Cancel | | `ready` | Preview completed, ready to apply | Apply, Cancel | | `applied` | Successfully executed | None (terminal) | | `cancelled` | Manually cancelled | None (terminal) | | `expired` | Draft expired before preview | None (terminal) | > **Warning** > > **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** ``` credit = (days_remaining / days_in_period) × item_price × quantity ``` Example: Remove a \$100/month item with 15 days remaining in a 30-day period * Credit: (15/30) × $100 = $50 ## UPDATE (change price or quantity) **Credit for old price + Charge for new price** ``` credit = (days_remaining / days_in_period) × old_price × old_quantity charge = (days_remaining / days_in_period) × new_price × new_quantity net = charge - credit ``` Example: Upgrade from $100/month to $200/month with 15 days remaining * Credit: (15/30) × $100 = $50 * Charge: (15/30) × $200 = $100 * Net: $100 - $50 = \$50 ## ADD (new item) **Charge for remaining time at new price** ``` charge = (days_remaining / days_in_period) × price × quantity ``` Example: Add a \$50/month add-on with 15 days remaining * Charge: (15/30) × $50 = $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 $200/month to $100/month with 15 days remaining * Credit: (15/30) × $200 = $100 * Charge: (15/30) × $100 = $50 * Net: $50 - $100 = -$50 → Issue $50 credit note **Response fields for downgrades:** ```json { "change_request": {...}, "result": { "subscription_id": "sub_abc123", "invoice_id": null, "credit_note_id": "cn_abc123", "payment_status": "not_required", "new_subscriptions": [], "step_results": [...] } } ``` # 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: ```python # 1. Create draft change_request = client.change_requests.create( account_id="acc_abc123", subscription_id="sub_abc123" ) # 2. Add changes client.change_requests.add_changes( account_id="acc_abc123", change_request_id=change_request["id"], item_changes=[{ "action": "update", "item_id": "si_monthly_plan", "price_id": "price_annual_plan" }] ) # 3. Preview preview = client.change_requests.preview( account_id="acc_abc123", change_request_id=change_request["id"] ) # 4. Show preview to customer print(f"Proration credit: ${preview['preview']['proration_credit_atom'] / 100}") print(f"Proration charge: ${preview['preview']['proration_charge_atom'] / 100}") print(f"Total to charge: ${preview['preview']['invoice_total_atom'] / 100}") # 5. Customer confirms, apply result = client.change_requests.apply( account_id="acc_abc123", change_request_id=change_request["id"] ) if result["result"]["payment_status"] == "paid": print("Success! Subscription updated.") else: print(f"Payment failed: {result['result'].get('payment_error')}") ``` ## Handle payment failure with retry ```python try: result = client.change_requests.apply( account_id="acc_abc123", change_request_id="chg_live_a1b2c3d4" ) except PaymentFailedError as e: # Change request stays in READY status # Prompt customer to update payment method print(f"Payment failed: {e.payment_error}") # After customer updates payment method, retry with new PM result = client.change_requests.apply( account_id="acc_abc123", change_request_id="chg_live_a1b2c3d4", payment_method_id="pm_new_card" ) # Idempotency ensures no double-charging ``` ## Batch multiple changes atomically ```python # Create draft change_request = client.change_requests.create( account_id="acc_abc123", subscription_id="sub_abc123" ) # Add all changes in one call client.change_requests.add_changes( account_id="acc_abc123", change_request_id=change_request["id"], item_changes=[ {"action": "update", "item_id": "si_basic", "price_id": "price_pro"}, {"action": "add", "price_id": "price_addon_storage", "quantity": 2}, {"action": "drop", "item_id": "si_old_addon"} ], coupon_changes=[ {"action": "add", "coupon_id": "coup_enterprise20"} ] ) # Preview and apply preview = client.change_requests.preview( account_id="acc_abc123", change_request_id=change_request["id"] ) result = client.change_requests.apply( account_id="acc_abc123", change_request_id=change_request["id"] ) ``` All changes are applied atomically — either all succeed or none are applied. ## Schedule changes for end of period ```python # Create and add changes change_request = client.change_requests.create( account_id="acc_abc123", subscription_id="sub_abc123" ) client.change_requests.add_changes( account_id="acc_abc123", change_request_id=change_request["id"], item_changes=[ { "action": "update", "item_id": "si_monthly_plan", "price_id": "price_annual_plan", "apply_at_end": True # Schedule for period end } ] ) # Preview (no proration for scheduled changes) preview = client.change_requests.preview( account_id="acc_abc123", change_request_id=change_request["id"] ) # proration_credit_atom and proration_charge_atom will be 0 # Apply (creates pending change on subscription) result = client.change_requests.apply( account_id="acc_abc123", change_request_id=change_request["id"] ) # payment_status will be "no_payment_required" ``` The change executes automatically at the subscription's `current_period_end`. # Comparison with other endpoints | Feature | Change Request | Update Items | Change Plan (deprecated) | | -------------------------- | --------------------------- | ------------------------------------------------- | ----------------------------------- | | **Preview before apply** | ✅ Explicit preview step | ❌ No preview | ❌ No preview | | **Payment before changes** | ✅ Guaranteed (Charge First) | ✅ Guaranteed (Charge First with `always_invoice`) | ✅ Guaranteed (Charge First, always) | | **Idempotent retry** | ✅ Built-in | ❌ No | ❌ No | | **Status tracking** | ✅ DRAFT → READY → APPLIED | ❌ No | ❌ No | | **Multi-step workflow** | ✅ Create → Preview → Apply | ❌ Single call | ❌ Single call | | **Scheduled changes** | ✅ `apply_at_end: true` | ✅ `start_at_end`/`drop_at_end` | ✅ `effective_at: "period_end"` | | **Same-interval changes** | ✅ Supported | ✅ Primary use case | ⚠️ Supported but not recommended | | **Cross-interval changes** | ✅ Supported | ❌ Not supported | ✅ Primary use case | | **Conflict detection** | ✅ Automatic in Preview | ❌ No | ❌ No | | **Deprecation status** | ✅ Current | ✅ Current | ⚠️ Planned for deprecation | **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: 1. `subscription.change_request.created` — Change request created (DRAFT) 2. `subscription.change_request.previewed` — Preview completed (READY) 3. `invoice.created` — Proration invoice created (if invoice\_total > 0) 4. `invoice.paid` — Payment succeeded (if invoice\_total > 0) 5. `customer.subscription.updated` — Subscription items modified 6. `subscription.change_request.applied` — Change request applied (APPLIED) ## Scheduled changes When a change request includes scheduled changes (`apply_at_end: true`): 1. Change request webhooks fire as normal (created, previewed, applied) 2. `customer.subscription.updated` — Pending changes stored on subscription 3. At period end, when the scheduled change executes: * `customer.subscription.updated` — Items activated/deactivated/updated ## Failed payment When payment fails during Apply: 1. `subscription.change_request.payment_failed` — Payment failed, still in READY 2. Customer updates payment method 3. Retry Apply → same sequence as successful payment > **Warning** > > **Delivery order is NOT guaranteed.** Design webhook handlers to be idempotent and don't assume ordering. > Multi-step workflow for previewing and applying subscription changes with explicit payment control, conflict resolution, and idempotent retry support.