> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dodopayments.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Subscription Upgrade & Downgrade Guide

> Learn how to change a customer's subscription plan, handle proration, and process webhooks reliably.

<CardGroup cols={3}>
  <Card title="Change Plan API" icon="code" href="/api-reference/subscriptions/change-plan">
    Full API docs for updating subscriptions.
  </Card>

  <Card title="Plan Change Preview" icon="eye" href="/api-reference/subscriptions/preview-change-plan">
    See charge amounts before changing plans.
  </Card>

  <Card title="Integration Guide" icon="book" href="/developer-resources/subscription-integration-guide">
    Step-by-step subscription setup.
  </Card>
</CardGroup>

## What is a subscription upgrade or downgrade?

Changing plans lets you move a customer between subscription tiers or quantities. Use it to:

* Align pricing with usage or features
* Move from monthly to annual (or vice versa)
* Adjust quantity for seat-based products

<Info>
  Plan changes can trigger an immediate charge depending on the proration mode you choose.
</Info>

## When to use plan changes

* Upgrade when a customer needs more features, usage, or seats
* Downgrade when usage decreases
* Migrate users to a new product or price without cancelling their subscription

## Plan Change Flow

```mermaid theme={null}
sequenceDiagram
    participant C as Customer
    participant A as Your App
    participant D as Dodo Payments
    C->>A: Request plan change
    A->>D: Preview change
    D-->>A: Charge breakdown
    A->>C: Show cost details
    C->>A: Confirm
    A->>D: Change plan API call
    D->>D: Prorate & charge
    D-->>A: Result + invoice
    D->>A: Webhooks (payment, plan_changed)
    A->>C: Plan updated
```

## Prerequisites

Before implementing subscription plan changes, ensure you have:

* A Dodo Payments merchant account with active subscription products
* API credentials (API key and webhook secret key) from the dashboard
* An existing active subscription to modify
* Webhook endpoint configured to handle subscription events

