> ## 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>
      Jumlah unit untuk rencana baru (untuk produk berbasis kursi).
    </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">
      Kode diskon **tambahan** opsional untuk diterapkan ke paket baru (maks 20, diterapkan dalam urutan array). Perilaku tergantung pada apa yang Anda berikan:

      * **Tidak disediakan / `null`** — diskon yang ada dengan `preserve_on_plan_change=true` dipertahankan jika berlaku untuk produk baru.
      * **`[]` (array kosong)** — menghapus semua diskon yang ada dari langganan.
      * **`["CODE_A", "CODE_B", ...]`** — menggantikan diskon yang ada dengan set baru ini.
    </ParamField>

    <ParamField path="discount_code" type="string" deprecated>
      **Dihapus** — lebih memilih `discount_codes` untuk integrasi baru. Bidang ini masih berfungsi untuk kompatibilitas ke belakang, tetapi tidak dapat digabungkan dengan `discount_codes` dalam permintaan yang sama.
    </ParamField>

    <ParamField path="effective_at" type="string" default="immediately">
      Kapan mengajukan perubahan paket:

      * `immediately` (default): Terapkan segera perubahan paket
      * `next_billing_date`: Jadwalkan perubahan untuk tanggal penagihan berikutnya. Pelanggan mempertahankan paket mereka saat ini hingga periode penagihan berakhir.

      Gunakan `next_billing_date` untuk penurunan versi sehingga pelanggan tetap mendapatkan manfaat paket mereka saat ini hingga akhir periode penagihan.
    </ParamField>
  </Step>

  <Step title="Handle Webhook Events">
    Mengatur penanganan webhook untuk melacak hasil perubahan paket:

    * `subscription.active`: Perubahan paket berhasil, langganan diperbarui
    * `subscription.plan_changed`: Paket langganan berubah (peningkatan/penurunan versi/pembaruan addon)
    * `subscription.on_hold`: Biaya perubahan paket gagal, langganan dijeda
    * `payment.succeeded`: Biaya langsung untuk perubahan paket berhasil
    * `payment.failed`: Biaya langsung gagal

    <Warning>
      Selalu verifikasi tanda tangan webhook dan terapkan pemrosesan acara idempoten.
    </Warning>
  </Step>

  <Step title="Update Your Application State">
    Berdasarkan acara webhook, perbarui aplikasi Anda:

    * Berikan/cabut fitur berdasarkan paket baru
    * Perbarui dasbor pelanggan dengan detail paket baru
    * Kirim email konfirmasi tentang perubahan paket
    * Catat perubahan penagihan untuk tujuan audit
  </Step>

  <Step title="Test and Monitor">
    Uji implementasi Anda secara menyeluruh:

    * Uji semua mode proratasi dengan berbeda skenario
    * Verifikasi penanganan webhook berfungsi dengan benar
    * Monitor tingkat keberhasilan perubahan paket
    * Siapkan peringatan untuk perubahan paket yang gagal

    <Check>
      Implementasi perubahan paket langganan Anda sekarang siap untuk digunakan produksi.
    </Check>
  </Step>
</Steps>

## Pratinjau Perubahan Paket

Sebelum melakukan perubahan paket, gunakan Preview API untuk menunjukkan kepada pelanggan berapa tepatnya yang akan mereka bayar:

<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>
  Gunakan API pratinjau untuk membuat dialog konfirmasi yang menunjukkan kepada pelanggan jumlah tepat yang akan mereka bayar sebelum mereka mengonfirmasi perubahan paket.
</Tip>

## API Perubahan Paket

Gunakan API Perubahan Paket untuk memodifikasi produk, kuantitas, dan perilaku proratasi untuk langganan aktif.

### Contoh cepat memulai

<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>
  Fields seperti <code>invoice\_id</code> dan <code>payment\_id</code> hanya dikembalikan ketika biaya langsung dan/atau faktur dibuat selama perubahan paket. Selalu andalkan acara webhook (misalnya, <code>payment.succeeded</code>, <code>subscription.plan\_changed</code>) untuk mengonfirmasi hasil.
</Note>

<Warning>
  Jika biaya langsung gagal, langganan mungkin berpindah ke `subscription.on_hold` hingga pembayaran berhasil.
</Warning>

## Mengelola Addon

Saat mengubah paket langganan, Anda juga dapat memodifikasi addon:

```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>
  Addon termasuk dalam perhitungan proratasi dan akan dikenakan biaya sesuai dengan mode proratasi yang dipilih.
</Info>

## Menerapkan Kode Diskon

Anda dapat menerapkan satu atau lebih **kode diskon tambahan** saat mengubah paket langganan (maks 20, diterapkan dalam urutan array). Ini berguna untuk menawarkan harga promosi pada peningkatan atau migrasi.

<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>

### Perilaku diskon pada perubahan paket

| Nilai `discount_codes`      | Perilaku                                                                                                            |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| Tidak disediakan / `null`   | Diskon yang ada dengan `preserve_on_plan_change=true` secara otomatis dipertahankan jika berlaku untuk produk baru. |
| `[]` (array kosong)         | **Semua** diskon yang ada dihapus dari langganan.                                                                   |
| `["CODE_A", "CODE_B", ...]` | Menggantikan diskon yang ada dengan set tambahan ini, divalidasi dan diterapkan dalam urutan array.                 |

<Info>
  Bidang tunggal `discount_code` di endpoint ini **dihapus** tetapi masih berfungsi untuk kompatibilitas ke belakang — integrasi yang ada tidak perlu mengubah segera. Tidak dapat digabungkan dengan `discount_codes` dalam permintaan yang sama. Migrasikan ke bentuk array saat nyaman.
</Info>

<Tip>
  Gunakan [Preview Plan Change API](/api-reference/subscriptions/preview-change-plan) dengan `discount_codes` untuk menunjukkan kepada pelanggan berapa banyak yang akan mereka hemat sebelum mengonfirmasi perubahan paket.
</Tip>

## Mode Prorasi

Pilih cara untuk menagih pelanggan saat mengubah paket:

#### `prorated_immediately`

* Menagih perbedaan parsial dalam siklus saat ini
* Jika dalam percobaan, langsung menagih dan beralih ke paket baru sekarang
* Penurunan versi: dapat menghasilkan kredit prorated yang diterapkan ke pembaruan mendatang

#### `full_immediately`

* Menagih jumlah penuh dari paket baru secara langsung
* Mengabaikan waktu yang tersisa dari paket lama

<Info>
  Kredit yang dibuat oleh penurunan versi menggunakan <code>difference\_immediately</code> diterapkan pada ruang lingkup langganan dan berbeda dari hak <a href="/features/credit-based-billing">Penagihan Berbasis Kredit</a>. Mereka secara otomatis diaplikasikan pada pembaruan di masa mendatang dari langganan yang sama dan tidak dapat dipindahtangankan antara langganan.
</Info>

#### `difference_immediately`

* Peningkatan: langsung menagih perbedaan harga antara paket lama dan baru
* Penurunan versi: tambahkan nilai yang tersisa sebagai kredit internal pada langganan dan terapkan secara otomatis saat pembaruan

#### `do_not_bill`

* Tidak ada biaya atau kredit yang dihitung
* Pelanggan beralih ke paket baru segera tanpa penyesuaian penagihan
* Siklus penagihan tetap tidak berubah
* Terbaik untuk migrasi sopan santun, perpindahan paket gratis, atau menyerap perbedaan biaya

| Fitur                  | `prorated_immediately`                    | `difference_immediately`               | `full_immediately`                    | `do_not_bill`                            |
| ---------------------- | ----------------------------------------- | -------------------------------------- | ------------------------------------- | ---------------------------------------- |
| **Biaya upgrade**      | Perbedaan sesuai prorated untuk sisa hari | Perbedaan harga penuh antara paket     | Harga penuh paket baru                | Tidak ada biaya                          |
| **Kredit downgrade**   | Kredit prorated untuk sisa hari           | Perbedaan harga penuh sebagai kredit   | Tidak ada kredit                      | Tidak ada kredit                         |
| **Siklus penagihan**   | Direset menjadi hari ini                  | Direset menjadi hari ini               | Direset menjadi hari ini              | Tidak berubah                            |
| **Perilaku percobaan** | Akhiri percobaan, kenakan biaya segera    | Akhiri percobaan, kenakan biaya segera | Akhiri percobaan, kenakan biaya penuh | Akhiri percobaan, tidak ada biaya        |
| **Terbaik untuk**      | Penagihan berbasis waktu adil             | Matematika upgrade/downgrade sederhana | Mereset siklus penagihan              | Migrasi gratis atau peralihan bersahabat |
| **Kompleksitas**       | Sedang (penghitungan hari)                | Rendah (pengurangan sederhana)         | Rendah (biaya penuh)                  | Tidak ada                                |

```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]
```

### Contoh skenario

Gunakan angka kanonik ini secara konsisten:

* Paket saat ini: **Basic** di **\$30/bulan**
* Target peningkatan: **Pro** di **\$80/bulan**
* Target penurunan (dari Pro): **Starter** di **\$20/bulan**
* Siklus penagihan: **30 hari**, dimulai pada **1 Januari**
* Perubahan paket terjadi pada **16 Januari** (15 hari tersisa, 15 hari digunakan)

<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
    → Billing cycle resets to today (January 16)
    → Next renewal (Feb 15): $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
    → Billing cycle resets to today (January 16)
    → Credit auto-applies to future renewals
    → Next renewal (Feb 15): $20.00 − $20.00 (from credit) = $0.00 ($10.00 credit remaining)
    → Following renewal (Mar 15): $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)
    → Billing cycle resets to today (January 16)
    → Next renewal (Feb 15): $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>

### Bagaimana setiap mode memproses penagihan

```mermaid theme={null}
flowchart LR
    A[Plan Change] --> B{Proration Mode}
    B -->|prorated| C[Calculate remaining days]
    C --> D[Charge prorated diff]
    D --> E[Reset 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 --> E
    B -->|do_not_bill| J[No charge / no credit]
    J --> K[Keep billing date]
```

<Tip>
  Pilih `prorated_immediately` untuk perhitungan waktu yang adil; pilih `full_immediately` untuk memulai ulang penagihan; gunakan `difference_immediately` untuk peningkatan sederhana dan kredit otomatis pada penurunan versi; atau gunakan `do_not_bill` untuk mengganti paket tanpa penyesuaian penagihan.
</Tip>

## Penanganan Kegagalan Pembayaran

Kontrol apa yang terjadi ketika pembayaran perubahan paket gagal menggunakan parameter `on_payment_failure`.

### Mode Kegagalan Pembayaran

<Tabs>
  <Tab title="prevent_change (Recommended for critical upgrades)">
    **Perilaku**: Pertahankan langganan pada paket saat ini hingga pembayaran berhasil.

    * Perubahan paket ditandai sebagai "pending"
    * Pelanggan mempertahankan akses ke paket mereka saat ini
    * Langganan berpindah ke status `active` hanya setelah pembayaran berhasil
    * Berguna ketika Anda ingin memastikan pembayaran sebelum memberikan fitur peningkatan

    ```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)">
    **Perilaku**: Terapkan perubahan paket segera terlepas dari hasil pembayaran.

    * Perubahan paket diterapkan bahkan jika pembayaran gagal
    * Pelanggan mendapatkan akses ke paket baru segera
    * Langganan dapat berpindah ke `on_hold` jika pembayaran gagal
    * Baik untuk pembaruan yang tidak kritis atau ketika Anda mempercayai pelanggan

    ```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>
  Jika tidak ditentukan, parameter `on_payment_failure` menggunakan pengaturan default tingkat bisnis Anda yang dikonfigurasi di dasbor.
</Info>

### Kapan Menggunakan Setiap Mode

| Skenario                                | Mode yang Direkomendasikan | Alasan                                       |
| --------------------------------------- | -------------------------- | -------------------------------------------- |
| Meningkatkan ke fitur premium           | `prevent_change`           | Pastikan pembayaran sebelum memberikan akses |
| Peningkatan jumlah (lebih banyak kursi) | `prevent_change`           | Mencegah penggunaan tanpa pembayaran         |
| Menurunkan paket                        | `apply_change`             | Pelanggan mengurangi pengeluaran             |
| Pelanggan perusahaan terpercaya         | `apply_change`             | Risiko lebih rendah dari ketidakpembayaran   |
| Konversi percobaan ke berbayar          | `prevent_change`           | Momen pembayaran kritis                      |

## Default Bisnis & Koleksi

Daripada memasukkan parameter prorated pada setiap perubahan paket, Anda dapat menetapkan **perilaku default untuk upgrade & downgrade** sekali di tingkat bisnis. Default ini berlaku untuk semua perubahan paket portal pelanggan, dan dapat **ditimpa sesuai koleksi produk**.

Ada default terpisah untuk upgrade dan downgrade:

| Pengaturan                               | Bidang (upgrade / downgrade)                                                | Default (upgrade)        | Default (downgrade)      |
| ---------------------------------------- | --------------------------------------------------------------------------- | ------------------------ | ------------------------ |
| Kapan paket baru dimulai                 | `effective_at_on_upgrade` / `effective_at_on_downgrade`                     | `immediately`            | `next_billing_date`      |
| Bagaimana cara pelanggan dikenakan biaya | `proration_billing_mode_on_upgrade` / `proration_billing_mode_on_downgrade` | `difference_immediately` | `difference_immediately` |
| Jika pembayaran pelanggan gagal          | `on_payment_failure`                                                        | `apply_change`           | `apply_change`           |

Konfigurasi default bisnis di bawah **Pengaturan → Langganan**, dan mengesampingkan koleksi pada setiap koleksi produk. Setiap bidang koleksi bersifat independen — biarkan tidak diatur untuk **mewarisi dari default bisnis**, atau tetapkan nilai untuk menimpanya hanya untuk koleksi tersebut.

### Urutan Resolusi

Untuk setiap perubahan paket, setiap pengaturan diselesaikan dalam urutan ini:

```
per-request value (Change Plan API) → collection field (if set) → business field → system default
```

<Info>
  Nilai yang diteruskan secara eksplisit ke [API Perubahan Paket](/api-reference/subscriptions/change-plan) selalu menang. Default bisnis dan koleksi hanya berlaku ketika tidak ada nilai eksplisit yang disediakan — yang merupakan kasus untuk semua perubahan paket yang dimulai dari portal pelanggan.
</Info>

<Tip>
  Pengaturan umum: biarkan upgrade `immediately` + `difference_immediately` sehingga pelanggan membayar perbedaannya dan mendapatkan akses segera, dan biarkan downgrade pada `next_billing_date` sehingga pelanggan mempertahankan paket mereka saat ini hingga siklus berakhir.
</Tip>

## Menangani Webhooks

Lacak status langganan melalui webhooks untuk mengonfirmasi perubahan paket dan pembayaran.

### Jenis Peristiwa untuk Ditangani

* `subscription.active`: langganan diaktifkan
* `subscription.plan_changed`: paket langganan berubah (perubahan upgrade/downgrade/addon)
* `subscription.on_hold`: pembayaran gagal, langganan dihentikan
* `subscription.renewed`: perpanjangan berhasil
* `payment.succeeded`: pembayaran untuk perubahan paket atau perpanjangan berhasil
* `payment.failed`: pembayaran gagal

<Info>
  Kami merekomendasikan untuk mendorong logika bisnis dari peristiwa langganan dan menggunakan peristiwa pembayaran untuk konfirmasi dan rekonsiliasi.
</Info>

### Verifikasi Tanda Tangan dan Tangani Intent

<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>
  Untuk skema payload terperinci, lihat <a href="/developer-resources/webhooks/intents/subscription">Payload webhook Langganan</a> dan <a href="/developer-resources/webhooks/intents/payment">Payload webhook Pembayaran</a>.
</Note>

## Praktik Terbaik

Ikuti rekomendasi ini untuk perubahan paket langganan yang andal:

### Strategi Perubahan Paket

* **Uji secara menyeluruh**: Selalu uji perubahan paket dalam mode uji sebelum produksi
* **Pilih proration dengan hati-hati**: Pilih mode proration yang paling sesuai dengan model bisnis Anda
* **Tangani kegagalan dengan baik**: Terapkan penanganan kesalahan dan logika coba ulang yang tepat
* **Pantau tingkat keberhasilan**: Lacak tingkat keberhasilan/kegagalan perubahan paket dan selidiki masalah

### Implementasi Webhook

* **Verifikasi tanda tangan**: Selalu validasi tanda tangan webhook untuk memastikan keaslian
* **Terapkan idempotensi**: Tangani duplikasi peristiwa webhook dengan baik
* **Proses secara asinkron**: Jangan memblokir respon webhook dengan operasi berat
* **Catat semuanya**: Pertahankan catatan rinci untuk debugging dan audit

### Pengalaman Pengguna

* **Komunikasikan dengan jelas**: Informasikan kepada pelanggan tentang perubahan dan waktu penagihan
* **Berikan konfirmasi**: Kirim konfirmasi email untuk perubahan paket yang berhasil
* **Tangani kasus tepi**: Pertimbangkan periode percobaan, perhitungan prorated, dan pembayaran yang gagal
* **Perbarui UI segera**: Refleksikan perubahan paket di antarmuka aplikasi Anda

## Masalah Umum dan Solusi

Selesaikan masalah-masalah umum yang dihadapi selama perubahan paket langganan:

<AccordionGroup>
  <Accordion title="Charge created but subscription not updated">
    **Gejala**: Panggilan API berhasil tetapi langganan tetap pada paket lama

    **Penyebab umum**:

    * Pemrosesan webhook gagal atau tertunda
    * Status aplikasi tidak diperbarui setelah menerima webhooks
    * Masalah transaksi database selama pembaruan status

    **Solusi**:

    * Terapkan penanganan webhook yang andal dengan logika coba ulang
    * Gunakan operasi idempoten untuk pembaruan status
    * Tambahkan pemantauan untuk mendeteksi dan memberi tahu jika ada peristiwa webhook yang terlewatkan
    * Verifikasi endpoint webhook dapat diakses dan merespon dengan benar
  </Accordion>

  <Accordion title="Credits not applied after downgrade">
    **Gejala**: Pelanggan melakukan downgrade tetapi tidak melihat saldo kredit

    **Penyebab umum**:

    * Ekspektasi mode proration: Downgrade memberi kredit perbedaan harga paket penuh dengan `difference_immediately`, sedangkan `prorated_immediately` membuat kredit prorated berdasarkan sisa waktu dalam siklus
    * Kredit bersifat spesifik untuk langganan dan tidak berpindah antar langganan
    * Saldo kredit tidak terlihat di dasbor pelanggan

    **Solusi**:

    * Gunakan `difference_immediately` untuk downgrade jika Anda menginginkan kredit otomatis
    * Jelaskan kepada pelanggan bahwa kredit berlaku untuk perpanjangan langganan yang sama di masa depan
    * Terapkan portal pelanggan untuk menunjukkan saldo kredit
    * Periksa pratinjau faktur berikutnya untuk melihat kredit yang diterapkan
  </Accordion>

  <Accordion title="Webhook signature verification fails">
    **Gejala**: Peristiwa webhook ditolak karena tanda tangan tidak valid

    **Penyebab umum**:

    * Kunci rahasia webhook salah
    * Badan permintaan mentah diubah sebelum verifikasi tanda tangan
    * Algoritma verifikasi tanda tangan salah

    **Solusi**:

    * Verifikasi Anda menggunakan `DODO_WEBHOOK_SECRET` yang benar dari dasbor
    * Baca badan permintaan mentah sebelum middleware parsing JSON
    * Gunakan perpustakaan verifikasi webhook standar untuk platform Anda
    * Uji verifikasi tanda tangan webhook di lingkungan pengembangan
  </Accordion>

  <Accordion title="Plan change fails with 422 error">
    **Gejala**: API mengembalikan kesalahan 422 Entitas Tidak Dapat Diproses

    **Penyebab umum**:

    * ID langganan atau produk tidak valid
    * Langganan tidak dalam keadaan aktif
    * Parameter yang diperlukan hilang
    * Produk tidak tersedia untuk perubahan paket

    **Solusi**:

    * Verifikasi langganan ada dan aktif
    * Periksa ID produk valid dan tersedia
    * Pastikan semua parameter yang diperlukan disediakan
    * Tinjau dokumentasi API untuk persyaratan parameter
  </Accordion>

  <Accordion title="Immediate charge fails during plan change">
    **Gejala**: Perubahan paket dimulai tetapi biaya segera gagal

    **Penyebab umum**:

    * Dana tidak mencukupi pada metode pembayaran pelanggan
    * Metode pembayaran kadaluwarsa atau tidak valid
    * Bank menolak transaksi
    * Deteksi penipuan memblokir biaya

    **Solusi**:

    * Tangani acara webhook `payment.failed` dengan tepat
    * Beritahu pelanggan untuk memperbarui metode pembayaran
    * Terapkan logika coba ulang untuk kegagalan sementara
    * Pertimbangkan mengizinkan perubahan paket dengan biaya langsung yang gagal
  </Accordion>

  <Accordion title="Subscription on hold after plan change">
    **Gejala**: Biaya perubahan paket gagal dan langganan pindah ke status `on_hold`

    **Apa yang terjadi**:
    Ketika biaya perubahan paket gagal, langganan secara otomatis ditempatkan di status `on_hold`. Langganan tidak akan diperpanjang secara otomatis hingga metode pembayaran diperbarui.

    **Solusi**: Perbarui metode pembayaran untuk mengaktifkan kembali langganan

    Untuk mengaktifkan kembali langganan dari status `on_hold` setelah perubahan paket gagal:

    1. **Perbarui metode pembayaran** menggunakan API Perbarui Metode Pembayaran
    2. **Pembuatan biaya otomatis**: API secara otomatis membuat biaya untuk sisa tagihan
    3. **Pembuatan faktur**: Faktur dihasilkan untuk biaya tersebut
    4. **Pemrosesan pembayaran**: Pembayaran diproses menggunakan metode pembayaran baru
    5. **Reaktivasi**: Setelah pembayaran berhasil, langganan diaktifkan kembali ke status `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>

    **Acara webhook untuk dipantau**:

    * `subscription.on_hold`: Langganan ditangguhkan (diterima saat biaya perubahan paket gagal)
    * `payment.succeeded`: Pembayaran untuk sisa tagihan berhasil (setelah memperbarui metode pembayaran)
    * `subscription.active`: Langganan diaktifkan kembali setelah pembayaran berhasil

    **Praktik terbaik**:

    * Beritahu pelanggan segera ketika biaya perubahan paket gagal
    * Berikan instruksi yang jelas tentang cara memperbarui metode pembayaran mereka
    * Pantau acara webhook untuk melacak status reaktivasi
    * Pertimbangkan mengimplementasikan logika coba ulang otomatis untuk kegagalan pembayaran sementara

    <Card title="Update Payment Method API Reference" icon="code" href="/api-reference/subscriptions/update-payment-method">
      Lihat dokumentasi API lengkap untuk memperbarui metode pembayaran dan mengaktifkan kembali langganan.
    </Card>
  </Accordion>