<Info>
  For detailed setup instructions, see our [Integration Guide](/developer-resources/integration-guide#dashboard-setup).
</Info>

## Step-by-Step Implementation Guide

Follow this comprehensive guide to implement subscription plan changes in your application:

<Steps>
  <Step title="Understand Plan Change Requirements">
    Before implementing, determine:

    * Which subscription products can be changed to which others
    * What proration mode fits your business model
    * How to handle failed plan changes gracefully
    * Which webhook events to track for state management

    <Tip>
      Test plan changes thoroughly in test mode before implementing in production.
    </Tip>
  </Step>

  <Step title="Choose Your Proration Strategy">
    Select the billing approach that aligns with your business needs:

    <Tabs>
      <Tab title="prorated_immediately">
        Best for: SaaS applications wanting to charge fairly for unused time

        * Calculates exact prorated amount based on remaining cycle time
        * Charges a prorated amount based on unused time remaining in the cycle
        * Provides transparent billing to customers
      </Tab>

      <Tab title="difference_immediately">
        Best for: Clear upgrade/downgrade scenarios

        * Upgrade: Charge immediate difference (e.g., $30→$80 = charge \$50)
        * Downgrade: Credit remaining value for future renewals
        * Simplifies billing logic and customer communication
      </Tab>

      <Tab title="full_immediately">
        Best for: When you want to reset the billing cycle

        * Charges full amount of new plan immediately
        * Ignores remaining time from old plan
        * Useful for annual to monthly transitions
      </Tab>

      <Tab title="do_not_bill">
        Best for: Free migrations, courtesy switches, or absorbing cost differences

        * No charges or credits calculated
        * Customer moves to the new plan immediately without any billing adjustment
        * Billing cycle remains unchanged
      </Tab>
    </Tabs>
  </Step>

  <Step title="Implement the Change Plan API">
    Use the Change Plan API to modify subscription details:

    <ParamField path="subscription_id" type="string" required>
      The ID of the active subscription to modify.
    </ParamField>

    <ParamField path="product_id" type="string" required>
      The new product ID to change the subscription to.
    </ParamField>

    <ParamField path="quantity" type="integer" required>
      新计划的单位数量（针对基于席位的产品）。
    </ParamField>

    <ParamField path="proration_billing_mode" type="string" required>
      How to handle immediate billing: `prorated_immediately`, `full_immediately`, `difference_immediately`, or `do_not_bill`.
    </ParamField>

    <ParamField path="addons" type="array">
      Optional addons for the new plan. Leaving this empty removes any existing addons.
    </ParamField>

    <ParamField path="on_payment_failure" type="string">
      Controls behavior when the plan change payment fails:

      * `prevent_change`: Keep subscription on current plan until payment succeeds
      * `apply_change` (default): Apply plan change immediately regardless of payment outcome

      If not specified, uses the business-level default setting.
    </ParamField>

    <ParamField path="discount_codes" type="array">
      可选的 **stacked** 折扣代码应用于新计划（最多20个，按数组顺序应用）。行为取决于您传递的内容：

      * **未提供 / `null`** — 如果适用于新产品，现有的折扣与 `preserve_on_plan_change=true` 会被保留。
      * **`[]`（空数组）** — 从订阅中移除所有现有折扣。
      * **`["CODE_A", "CODE_B", ...]`** — 用该堆叠集替换任何现有的折扣。
    </ParamField>

    <ParamField path="discount_code" type="string" deprecated>
      **已弃用** — 新的集成优先使用 `discount_codes`。此字段仍然保留后向兼容性，但不能在同一请求中与 `discount_codes` 结合使用。
    </ParamField>

    <ParamField path="effective_at" type="string" default="immediately">
      何时应用计划更改：

      * `immediately`（默认）：立即应用计划更改
      * `next_billing_date`：为下一个账单日期安排更改。客户在账单周期结束前保持当前计划。

      对降级使用 `next_billing_date`，以便客户在账单周期结束前保留当前计划的好处。
    </ParamField>
  </Step>

  <Step title="Handle Webhook Events">
    设置 webhook 处理以跟踪计划更改的结果：

    * `subscription.active`：计划更改成功，订阅已更新
    * `subscription.plan_changed`：订阅计划已更改（升级/降级/插件更新）
    * `subscription.on_hold`：计划更改费用失败，订阅暂停
    * `payment.succeeded`：计划更改的即时费用已成功
    * `payment.failed`：即时费用失败

    <Warning>
      始终验证 webhook 签名并实现幂等事件处理。
    </Warning>
  </Step>

  <Step title="Update Your Application State">
    根据 webhook 事件更新您的应用程序：

    * 根据新计划授予/撤销功能
    * 使用新计划详细信息更新客户仪表板
    * 发送有关计划更改的确认电子邮件
    * 记录账单更改以供审核
  </Step>

  <Step title="Test and Monitor">
    彻底测试您的实现：

    * 使用不同场景测试所有比例模式
    * 验证 webhook 处理是否正常工作
    * 监控计划更改的成功率
    * 为计划更改失败设置警报

    <Check>
      您的订阅计划更改实施现在已准备好用于生产环境。
    </Check>
  </Step>
</Steps>

## 预览计划更改

在确认计划更改之前，使用预览 API 向客户展示他们将被收取的确切费用：

<Tabs>
  <Tab title="Node.js SDK">
    ```javascript theme={null}
    const preview = await client.subscriptions.previewChangePlan('sub_123', {
      product_id: 'prod_pro',
      quantity: 1,
      proration_billing_mode: 'prorated_immediately'
    });

    // Show customer the charge before confirming
    console.log('Immediate charge:', preview.immediate_charge.summary);
    console.log('New plan details:', preview.new_plan);
    ```
  </Tab>

  <Tab title="Python SDK">
    ```python theme={null}
    preview = client.subscriptions.preview_change_plan(
        subscription_id="sub_123",
        product_id="prod_pro",
        quantity=1,
        proration_billing_mode="prorated_immediately"
    )

    # Show customer the charge before confirming
    print("Immediate charge:", preview.immediate_charge.summary)
    print("New plan details:", preview.new_plan)
    ```
  </Tab>
</Tabs>

<Tip>
  使用预览 API 构建确认对话框，在客户确认计划更改之前显示他们将被收取的确切金额。
</Tip>

## 更改计划 API

使用更改计划 API 修改活动订阅的产品、数量和比例行为。

### 快速启动示例

<Tabs>
  <Tab title="Node.js SDK">
    ```javascript theme={null}
    import DodoPayments from 'dodopayments';

    const client = new DodoPayments({
      bearerToken: process.env.DODO_PAYMENTS_API_KEY,
      environment: 'test_mode', // defaults to 'live_mode'
    });

    async function changePlan() {
      const result = await client.subscriptions.changePlan('sub_123', {
        product_id: 'prod_new',
        quantity: 3,
        proration_billing_mode: 'prorated_immediately',
        on_payment_failure: 'prevent_change', // Optional: control behavior on payment failure
      });
      console.log(result.status, result.invoice_id, result.payment_id);
    }

    changePlan();
    ```
  </Tab>

  <Tab title="Python SDK">
    ```python theme={null}
    import os
    from dodopayments import DodoPayments

    client = DodoPayments(
        bearer_token=os.environ.get("DODO_PAYMENTS_API_KEY"),
        environment="test_mode",  # defaults to "live_mode"
    )

    result = client.subscriptions.change_plan(
        subscription_id="sub_123",
        product_id="prod_new",
        quantity=3,
        proration_billing_mode="prorated_immediately",
        on_payment_failure="prevent_change",  # Optional: control behavior on payment failure
    )
    print(result.status, result.get("invoice_id"), result.get("payment_id"))
    ```
  </Tab>

  <Tab title="Go SDK">
    ```go theme={null}
    package main

    import (
      "context"
      "fmt"
      "github.com/dodopayments/dodopayments-go"
      "github.com/dodopayments/dodopayments-go/option"
    )

    func main() {
      client := dodopayments.NewClient(option.WithBearerToken("YOUR_TOKEN"))
      // ChangePlan returns no response body; a nil error means the change succeeded.
      err := client.Subscriptions.ChangePlan(context.TODO(), "sub_123", dodopayments.SubscriptionChangePlanParams{
        UpdateSubscriptionPlanReq: dodopayments.UpdateSubscriptionPlanReqParam{
          ProductID:            dodopayments.F("prod_new"),
          Quantity:             dodopayments.F(int64(3)),
          ProrationBillingMode: dodopayments.F(dodopayments.UpdateSubscriptionPlanReqProrationBillingModeProratedImmediately),
          OnPaymentFailure:     dodopayments.F(dodopayments.UpdateSubscriptionPlanReqOnPaymentFailurePreventChange), // Optional
        },
      })
      if err != nil { panic(err) }
      fmt.Println("Plan change applied")
    }
    ```
  </Tab>

  <Tab title="HTTP">
    ```bash theme={null}
    curl -X POST "$DODO_API_BASE/subscriptions/sub_123/change-plan" \
      -H "Authorization: Bearer $DODO_PAYMENTS_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "product_id": "prod_new",
        "quantity": 3,
        "proration_billing_mode": "prorated_immediately",
        "on_payment_failure": "prevent_change"
      }'
    ```
  </Tab>
</Tabs>

```json Success theme={null}
{
  "status": "processing",
  "subscription_id": "sub_123",
  "invoice_id": "inv_789",
  "payment_id": "pay_456",
  "proration_billing_mode": "prorated_immediately"
}
```

<Note>
  只有在计划更改期间创建即时费用和/或发票时，字段如<code>invoice\_id</code>和<code>payment\_id</code>才会返回。总是依赖 webhook 事件（例如<code>payment.succeeded</code>，<code>subscription.plan\_changed</code>）来确认结果。
</Note>

<Warning>
  如果即时费用失败，订阅可能会移动到 `subscription.on_hold`，直到付款成功。
</Warning>

## 管理插件

在更改订阅计划时，您还可以修改插件：

```javascript theme={null}
// Add addons to the new plan
await client.subscriptions.changePlan('sub_123', {
  product_id: 'prod_new',
  quantity: 1,
  proration_billing_mode: 'difference_immediately',
  addons: [
    { addon_id: 'addon_123', quantity: 2 }
  ]
});

// Remove all existing addons
await client.subscriptions.changePlan('sub_123', {
  product_id: 'prod_new',
  quantity: 1,
  proration_billing_mode: 'difference_immediately',
  addons: [] // Empty array removes all existing addons
});
```

<Info>
  插件包含在比例计算中，将根据所选的比例模式收费。
</Info>

## 应用折扣代码

在更改订阅计划时，您可以应用一个或多个 **stacked** 折扣代码（最多20个，按数组顺序应用）。这对于在升级或迁移时提供促销价格很有用。

<Tabs>
  <Tab title="Node.js SDK">
    ```javascript theme={null}
    // Apply stacked discount codes during plan change
    await client.subscriptions.changePlan('sub_123', {
      product_id: 'prod_pro',
      quantity: 1,
      proration_billing_mode: 'prorated_immediately',
      discount_codes: ['UPGRADE20']
    });
    ```
  </Tab>

  <Tab title="Python SDK">
    ```python theme={null}
    # Apply stacked discount codes during plan change
    client.subscriptions.change_plan(
        subscription_id="sub_123",
        product_id="prod_pro",
        quantity=1,
        proration_billing_mode="prorated_immediately",
        discount_codes=["UPGRADE20"]
    )
    ```
  </Tab>

  <Tab title="HTTP">
    ```bash theme={null}
    curl -X POST "$DODO_API_BASE/subscriptions/sub_123/change-plan" \
      -H "Authorization: Bearer $DODO_PAYMENTS_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "product_id": "prod_pro",
        "quantity": 1,
        "proration_billing_mode": "prorated_immediately",
        "discount_codes": ["UPGRADE20"]
      }'
    ```
  </Tab>
</Tabs>

### 计划更改时的折扣行为

| `discount_codes` 值          | 行为                                               |
| --------------------------- | ------------------------------------------------ |
| 未提供 / `null`                | 自动保留适用于新产品的现有折扣与 `preserve_on_plan_change=true`。 |
| `[]`（空数组）                   | 从订阅中移除**所有**现有折扣。                                |
| `["CODE_A", "CODE_B", ...]` | 用该堆叠集替换任何现有折扣，按数组顺序验证和应用。                        |

<Info>
  该端点上的单一 `discount_code` 字段 **已弃用** 但仍然可以用于后向兼容性 — 现有集成不需要立即更改。它不能与 `discount_codes` 在同一请求中结合使用。方便时迁移到数组形式。
</Info>

<Tip>
  使用 [预览计划更改 API](/api-reference/subscriptions/preview-change-plan) 和 `discount_codes`，在确认计划更改前向客户展示他们节省的确切金额。
</Tip>

## 比例模式

选择在更改计划时如何向客户收费：

#### `prorated_immediately`

* 对当前周期的部分差异收费
* 如果在试用期内，立即收费并转换为新计划
* 降级：可能生成预付抵用金，用于未来续订

#### `full_immediately`

* 立即全额收费
* 忽略旧计划的剩余时间

<Info>
  使用<code>difference\_immediately</code>进行的降级所创建的抵用金是订阅范围内的，与<a href="/features/credit-based-billing">基于抵用的结算</a>权利相区别。它们会自动应用于同一订阅的未来续订，不能在订阅之间转移。
</Info>

#### `difference_immediately`

* 升级：立即收费旧计划和新计划之间的价格差异
* 降级：将剩余价值作为内部抵用金添加到订阅，并在续订时自动应用

#### `do_not_bill`

* 不计算任何费用或抵用金
* 客户立即切换到新计划，没有任何账单调整
* 账单周期保持不变
* 最适合礼遇迁移、免费计划切换或吸收成本差异

| 功能       | `prorated_immediately` | `difference_immediately` | `full_immediately` | `do_not_bill` |
| -------- | ---------------------- | ------------------------ | ------------------ | ------------- |
| **升级费用** | 剩余天数的比例差异              | 计划之间的全额价格差异              | 新计划的全额价格           | 无费用           |
| **降级抵用** | 剩余天数的比例抵用              | 全额价格差异作为抵用               | 无抵用                | 无抵用           |
| **账单周期** | 未变                     | 未变                       | 重置为今天              | 未变            |
| **试用行为** | 结束试用，立即收费              | 结束试用，立即收费                | 结束试用，全额收费          | 结束试用，无费用      |
| **最适合**  | 公平的基于时间的账单             | 简单的升级/降级计算               | 重置账单周期             | 免费迁移或礼遇切换     |
| **复杂性**  | 中等（天数计算）               | 低（简单减法）                  | 低（全额收费）            | 无             |

```mermaid theme={null}
flowchart TD
    A{Need fair day-by-day billing?} -->|Yes| B[prorated_immediately]
    A -->|No| C{Need to reset billing cycle?}
    C -->|Yes| D[full_immediately]
    C -->|No| E{Skip billing entirely?}
    E -->|Yes| F[do_not_bill]
    E -->|No| G[difference_immediately]
```

### 示例场景

使用这些规范数字保持一致：

* 当前计划：**Basic** 每月 **\$30**
* 升级目标：**Pro** 每月 **\$80**
* 降级目标（从 Pro）：**Starter** 每月 **\$20**
* 账单周期：**30天**，从 **1月1日** 开始
* 计划更改发生在 **1月16日**（剩余 15 天，已使用 15 天）

<AccordionGroup>
  <Accordion title="Upgrade: Basic ($30) → Pro ($80) with prorated_immediately">
    ```
    Step 1: Calculate unused credit from current plan
      Unused days = 15 out of 30 days
      Credit = $30 × (15/30) = $15.00

    Step 2: Calculate prorated cost of new plan
      Remaining days = 15 out of 30 days
      New plan cost = $80 × (15/30) = $40.00

    Step 3: Calculate immediate charge
      Charge = New plan cost − Credit
      Charge = $40.00 − $15.00 = $25.00

    → Customer pays $25.00 now
    → Next renewal (Feb 1): $80.00/month
    ```

    ```javascript theme={null}
    await client.subscriptions.changePlan('sub_123', {
      product_id: 'prod_pro',
      quantity: 1,
      proration_billing_mode: 'prorated_immediately'
    })
    ```
  </Accordion>

  <Accordion title="Downgrade: Pro ($80) → Starter ($20) with prorated_immediately">
    ```
    Step 1: Calculate unused credit from current plan
      Unused days = 15 out of 30 days
      Credit = $80 × (15/30) = $40.00

    Step 2: Calculate prorated cost of new plan
      Remaining days = 15 out of 30 days
      New plan cost = $20 × (15/30) = $10.00

    Step 3: Calculate credit balance
      Credit = $40.00 − $10.00 = $30.00

    → No charge — $30.00 credit added to subscription
    → Credit auto-applies to future renewals
    → Next renewal (Feb 1): $20.00 − $30.00 credit = $0.00
    → Following renewal (Mar 1): $20.00 − $10.00 remaining credit = $10.00
    ```

    ```javascript theme={null}
    await client.subscriptions.changePlan('sub_123', {
      product_id: 'prod_starter',
      quantity: 1,
      proration_billing_mode: 'prorated_immediately'
    })
    ```
  </Accordion>

  <Accordion title="Upgrade: Basic ($30) → Pro ($80) with difference_immediately">
    ```
    Immediate charge = New plan price − Old plan price
                     = $80 − $30
                     = $50.00

    → Customer pays $50.00 now (regardless of cycle position)
    → Next renewal (Feb 1): $80.00/month
    ```

    ```javascript theme={null}
    await client.subscriptions.changePlan('sub_123', {
      product_id: 'prod_pro',
      quantity: 1,
      proration_billing_mode: 'difference_immediately'
    })
    ```
  </Accordion>

  <Accordion title="Downgrade: Pro ($80) → Starter ($20) with difference_immediately">
    ```
    Credit = Old plan price − New plan price
           = $80 − $20
           = $60.00

    → No charge — $60.00 credit added to subscription
    → Credit auto-applies to future renewals
    → Next renewal: $20.00 − $20.00 (from credit) = $0.00
    → Following renewal: $20.00 − $20.00 (from credit) = $0.00
    → Third renewal: $20.00 − $20.00 (from remaining credit) = $0.00
    ```

    ```javascript theme={null}
    await client.subscriptions.changePlan('sub_123', {
      product_id: 'prod_starter',
      quantity: 1,
      proration_billing_mode: 'difference_immediately'
    })
    ```
  </Accordion>

  <Accordion title="Upgrade: Basic ($30) → Pro ($80) with full_immediately">
    ```
    Immediate charge = Full new plan price = $80.00

    → Customer pays $80.00 now
    → No credit for unused time on old plan
    → Billing cycle resets to today (January 16)
    → Next renewal: February 16 at $80.00/month
    ```

    ```javascript theme={null}
    await client.subscriptions.changePlan('sub_123', {
      product_id: 'prod_pro',
      quantity: 1,
      proration_billing_mode: 'full_immediately'
    })
    ```
  </Accordion>

  <Accordion title="Mid-cycle upgrade with add-ons using prorated_immediately">
    ```
    Current: Basic plan ($30/month), no add-ons
    New: Pro plan ($80/month) + Extra Seats add-on ($10/seat × 3 seats = $30/month)
    Change on day 16 of 30 (15 days remaining)

    Step 1: Credit from current plan
      Credit = $30 × (15/30) = $15.00

    Step 2: Prorated cost of new plan + add-ons
      New plan = $80 × (15/30) = $40.00
      Add-ons = $30 × (15/30) = $15.00
      Total new = $55.00

    Step 3: Immediate charge
      Charge = $55.00 − $15.00 = $40.00

    → Customer pays $40.00 now
    → Next renewal: $80.00 + $30.00 = $110.00/month
    ```

    ```javascript theme={null}
    await client.subscriptions.changePlan('sub_123', {
      product_id: 'prod_pro',
      quantity: 1,
      proration_billing_mode: 'prorated_immediately',
      addons: [
        { addon_id: 'addon_seats', quantity: 3 }
      ]
    })
    ```
  </Accordion>
</AccordionGroup>

### 每种模式如何处理账单

```mermaid theme={null}
flowchart LR
    A[Plan Change] --> B{Proration Mode}
    B -->|prorated| C[Calculate remaining days]
    C --> D[Charge prorated diff]
    D --> E[Keep billing date]
    B -->|difference| F[Price difference]
    F -->|Upgrade| G[Charge diff]
    F -->|Downgrade| H[Add credit]
    G --> E
    H --> E
    B -->|full| I[Charge full price]
    I --> J[Reset billing date]
```

<Tip>
  选择 `prorated_immediately` 进行公平的时间计费；选择 `full_immediately` 重启账单周期；使用 `difference_immediately` 执行简单升级并在降级时自动抵用；或使用 `do_not_bill` 在不进行任何账单调整的情况下切换计划。
</Tip>

## 处理支付失败

控制计划更改支付失败时的行为，使用 `on_payment_failure` 参数。

### 支付失败模式

<Tabs>
  <Tab title="prevent_change (Recommended for critical upgrades)">
    **行为**：保持订阅在当前计划上，直到支付成功。

    * 计划更改标记为“待定”
    * 客户继续访问现有计划
    * 只有在支付成功后订阅才会移至 `active` 状态
    * 当您想确保在授予升级功能前支付成功时很有用

    ```javascript theme={null}
    await client.subscriptions.changePlan('sub_123', {
      product_id: 'prod_pro',
      quantity: 1,
      proration_billing_mode: 'prorated_immediately',
      on_payment_failure: 'prevent_change'
    });
    ```
  </Tab>

  <Tab title="apply_change (Default)">
    **行为**：不管支付结果如何立即应用计划更改。

    * 即使支付失败，计划更改也会被应用
    * 客户立即使用新计划
    * 如果支付失败，订阅可能会移至 `on_hold`
    * 适用于非关键升级或当您信任客户时

    ```javascript theme={null}
    await client.subscriptions.changePlan('sub_123', {
      product_id: 'prod_pro',
      quantity: 1,
      proration_billing_mode: 'prorated_immediately',
      on_payment_failure: 'apply_change' // This is the default
    });
    ```
  </Tab>
</Tabs>

<Info>
  如果未指定，`on_payment_failure` 参数使用您的仪表板中配置的业务级别默认设置。
</Info>

### 何时使用每种模式

| 场景         | 推荐模式             | 理由              |
| ---------- | ---------------- | --------------- |
| 升级到高级功能    | `prevent_change` | 确保在授予访问权限之前支付成功 |
| 数量增加（更多席位） | `prevent_change` | 防止未支付情况下使用      |
| 降级计划       | `apply_change`   | 客户在减少支出         |
| 可信企业客户     | `apply_change`   | 非支付风险较低         |
| 从试用到付费转换   | `prevent_change` | 关键支付时刻          |

## 处理 webhooks

通过 webhooks 跟踪订阅状态以确认计划更改和支付。

### 处理的事件类型

* `subscription.active`：订阅已激活
* `subscription.plan_changed`：订阅计划已更改（升级/降级/插件更改）
* `subscription.on_hold`：费用失败，订阅暂停
* `subscription.renewed`：续订成功
* `payment.succeeded`：计划更改或续订的支付成功
* `payment.failed`：支付失败

<Info>
  我们建议从订阅事件中驱动业务逻辑，使用支付事件进行确认和对账。
</Info>

### 验证签名并处理意图

<Tabs>
  <Tab title="Next.js Route Handler">
    ```javascript theme={null}
    import { NextRequest, NextResponse } from 'next/server';

    export async function POST(req) {
      const webhookId = req.headers.get('webhook-id');
      const webhookSignature = req.headers.get('webhook-signature');
      const webhookTimestamp = req.headers.get('webhook-timestamp');
      const secret = process.env.DODO_WEBHOOK_SECRET;

      const payload = await req.text();
      // verifySignature is a placeholder – in production, use a Standard Webhooks library
      const { valid, event } = await verifySignature(
        payload,
        { id: webhookId, signature: webhookSignature, timestamp: webhookTimestamp },
        secret
      );
      if (!valid) return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });

      switch (event.type) {
        case 'subscription.active':
          // mark subscription active in your DB
          break;
        case 'subscription.plan_changed':
          // refresh entitlements and reflect the new plan in your UI
          break;
        case 'subscription.on_hold':
          // notify user to update payment method
          break;
        case 'subscription.renewed':
          // extend access window
          break;
        case 'payment.succeeded':
          // reconcile payment for plan change
          break;
        case 'payment.failed':
          // log and alert
          break;
        default:
          // ignore unknown events
          break;
      }

      return NextResponse.json({ received: true });
    }
    ```
  </Tab>

  <Tab title="Express.js">
    ```javascript theme={null}
    import express from 'express';

    const app = express();
    app.post('/webhooks/dodo', express.raw({ type: 'application/json' }), async (req, res) => {
      const webhookId = req.header('webhook-id');
      const webhookSignature = req.header('webhook-signature');
      const webhookTimestamp = req.header('webhook-timestamp');
      const secret = process.env.DODO_WEBHOOK_SECRET;
      const payload = req.body.toString('utf8');

      const { valid, event } = await verifySignature(
        payload,
        { id: webhookId, signature: webhookSignature, timestamp: webhookTimestamp },
        secret
      );
      if (!valid) return res.status(400).send('Invalid signature');

      // handle events like above
      res.json({ received: true });
    });

    app.listen(3000);
    ```
  </Tab>
</Tabs>

<Note>
  有关详细的负载架构，请参见<a href="/developer-resources/webhooks/intents/subscription">订阅 webhook 负载</a>和<a href="/developer-resources/webhooks/intents/payment">支付 webhook 负载</a>。
</Note>

## 最佳实践

请遵循以下建议，以实现可靠的订阅计划更改：

### 计划更改策略

* **彻底测试**：在生产之前始终在测试模式下测试计划更改
* **仔细选择比例方式**：选择符合您业务模型的比例方式
* **优雅地处理失败**：实现适当的错误处理和重试逻辑
* **监控成功率**：跟踪计划更改成功/失败率并调查问题

### Webhook 实施

* **验证签名**：始终验证 webhook 签名以确保真实性
* **实现幂等性**：优雅地处理重复的 webhook 事件
* **异步处理**：不要用繁重的操作阻塞 webhook 响应
* **记录一切**：保持详细日志以进行调试和审计

### 用户体验

* **清晰沟通**：告知客户账单变化和时间
* **提供确认**：为成功的计划更改发送电子邮件确认
* **处理边缘案例**：考虑试用期、比例调整和支付失败
* **立即更新 UI**：在您的应用程序界面中反映计划更改

## 常见问题及解决方案

解决订阅计划更改中遇到的典型问题：

<AccordionGroup>
  <Accordion title="Charge created but subscription not updated">
    **症状**：API 调用成功但订阅仍保留在旧计划

    **常见原因**：

    * Webhook 处理失败或延迟
    * 在接收到 webhooks 后应用程序状态未更新
    * 在状态更新期间出现数据库事务问题

    **解决方案**：

    * 实现强大的 webhook 处理和重试逻辑
    * 对状态更新使用幂等操作
    * 添加监控以检测和提醒遗漏的 webhook 事件
    * 验证 webhook 终端是否可访问并正确响应
  </Accordion>

  <Accordion title="Credits not applied after downgrade">
    **症状**：客户降级但未看到抵用余额

    **常见原因**：

    * 比例方式期望：降级使用 `difference_immediately` 抵用全额计划价格差异，而 `prorated_immediately` 根据周期内的剩余时间创建比例抵用金
    * 抵用金是订阅特定的，不会在订阅之间转移
    * 客户仪表板上未显示抵用余额

    **解决方案**：

    * 对降级使用 `difference_immediately` ，当您希望自动抵用时
    * 向客户解释抵用适用于同一订阅的未来续订
    * 实现客户门户显示抵用余额
    * 检查下一个发票预览以查看应用的抵用金
  </Accordion>

  <Accordion title="Webhook signature verification fails">
    **症状**：由于签名无效，Webhook 事件被拒绝

    **常见原因**：

    * 错误的 webhook 秘钥
    * 在签名验证前修改了原始请求主体
    * 错误的签名验证算法

    **解决方案**：

    * 验证您使用的 `DODO_WEBHOOK_SECRET` 是否正确，并来自仪表板
    * 在任何 JSON 解析中间件之前阅读原始请求主体
    * 使用适用于您的平台的标准 webhook 验证库
    * 在开发环境中测试 webhook 签名验证
  </Accordion>

  <Accordion title="Plan change fails with 422 error">
    **症状**：API 返回 422 不可处理的实体错误

    **常见原因**：

    * 无效的订阅 ID 或产品 ID
    * 订阅未处于活动状态
    * 缺少必需参数
    * 产品不可用于计划更改

    **解决方案**：

    * 验证订阅是否存在且处于活动状态
    * 检查产品 ID 是否有效且可用
    * 确保提供所有必需的参数
    * 查看 API 文档以获取参数要求
  </Accordion>

  <Accordion title="Immediate charge fails during plan change">
    **症状**：计划更改启动但即时费用失败

    **常见原因**：

    * 客户的支付方式上的资金不足
    * 支付方式过期或无效
    * 银行拒绝交易
    * 欺诈检测阻止了收费

    **解决方案**：

    * 适当地处理 `payment.failed` webhooks 事件
    * 通知客户更新支付方式
    * 为临时故障实现重试逻辑
    * 考虑允许在即时费用失败的情况下进行计划更改
  </Accordion>

  <Accordion title="Subscription on hold after plan change">
    **症状**：计划更改费用失败，订阅移至 `on_hold` 状态

    **发生了什么**：
    当计划更改费用失败时，订阅会自动进入 `on_hold` 状态。直到更新支付方式，订阅才会自动续订。

    **解决方案**：更新支付方式以重新激活订阅

    要在计划更改失败后从 `on_hold` 状态重新激活订阅：

    1. **使用更新支付方式 API 更新支付方式**
    2. **自动收费创建**：API 自动为剩余款项创建费用
    3. **生成发票**：为费用生成发票
    4. **支付处理**：使用新支付方式处理支付
    5. **重新激活**：在支付成功后，将订阅重新激活到 `active` 状态

    <CodeGroup>
      ```javascript Node.js theme={null}
      // Reactivate subscription from on_hold after failed plan change
      async function reactivateAfterFailedPlanChange(subscriptionId) {
        // Update payment method - automatically creates charge for remaining dues
        const response = await client.subscriptions.updatePaymentMethod(subscriptionId, {
          type: 'new',
          return_url: 'https://example.com/return'
        });
        
        if (response.payment_id) {
          console.log('Charge created for remaining dues:', response.payment_id);
          console.log('Payment link:', response.payment_link);
          
          // Redirect customer to payment_link to complete payment
          // Monitor webhooks for:
          // 1. payment.succeeded - charge succeeded
          // 2. subscription.active - subscription reactivated
        }
        
        return response;
      }

      // Or use existing payment method if available
      async function reactivateWithExistingPaymentMethod(subscriptionId, paymentMethodId) {
        const response = await client.subscriptions.updatePaymentMethod(subscriptionId, {
          type: 'existing',
          payment_method_id: paymentMethodId
        });
        
        // Monitor webhooks for payment.succeeded and subscription.active
        return response;
      }
      ```

      ```python Python theme={null}
      # Reactivate subscription from on_hold after failed plan change
      def reactivate_after_failed_plan_change(subscription_id):
          # Update payment method - automatically creates charge for remaining dues
          response = client.subscriptions.update_payment_method(
              subscription_id=subscription_id,
              type="new",
              return_url="https://example.com/return"
          )
          
          if response.payment_id:
              print("Charge created for remaining dues:", response.payment_id)
              print("Payment link:", response.payment_link)
              
              # Redirect customer to payment_link to complete payment
              # Monitor webhooks for:
              # 1. payment.succeeded - charge succeeded
              # 2. subscription.active - subscription reactivated
          
          return response

      # Or use existing payment method if available
      def reactivate_with_existing_payment_method(subscription_id, payment_method_id):
          response = client.subscriptions.update_payment_method(
              subscription_id=subscription_id,
              type="existing",
              payment_method_id=payment_method_id
          )
          
          # Monitor webhooks for payment.succeeded and subscription.active
          return response
      ```
    </CodeGroup>

    **要监控的 Webhook 事件**：

    * `subscription.on_hold`：订阅已暂停（在计划更改费用失败时接收）
    * `payment.succeeded`：剩余费用支付成功（在更新支付方式后）
    * `subscription.active`：支付成功后订阅重新激活

    **最佳做法**：

    * 在计划更改费用失败时立即通知客户
    * 提供有关如何更新支付方式的明确说明
    * 监控 webhook 事件以跟踪重新激活状态
    * 考虑为临时支付故障实施自动重试逻辑

    <Card title="Update Payment Method API Reference" icon="code" href="/api-reference/subscriptions/update-payment-method">
      查看完整的 API 文档以更新支付方式并重新激活订阅。
    </Card>
  </Accordion>
</AccordionGroup>

## 测试您的实现

按照以下步骤彻底测试您的订阅计划更改实现：

<Steps>
  <Step title="Set up test environment">
    * 使用测试 API 密钥和测试产品
    * 创建具有不同计划类型的测试订阅
    * 配置测试 webhook 终端
    * 设置监控和日志记录
  </Step>

  <Step title="Test different proration modes">
    * 使用各种账单周期位置测试 `prorated_immediately`
    * 为升级和降级测试 `difference_immediately`
    * 测试 `full_immediately` 以重置账单周期
    * 测试 `do_not_bill` 以进行无费用/无抵用的计划切换
    * 验证抵用计算是否正确
  </Step>

  <Step title="Test webhook handling">
    * 验证是否收到了所有相关的 webhook 事件
    * 测试 webhook 签名验证
    * 优雅地处理重复的 webhook 事件
    * 测试 webhook 处理失败情况
  </Step>

  <Step title="Test error scenarios">
    * 测试无效的订阅 ID
    * 测试过期的支付方式
    * 测试网络故障和超时
    * 测试资金不足
  </Step>

  <Step title="Monitor in production">
    * 为失败的计划更改设置警报
    * 监控 webhook 处理时间
    * 跟踪计划更改成功率
    * 查看客户支持票据以解决计划更改问题
  </Step>
</Steps>

## 错误处理

在您的实现中优雅地处理常见的 API 错误：

### HTTP 状态代码

<AccordionGroup>
  <Accordion title="200 OK">
    计划更改请求已成功处理。订阅正在更新，支付处理已经开始。
  </Accordion>

  <Accordion title="400 Bad Request">
    请求参数无效。检查所有必填字段是否已提供并正确格式化。
  </Accordion>

  <Accordion title="401 Unauthorized">
    无效或缺失的 API 密钥。请验证您的 `DODO_PAYMENTS_API_KEY` 是否正确且拥有适当的权限。
  </Accordion>

  <Accordion title="404 Not Found">
    订阅 ID 未找到或不属于您的帐户。
  </Accordion>

  <Accordion title="422 Unprocessable Entity">
    订阅无法更改（例如，已取消，产品不可用等）。
  </Accordion>

  <Accordion title="500 Internal Server Error">
    服务器发生错误。稍作延迟后重试请求。
  </Accordion>
</AccordionGroup>

### 错误响应格式

```json theme={null}
{
  "error": {
    "code": "subscription_not_found",
    "message": "The subscription with ID 'sub_123' was not found",
    "details": {
      "subscription_id": "sub_123"
    }
  }
}
```

## 下一步

* 查看 <a href="/api-reference/subscriptions/change-plan">更改计划 API</a>
* 浏览<a href="/features/credit-based-billing">基于抵用的结算</a>
* 实现 `subscription.on_hold` 警报
* 查看我们的<a href="/developer-resources/webhooks">Webhook 集成指南</a>

<AccordionGroup>
  <Accordion title="200 OK">
    计划变更请求已成功处理。订阅正在更新，支付处理已开始。
  </Accordion>

  <Accordion title="400 Bad Request">
    请求参数无效。检查是否提供了所有必填字段，并且格式正确。
  </Accordion>

  <Accordion title="401 Unauthorized">
    API 密钥无效或缺失。验证你的 `DODO_PAYMENTS_API_KEY` 是否正确且具有适当的权限。
  </Accordion>

  <Accordion title="404 Not Found">
    未找到订阅 ID 或不属于你的账户。
  </Accordion>

  <Accordion title="422 Unprocessable Entity">
    无法更改订阅（例如，已取消、产品不可用等）。
  </Accordion>

  <Accordion title="500 Internal Server Error">
    服务器发生错误。请稍后重试请求。
  </Accordion>
</AccordionGroup>

### 错误响应格式

```json theme={null}
{
  "error": {
    "code": "subscription_not_found",
    "message": "The subscription with ID 'sub_123' was not found",
    "details": {
      "subscription_id": "sub_123"
    }
  }
}
```

## 后续步骤

* 查看 <a href="/api-reference/subscriptions/change-plan">更改计划 API</a>
* 探索 <a href="/features/credit-based-billing">基于信用的计费</a>
* 为 `subscription.on_hold` 实现警报
* 查看我们的 <a href="/developer-resources/webhooks">Webhook 集成指南</a>