</AccordionGroup>

## Menguji Implementasi Anda

Ikuti langkah-langkah ini untuk menguji implementasi perubahan paket langganan Anda secara menyeluruh:

<Steps>
  <Step title="Set up test environment">
    * Gunakan kunci API uji dan produk uji
    * Buat langganan uji dengan berbagai jenis paket
    * Konfigurasikan endpoint webhook uji
    * Siapkan pemantauan dan pencatatan
  </Step>

  <Step title="Test different proration modes">
    * Uji `prorated_immediately` dengan berbagai posisi siklus penagihan
    * Uji `difference_immediately` untuk upgrade dan downgrade
    * Uji `full_immediately` untuk mereset siklus penagihan
    * Uji `do_not_bill` untuk peralihan paket tanpa biaya/kredit
    * Verifikasi perhitungan kredit benar
  </Step>

  <Step title="Test webhook handling">
    * Verifikasi semua peristiwa webhook terkait diterima
    * Uji verifikasi tanda tangan webhook
    * Tangani duplikasi peristiwa webhook dengan baik
    * Uji skenario kegagalan pemrosesan webhook
  </Step>

  <Step title="Test error scenarios">
    * Uji dengan ID langganan tidak valid
    * Uji dengan metode pembayaran kadaluarsa
    * Uji kegagalan jaringan dan waktu habis
    * Uji dengan dana tidak mencukupi
  </Step>

  <Step title="Monitor in production">
    * Siapkan peringatan untuk perubahan paket yang gagal
    * Pantau waktu pemrosesan webhook
    * Lacak tingkat keberhasilan perubahan paket
    * Tinjau tiket dukungan pelanggan untuk masalah perubahan paket
  </Step>
</Steps>

## Penanganan Kesalahan

Tangani kesalahan API umum dengan baik dalam implementasi Anda:

### Kode Status HTTP

<AccordionGroup>
  <Accordion title="200 OK">
    Permintaan perubahan paket diproses dengan sukses. Langganan sedang diperbarui dan pemrosesan pembayaran telah dimulai.
  </Accordion>

  <Accordion title="400 Bad Request">
    Parameter permintaan tidak valid. Periksa bahwa semua bidang yang diperlukan disediakan dan diformat dengan benar.
  </Accordion>

  <Accordion title="401 Unauthorized">
    Kunci API tidak valid atau hilang. Verifikasi `DODO_PAYMENTS_API_KEY` Anda benar dan memiliki izin yang benar.
  </Accordion>

  <Accordion title="404 Not Found">
    ID langganan tidak ditemukan atau tidak milik akun Anda.
  </Accordion>

  <Accordion title="422 Unprocessable Entity">
    Langganan tidak dapat diubah (misal, sudah dibatalkan, produk tidak tersedia, dll.).
  </Accordion>

  <Accordion title="500 Internal Server Error">
    Terjadi kesalahan server. Coba kembali permintaan setelah jeda singkat.
  </Accordion>
</AccordionGroup>

### Format Respons Kesalahan

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

## Langkah berikutnya

* Tinjau <a href="/api-reference/subscriptions/change-plan">API Perubahan Paket</a>
* Eksplorasi <a href="/features/credit-based-billing">Penagihan Berbasis Kredit</a>
* Terapkan peringatan untuk `subscription.on_hold`
* Lihat <a href="/developer-resources/webhooks">Panduan Integrasi Webhook kami</a>
