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

# Upsells & Downsells

> Tingkatkan pendapatan dengan pembelian sekali klik menggunakan metode pembayaran yang tersimpan untuk penawaran pasca-pembelian, peningkatan langganan, dan cross-sell.

<Info>
  Upsells dan downsells memungkinkan Anda menawarkan produk tambahan atau perubahan paket kepada pelanggan menggunakan metode pembayaran yang tersimpan. Ini memungkinkan pembelian sekali klik yang melewati pengumpulan pembayaran, secara dramatis meningkatkan rasio konversi.
</Info>

<CardGroup cols={3}>
  <Card title="Post-Purchase Upsells" icon="cart-plus">
    Tawarkan produk pelengkap segera setelah checkout dengan pembelian sekali klik.
  </Card>

  <Card title="Subscription Upgrades" icon="arrow-up">
    Pindahkan pelanggan ke tingkat yang lebih tinggi dengan proration otomatis dan penagihan instan.
  </Card>

  <Card title="Cross-Sells" icon="grid-2-plus">
    Tambahkan produk terkait untuk pelanggan yang sudah ada tanpa memasukkan kembali detail pembayaran.
  </Card>
</CardGroup>

## Overview

Upsells dan downsells adalah strategi optimalisasi pendapatan yang ampuh:

* **Upsells**: Tawarkan produk bernilai tinggi atau peningkatan (misalnya, paket Pro daripada Basic)
* **Downsells**: Tawarkan alternatif dengan harga lebih rendah saat pelanggan menolak atau menurunkan tingkat
* **Cross-sells**: Sarankan produk pelengkap (misalnya, add-on, barang terkait)

Dodo Payments mendukung alur ini melalui parameter `payment_method_id`, yang memungkinkan Anda menagih metode pembayaran tersimpan pelanggan tanpa mengharuskan mereka memasukkan detail kartu lagi.

### Key Benefits

| Manfaat                                | Dampak                                                     |
| -------------------------------------- | ---------------------------------------------------------- |
| **Pembelian sekali klik**              | Lewati formulir pembayaran sepenuhnya untuk pelanggan lama |
| **Konversi lebih tinggi**              | Kurangi gesekan saat keputusan dibuat                      |
| **Pemrosesan instan**                  | Penagihan diproses segera dengan `confirm: true`           |
| **Pengalaman pengguna tanpa hambatan** | Pelanggan tetap berada dalam aplikasi Anda sepanjang alur  |

## How It Works

```mermaid theme={null}
sequenceDiagram
    participant C as Customer
    participant A as Your App
    participant D as Dodo API
    
    C->>A: Completes purchase
    A->>D: Get payment methods
    D-->>A: payment_method_id
    A->>C: Show upsell offer
    C->>A: Accept (one click)
    A->>D: Charge with payment_method_id
    D-->>A: Payment success
    D->>A: Webhook: payment.succeeded
```

## Prerequisites

Sebelum menerapkan upsells dan downsells, pastikan Anda memiliki:

<Steps>
  <Step title="Customer with Saved Payment Method">
    Pelanggan harus sudah menyelesaikan setidaknya satu pembelian. Metode pembayaran otomatis disimpan saat pelanggan menyelesaikan checkout.
  </Step>

  <Step title="Products Configured">
    Buat produk upsell Anda di dasbor Dodo Payments. Ini bisa berupa pembayaran satu kali, langganan, atau add-on.
  </Step>

  <Step title="Webhook Endpoint">
    Atur webhook untuk menangani `payment.succeeded`, `payment.failed`, dan `subscription.plan_changed`.
  </Step>
</Steps>

## Getting Customer Payment Methods

Sebelum menawarkan upsell, ambil metode pembayaran tersimpan pelanggan:

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    import DodoPayments from 'dodopayments';

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

    async function getPaymentMethods(customerId: string) {
      const paymentMethods = await client.customers.retrievePaymentMethods(customerId);
      
      // Returns array of saved payment methods
      // Each has: payment_method_id, type, card (last4, brand, exp_month, exp_year)
      return paymentMethods;
    }

    // Example usage
    const methods = await getPaymentMethods('cus_123');
    console.log('Available payment methods:', methods);

    // Use the first available method for upsell
    const primaryMethod = methods[0]?.payment_method_id;
    ```
  </Tab>

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

    client = DodoPayments(
        bearer_token=os.environ.get("DODO_PAYMENTS_API_KEY"),
        environment="live_mode",
    )

    def get_payment_methods(customer_id: str):
        payment_methods = client.customers.retrieve_payment_methods(customer_id)
        
        # Returns list of saved payment methods
        # Each has: payment_method_id, type, card (last4, brand, exp_month, exp_year)
        return payment_methods

    # Example usage
    methods = get_payment_methods("cus_123")
    print("Available payment methods:", methods)

    # Use the first available method for upsell
    primary_method = methods[0].payment_method_id if methods else None
    ```
  </Tab>

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

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

    func getPaymentMethods(customerID string) ([]dodopayments.CustomerGetPaymentMethodsResponseItem, error) {
        client := dodopayments.NewClient(
            option.WithBearerToken(os.Getenv("DODO_PAYMENTS_API_KEY")),
        )
        
        resp, err := client.Customers.GetPaymentMethods(
            context.TODO(),
            customerID,
        )
        if err != nil {
            return nil, err
        }
        
        return resp.Items, nil
    }

    func main() {
        methods, err := getPaymentMethods("cus_123")
        if err != nil {
            panic(err)
        }
        
        fmt.Println("Available payment methods:", methods)
        
        // Use the first available method for upsell
        if len(methods) > 0 {
            primaryMethod := methods[0].PaymentMethodID
            fmt.Println("Primary method:", primaryMethod)
        }
    }
    ```
  </Tab>
</Tabs>

<Info>
  Metode pembayaran otomatis disimpan saat pelanggan menyelesaikan checkout. Anda tidak perlu menyimpannya secara eksplisit.
</Info>

## Post-Purchase One-Click Upsells

Tawarkan produk tambahan segera setelah pembelian berhasil. Pelanggan dapat menerima dengan satu klik karena metode pembayaran mereka sudah tersimpan.

```mermaid theme={null}
flowchart LR
    A[Purchase] --> B{Upsell?}
    B -->|Yes| C[Show Offer]
    B -->|No| F[Done]
    C --> D{Accept?}
    D -->|Yes| E[One-Click Charge] --> F
    D -->|No| F
```

### Implementation

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    import DodoPayments from 'dodopayments';

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

    async function createOneClickUpsell(
      customerId: string,
      paymentMethodId: string,
      upsellProductId: string
    ) {
      // Create checkout session with saved payment method
      // confirm: true processes the payment immediately
      const session = await client.checkoutSessions.create({
        product_cart: [
          {
            product_id: upsellProductId,
            quantity: 1
          }
        ],
        customer: {
          customer_id: customerId
        },
        payment_method_id: paymentMethodId,
        confirm: true,  // Required when using payment_method_id
        return_url: 'https://yourapp.com/upsell-success',
        feature_flags: {
          redirect_immediately: true  // Skip success page
        },
        metadata: {
          upsell_source: 'post_purchase',
          original_order_id: 'order_123'
        }
      });

      return session;
    }

    // Example: Offer premium add-on after initial purchase
    async function handlePostPurchaseUpsell(customerId: string) {
      // Get customer's payment methods
      const methods = await client.customers.retrievePaymentMethods(customerId);
      
      if (methods.length === 0) {
        console.log('No saved payment methods available');
        return null;
      }

      // Create the upsell with one-click checkout
      const upsell = await createOneClickUpsell(
        customerId,
        methods[0].payment_method_id,
        'prod_premium_addon'
      );

      console.log('Upsell processed:', upsell.session_id);
      return upsell;
    }
    ```
  </Tab>

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

    client = DodoPayments(
        bearer_token=os.environ.get("DODO_PAYMENTS_API_KEY"),
        environment="live_mode",
    )

    def create_one_click_upsell(
        customer_id: str,
        payment_method_id: str,
        upsell_product_id: str
    ):
        """Create a one-click upsell using saved payment method."""
        
        # Create checkout session with saved payment method
        # confirm=True processes the payment immediately
        session = client.checkout_sessions.create(
            product_cart=[
                {
                    "product_id": upsell_product_id,
                    "quantity": 1
                }
            ],
            customer={
                "customer_id": customer_id
            },
            payment_method_id=payment_method_id,
            confirm=True,  # Required when using payment_method_id
            return_url="https://yourapp.com/upsell-success",
            feature_flags={
                "redirect_immediately": True  # Skip success page
            },
            metadata={
                "upsell_source": "post_purchase",
                "original_order_id": "order_123"
            }
        )
        
        return session

    def handle_post_purchase_upsell(customer_id: str):
        """Offer premium add-on after initial purchase."""
        
        # Get customer's payment methods
        methods = client.customers.retrieve_payment_methods(customer_id)
        
        if not methods:
            print("No saved payment methods available")
            return None
        
        # Create the upsell with one-click checkout
        upsell = create_one_click_upsell(
            customer_id=customer_id,
            payment_method_id=methods[0].payment_method_id,
            upsell_product_id="prod_premium_addon"
        )
        
        print(f"Upsell processed: {upsell.session_id}")
        return upsell
    ```
  </Tab>

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

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

    func createOneClickUpsell(
        customerID string,
        paymentMethodID string,
        upsellProductID string,
    ) (*dodopayments.CheckoutSessionResponse, error) {
        client := dodopayments.NewClient(
            option.WithBearerToken(os.Getenv("DODO_PAYMENTS_API_KEY")),
        )
        
        // Create checkout session with saved payment method
        // Confirm: true processes the payment immediately
        session, err := client.CheckoutSessions.New(context.TODO(), dodopayments.CheckoutSessionNewParams{
            CheckoutSessionRequest: dodopayments.CheckoutSessionRequestParam{
                ProductCart: dodopayments.F([]dodopayments.ProductItemReqParam{
                    {
                        ProductID: dodopayments.F(upsellProductID),
                        Quantity:  dodopayments.F(int64(1)),
                    },
                }),
                Customer: dodopayments.F[dodopayments.CustomerRequestUnionParam](
                    dodopayments.AttachExistingCustomerParam{
                        CustomerID: dodopayments.F(customerID),
                    },
                ),
                PaymentMethodID: dodopayments.F(paymentMethodID),
                Confirm:         dodopayments.F(true), // Required when using payment_method_id
                ReturnURL:       dodopayments.F("https://yourapp.com/upsell-success"),
                FeatureFlags: dodopayments.F(dodopayments.CheckoutSessionFlagsParam{
                    RedirectImmediately: dodopayments.F(true), // Skip success page
                }),
                Metadata: dodopayments.F(map[string]string{
                    "upsell_source":     "post_purchase",
                    "original_order_id": "order_123",
                }),
            },
        })
        
        return session, err
    }

    func handlePostPurchaseUpsell(customerID string) (*dodopayments.CheckoutSessionResponse, error) {
        client := dodopayments.NewClient(
            option.WithBearerToken(os.Getenv("DODO_PAYMENTS_API_KEY")),
        )
        
        // Get customer's payment methods
        resp, err := client.Customers.GetPaymentMethods(context.TODO(), customerID)
        if err != nil {
            return nil, err
        }
        
        if len(resp.Items) == 0 {
            fmt.Println("No saved payment methods available")
            return nil, nil
        }
        
        // Create the upsell with one-click checkout
        upsell, err := createOneClickUpsell(
            customerID,
            resp.Items[0].PaymentMethodID,
            "prod_premium_addon",
        )
        if err != nil {
            return nil, err
        }
        
        fmt.Printf("Upsell processed: %s\n", upsell.SessionID)
        return upsell, nil
    }
    ```
  </Tab>
</Tabs>

<Warning>
  Saat menggunakan `payment_method_id`, Anda harus mengatur `confirm: true` dan menyediakan `customer_id` yang sudah ada. Metode pembayaran harus milik pelanggan tersebut.
</Warning>

## Subscription Upgrades

Pindahkan pelanggan ke paket langganan tingkat lebih tinggi dengan proration otomatis.

```mermaid theme={null}
sequenceDiagram
    participant C as Customer
    participant A as Your App
    participant D as Dodo API
    
    C->>A: Request upgrade
    A->>D: Preview change plan
    D-->>A: Charge amount
    A->>C: Confirm $50 charge?
    C->>A: Confirm
    A->>D: Change plan
    D->>A: Webhook: plan_changed
```

### Preview Before Committing

Selalu buat pratinjau perubahan paket untuk menunjukkan kepada pelanggan persis berapa yang akan mereka bayar:

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    async function previewUpgrade(
      subscriptionId: string,
      newProductId: string
    ) {
      const preview = await client.subscriptions.previewChangePlan(subscriptionId, {
        product_id: newProductId,
        quantity: 1,
        proration_billing_mode: 'difference_immediately'
      });

      return {
        immediateCharge: preview.immediate_charge?.summary,
        newPlan: preview.new_plan,
        effectiveDate: preview.effective_date
      };
    }

    // Show customer the charge before confirming
    const preview = await previewUpgrade('sub_123', 'prod_pro_plan');
    console.log(`Upgrade will charge: ${preview.immediateCharge}`);
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    def preview_upgrade(subscription_id: str, new_product_id: str):
        preview = client.subscriptions.preview_change_plan(
            subscription_id=subscription_id,
            product_id=new_product_id,
            quantity=1,
            proration_billing_mode="difference_immediately"
        )
        
        return {
            "immediate_charge": preview.immediate_charge.summary if preview.immediate_charge else None,
            "new_plan": preview.new_plan,
            "effective_date": preview.effective_date
        }

    # Show customer the charge before confirming
    preview = preview_upgrade("sub_123", "prod_pro_plan")
    print(f"Upgrade will charge: {preview['immediate_charge']}")
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    func previewUpgrade(subscriptionID string, newProductID string) (map[string]interface{}, error) {
        client := dodopayments.NewClient(
            option.WithBearerToken(os.Getenv("DODO_PAYMENTS_API_KEY")),
        )
        
        preview, err := client.Subscriptions.PreviewChangePlan(
            context.TODO(),
            subscriptionID,
            dodopayments.SubscriptionPreviewChangePlanParams{
                UpdateSubscriptionPlanReq: dodopayments.UpdateSubscriptionPlanReqParam{
                    ProductID:            dodopayments.F(newProductID),
                    Quantity:             dodopayments.F(int64(1)),
                    ProrationBillingMode: dodopayments.F(dodopayments.UpdateSubscriptionPlanReqProrationBillingModeDifferenceImmediately),
                },
            },
        )
        if err != nil {
            return nil, err
        }
        
        return map[string]interface{}{
            "immediate_charge": preview.ImmediateCharge.Summary,
            "new_plan":         preview.NewPlan,
            "effective_at":     preview.ImmediateCharge.EffectiveAt,
        }, nil
    }
    ```
  </Tab>
</Tabs>

### Execute the Upgrade

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    async function upgradeSubscription(
      subscriptionId: string,
      newProductId: string,
      prorationMode: 'prorated_immediately' | 'difference_immediately' | 'full_immediately' = 'difference_immediately'
    ) {
      const result = await client.subscriptions.changePlan(subscriptionId, {
        product_id: newProductId,
        quantity: 1,
        proration_billing_mode: prorationMode
      });

      return {
        status: result.status,
        subscriptionId: result.subscription_id,
        paymentId: result.payment_id,
        invoiceId: result.invoice_id
      };
    }

    // Upgrade from Basic ($30) to Pro ($80)
    // With difference_immediately: charges $50 instantly
    const upgrade = await upgradeSubscription('sub_123', 'prod_pro_plan');
    console.log('Upgrade status:', upgrade.status);
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    def upgrade_subscription(
        subscription_id: str,
        new_product_id: str,
        proration_mode: str = "difference_immediately"
    ):
        result = client.subscriptions.change_plan(
            subscription_id=subscription_id,
            product_id=new_product_id,
            quantity=1,
            proration_billing_mode=proration_mode
        )
        
        return {
            "status": result.status,
            "subscription_id": result.subscription_id,
            "payment_id": result.payment_id,
            "invoice_id": result.invoice_id
        }

    # Upgrade from Basic ($30) to Pro ($80)
    # With difference_immediately: charges $50 instantly
    upgrade = upgrade_subscription("sub_123", "prod_pro_plan")
    print(f"Upgrade status: {upgrade['status']}")
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    func upgradeSubscription(
        subscriptionID string,
        newProductID string,
        prorationMode dodopayments.UpdateSubscriptionPlanReqProrationBillingMode,
    ) error {
        client := dodopayments.NewClient(
            option.WithBearerToken(os.Getenv("DODO_PAYMENTS_API_KEY")),
        )
        
        // ChangePlan returns no response body; a nil error means the change succeeded.
        err := client.Subscriptions.ChangePlan(
            context.TODO(),
            subscriptionID,
            dodopayments.SubscriptionChangePlanParams{
                UpdateSubscriptionPlanReq: dodopayments.UpdateSubscriptionPlanReqParam{
                    ProductID:            dodopayments.F(newProductID),
                    Quantity:             dodopayments.F(int64(1)),
                    ProrationBillingMode: dodopayments.F(prorationMode),
                },
            },
        )
        
        return err
    }

    // Upgrade from Basic ($30) to Pro ($80)
    // With DifferenceImmediately: charges $50 instantly
    err := upgradeSubscription(
        "sub_123",
        "prod_pro_plan",
        dodopayments.UpdateSubscriptionPlanReqProrationBillingModeDifferenceImmediately,
    )
    if err != nil {
        panic(err)
    }
    fmt.Println("Upgrade succeeded")
    ```
  </Tab>
</Tabs>

### Proration Modes

Pilih bagaimana pelanggan ditagih saat melakukan upgrade:

| Mode                     | Perilaku                                                  | Terbaik Untuk                 |
| ------------------------ | --------------------------------------------------------- | ----------------------------- |
| `difference_immediately` | Menagih selisih harga secara instan ($30→$80 = \$50)      | Upgrade sederhana             |
| `prorated_immediately`   | Menagih berdasarkan sisa waktu dalam siklus tagihan       | Penagihan adil berbasis waktu |
| `full_immediately`       | Menagih harga penuh paket baru, mengabaikan waktu tersisa | Siklus tagihan direset        |

<Tip>
  Gunakan `difference_immediately` untuk alur upgrade yang sederhana. Gunakan `prorated_immediately` ketika Anda ingin memperhitungkan waktu yang tidak terpakai pada paket saat ini.
</Tip>

## Cross-Sells

Tambahkan produk pelengkap untuk pelanggan yang sudah ada tanpa mengharuskan mereka memasukkan kembali detail pembayaran.

```mermaid theme={null}
flowchart LR
    A[View Product] --> B{Existing Customer?}
    B -->|Yes| C[One-Click Buy] --> E[Deliver]
    B -->|No| D[Standard Checkout] --> E
```

### Implementation

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    async function createCrossSell(
      customerId: string,
      paymentMethodId: string,
      productId: string,
      quantity: number = 1
    ) {
      // Create a one-time payment using saved payment method
      const payment = await client.payments.create({
        product_cart: [
          {
            product_id: productId,
            quantity: quantity
          }
        ],
        customer_id: customerId,
        payment_method_id: paymentMethodId,
        return_url: 'https://yourapp.com/purchase-complete',
        metadata: {
          purchase_type: 'cross_sell',
          source: 'product_recommendation'
        }
      });

      return payment;
    }

    // Example: Customer bought a course, offer related ebook
    async function offerRelatedProduct(customerId: string, relatedProductId: string) {
      const methods = await client.customers.retrievePaymentMethods(customerId);
      
      if (methods.length === 0) {
        // Fall back to standard checkout
        return client.checkoutSessions.create({
          product_cart: [{ product_id: relatedProductId, quantity: 1 }],
          customer: { customer_id: customerId },
          return_url: 'https://yourapp.com/purchase-complete'
        });
      }

      // One-click purchase
      return createCrossSell(customerId, methods[0].payment_method_id, relatedProductId);
    }
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    def create_cross_sell(
        customer_id: str,
        payment_method_id: str,
        product_id: str,
        quantity: int = 1
    ):
        """Create a one-time payment using saved payment method."""
        
        payment = client.payments.create(
            product_cart=[
                {
                    "product_id": product_id,
                    "quantity": quantity
                }
            ],
            customer_id=customer_id,
            payment_method_id=payment_method_id,
            return_url="https://yourapp.com/purchase-complete",
            metadata={
                "purchase_type": "cross_sell",
                "source": "product_recommendation"
            }
        )
        
        return payment

    def offer_related_product(customer_id: str, related_product_id: str):
        """Offer related product with one-click purchase if possible."""
        
        methods = client.customers.retrieve_payment_methods(customer_id)
        
        if not methods:
            # Fall back to standard checkout
            return client.checkout_sessions.create(
                product_cart=[{"product_id": related_product_id, "quantity": 1}],
                customer={"customer_id": customer_id},
                return_url="https://yourapp.com/purchase-complete"
            )
        
        # One-click purchase
        return create_cross_sell(customer_id, methods[0].payment_method_id, related_product_id)
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    func createCrossSell(
        customerID string,
        paymentMethodID string,
        productID string,
        quantity int64,
    ) (*dodopayments.PaymentNewResponse, error) {
        client := dodopayments.NewClient(
            option.WithBearerToken(os.Getenv("DODO_PAYMENTS_API_KEY")),
        )
        
        payment, err := client.Payments.New(context.TODO(), dodopayments.PaymentNewParams{
            ProductCart: dodopayments.F([]dodopayments.OneTimeProductCartItemParam{
                {
                    ProductID: dodopayments.F(productID),
                    Quantity:  dodopayments.F(quantity),
                },
            }),
            Customer: dodopayments.F[dodopayments.CustomerRequestUnionParam](
                dodopayments.AttachExistingCustomerParam{CustomerID: dodopayments.F(customerID)},
            ),
            Billing: dodopayments.F(dodopayments.BillingAddressParam{
                Country: dodopayments.F(dodopayments.CountryCodeUs),
                City:    dodopayments.F("San Francisco"),
                State:   dodopayments.F("CA"),
                Street:  dodopayments.F("1 Market St"),
                Zipcode: dodopayments.F("94105"),
            }),
            PaymentMethodID: dodopayments.F(paymentMethodID),
            ReturnURL:       dodopayments.F("https://yourapp.com/purchase-complete"),
            Metadata: dodopayments.F(map[string]string{
                "purchase_type": "cross_sell",
                "source":        "product_recommendation",
            }),
        })
        
        return payment, err
    }

    func offerRelatedProduct(customerID string, relatedProductID string) (interface{}, error) {
        client := dodopayments.NewClient(
            option.WithBearerToken(os.Getenv("DODO_PAYMENTS_API_KEY")),
        )
        
        resp, err := client.Customers.GetPaymentMethods(context.TODO(), customerID)
        if err != nil {
            return nil, err
        }
        
        if len(resp.Items) == 0 {
            // Fall back to standard checkout
            return client.CheckoutSessions.New(context.TODO(), dodopayments.CheckoutSessionNewParams{
                CheckoutSessionRequest: dodopayments.CheckoutSessionRequestParam{
                    ProductCart: dodopayments.F([]dodopayments.ProductItemReqParam{
                        {ProductID: dodopayments.F(relatedProductID), Quantity: dodopayments.F(int64(1))},
                    }),
                    Customer: dodopayments.F[dodopayments.CustomerRequestUnionParam](
                        dodopayments.AttachExistingCustomerParam{CustomerID: dodopayments.F(customerID)},
                    ),
                    ReturnURL: dodopayments.F("https://yourapp.com/purchase-complete"),
                },
            })
        }
        
        // One-click purchase
        return createCrossSell(customerID, resp.Items[0].PaymentMethodID, relatedProductID, 1)
    }
    ```
  </Tab>
</Tabs>

## Subscription Downgrades

Ketika pelanggan ingin pindah ke paket tingkat lebih rendah, tangani transisi dengan mulus menggunakan kredit otomatis.

### How Downgrades Work

1. Pelanggan meminta downgrade (Pro → Basic)
2. Sistem menghitung nilai tersisa pada paket saat ini
3. Kredit ditambahkan ke langganan untuk perpanjangan berikutnya
4. Pelanggan langsung beralih ke paket baru

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    async function downgradeSubscription(
      subscriptionId: string,
      newProductId: string
    ) {
      // Preview the downgrade first
      const preview = await client.subscriptions.previewChangePlan(subscriptionId, {
        product_id: newProductId,
        quantity: 1,
        proration_billing_mode: 'difference_immediately'
      });

      console.log('Credit to be applied:', preview.credit_amount);

      // Execute the downgrade
      const result = await client.subscriptions.changePlan(subscriptionId, {
        product_id: newProductId,
        quantity: 1,
        proration_billing_mode: 'difference_immediately'
      });

      // Credits are automatically applied to future renewals
      return result;
    }

    // Downgrade from Pro ($80) to Basic ($30)
    // $50 credit added to subscription, auto-applied on next renewal
    const downgrade = await downgradeSubscription('sub_123', 'prod_basic_plan');
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    def downgrade_subscription(subscription_id: str, new_product_id: str):
        # Preview the downgrade first
        preview = client.subscriptions.preview_change_plan(
            subscription_id=subscription_id,
            product_id=new_product_id,
            quantity=1,
            proration_billing_mode="difference_immediately"
        )
        
        print(f"Credit to be applied: {preview.credit_amount}")
        
        # Execute the downgrade
        result = client.subscriptions.change_plan(
            subscription_id=subscription_id,
            product_id=new_product_id,
            quantity=1,
            proration_billing_mode="difference_immediately"
        )
        
        # Credits are automatically applied to future renewals
        return result

    # Downgrade from Pro ($80) to Basic ($30)
    # $50 credit added to subscription, auto-applied on next renewal
    downgrade = downgrade_subscription("sub_123", "prod_basic_plan")
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    func downgradeSubscription(subscriptionID string, newProductID string) error {
        client := dodopayments.NewClient(
            option.WithBearerToken(os.Getenv("DODO_PAYMENTS_API_KEY")),
        )
        
        // Preview the downgrade first
        preview, err := client.Subscriptions.PreviewChangePlan(
            context.TODO(),
            subscriptionID,
            dodopayments.SubscriptionPreviewChangePlanParams{
                UpdateSubscriptionPlanReq: dodopayments.UpdateSubscriptionPlanReqParam{
                    ProductID:            dodopayments.F(newProductID),
                    Quantity:             dodopayments.F(int64(1)),
                    ProrationBillingMode: dodopayments.F(dodopayments.UpdateSubscriptionPlanReqProrationBillingModeDifferenceImmediately),
                },
            },
        )
        if err != nil {
            return err
        }
        
        fmt.Printf("Customer credits to be applied: %v\n", preview.ImmediateCharge.Summary.CustomerCredits)
        
        // Execute the downgrade (returns no response body; nil error means success)
        return client.Subscriptions.ChangePlan(
            context.TODO(),
            subscriptionID,
            dodopayments.SubscriptionChangePlanParams{
                UpdateSubscriptionPlanReq: dodopayments.UpdateSubscriptionPlanReqParam{
                    ProductID:            dodopayments.F(newProductID),
                    Quantity:             dodopayments.F(int64(1)),
                    ProrationBillingMode: dodopayments.F(dodopayments.UpdateSubscriptionPlanReqProrationBillingModeDifferenceImmediately),
                },
            },
        )
    }
    ```
  </Tab>
</Tabs>

<Info>
  Kredit dari penurunan level menggunakan `difference_immediately` bersifat lingkup langganan dan diterapkan secara otomatis ke perpanjangan berikutnya. Mereka berbeda dari hak [Credit-Based Billing](/features/credit-based-billing).
</Info>

## Complete Example: Post-Purchase Upsell Flow

Berikut implementasi lengkap yang menunjukkan bagaimana menawarkan upsell setelah pembelian berhasil:

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    import DodoPayments from 'dodopayments';
    import express from 'express';

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

    const app = express();

    // Store for tracking upsell eligibility (use your database in production)
    const eligibleUpsells = new Map<string, { customerId: string; productId: string }>();

    // Webhook handler for initial purchase success
    app.post('/webhooks/dodo', express.raw({ type: 'application/json' }), async (req, res) => {
      const event = JSON.parse(req.body.toString());
      
      switch (event.type) {
        case 'payment.succeeded':
          // Check if customer is eligible for upsell
          const customerId = event.data.customer_id;
          const productId = event.data.product_id;
          
          // Define upsell rules (e.g., bought Basic, offer Pro)
          const upsellProduct = getUpsellProduct(productId);
          
          if (upsellProduct) {
            eligibleUpsells.set(customerId, {
              customerId,
              productId: upsellProduct
            });
          }
          break;
          
        case 'payment.failed':
          console.log('Payment failed:', event.data.payment_id);
          // Handle failed upsell payment
          break;
      }
      
      res.json({ received: true });
    });

    // API endpoint to check upsell eligibility
    app.get('/api/upsell/:customerId', async (req, res) => {
      const { customerId } = req.params;
      const upsell = eligibleUpsells.get(customerId);
      
      if (!upsell) {
        return res.json({ eligible: false });
      }
      
      // Get payment methods
      const methods = await client.customers.retrievePaymentMethods(customerId);
      
      if (methods.length === 0) {
        return res.json({ eligible: false, reason: 'no_payment_method' });
      }
      
      // Get product details for display
      const product = await client.products.retrieve(upsell.productId);
      
      res.json({
        eligible: true,
        product: {
          id: product.product_id,
          name: product.name,
          price: product.price,
          currency: product.currency
        },
        paymentMethodId: methods[0].payment_method_id
      });
    });

    // API endpoint to accept upsell
    app.post('/api/upsell/:customerId/accept', async (req, res) => {
      const { customerId } = req.params;
      const upsell = eligibleUpsells.get(customerId);
      
      if (!upsell) {
        return res.status(400).json({ error: 'No upsell available' });
      }
      
      try {
        const methods = await client.customers.retrievePaymentMethods(customerId);
        
        // Create one-click purchase
        const session = await client.checkoutSessions.create({
          product_cart: [{ product_id: upsell.productId, quantity: 1 }],
          customer: { customer_id: customerId },
          payment_method_id: methods[0].payment_method_id,
          confirm: true,
          return_url: `${process.env.APP_URL}/upsell-success`,
          feature_flags: { redirect_immediately: true },
          metadata: { upsell: 'true', source: 'post_purchase' }
        });
        
        // Clear the upsell offer
        eligibleUpsells.delete(customerId);
        
        res.json({ success: true, sessionId: session.session_id });
      } catch (error) {
        console.error('Upsell failed:', error);
        res.status(500).json({ error: 'Upsell processing failed' });
      }
    });

    // Helper function to determine upsell product
    function getUpsellProduct(purchasedProductId: string): string | null {
      const upsellMap: Record<string, string> = {
        'prod_basic_plan': 'prod_pro_plan',
        'prod_starter_course': 'prod_complete_bundle',
        'prod_single_license': 'prod_team_license'
      };
      
      return upsellMap[purchasedProductId] || null;
    }

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

  <Tab title="Python">
    ```python theme={null}
    import os
    from flask import Flask, request, jsonify
    from dodopayments import DodoPayments

    client = DodoPayments(
        bearer_token=os.environ.get("DODO_PAYMENTS_API_KEY"),
        environment="live_mode",
    )

    app = Flask(__name__)

    # Store for tracking upsell eligibility (use your database in production)
    eligible_upsells = {}

    @app.route('/webhooks/dodo', methods=['POST'])
    def webhook_handler():
        event = request.json
        
        if event['type'] == 'payment.succeeded':
            # Check if customer is eligible for upsell
            customer_id = event['data']['customer_id']
            product_id = event['data']['product_id']
            
            # Define upsell rules
            upsell_product = get_upsell_product(product_id)
            
            if upsell_product:
                eligible_upsells[customer_id] = {
                    'customer_id': customer_id,
                    'product_id': upsell_product
                }
        
        elif event['type'] == 'payment.failed':
            print(f"Payment failed: {event['data']['payment_id']}")
        
        return jsonify({'received': True})

    @app.route('/api/upsell/<customer_id>', methods=['GET'])
    def check_upsell(customer_id):
        upsell = eligible_upsells.get(customer_id)
        
        if not upsell:
            return jsonify({'eligible': False})
        
        # Get payment methods
        methods = client.customers.retrieve_payment_methods(customer_id)
        
        if not methods:
            return jsonify({'eligible': False, 'reason': 'no_payment_method'})
        
        # Get product details for display
        product = client.products.retrieve(upsell['product_id'])
        
        return jsonify({
            'eligible': True,
            'product': {
                'id': product.product_id,
                'name': product.name,
                'price': product.price,
                'currency': product.currency
            },
            'payment_method_id': methods[0].payment_method_id
        })

    @app.route('/api/upsell/<customer_id>/accept', methods=['POST'])
    def accept_upsell(customer_id):
        upsell = eligible_upsells.get(customer_id)
        
        if not upsell:
            return jsonify({'error': 'No upsell available'}), 400
        
        try:
            methods = client.customers.retrieve_payment_methods(customer_id)
            
            # Create one-click purchase
            session = client.checkout_sessions.create(
                product_cart=[{'product_id': upsell['product_id'], 'quantity': 1}],
                customer={'customer_id': customer_id},
                payment_method_id=methods[0].payment_method_id,
                confirm=True,
                return_url=f"{os.environ['APP_URL']}/upsell-success",
                feature_flags={'redirect_immediately': True},
                metadata={'upsell': 'true', 'source': 'post_purchase'}
            )
            
            # Clear the upsell offer
            del eligible_upsells[customer_id]
            
            return jsonify({'success': True, 'session_id': session.session_id})
        
        except Exception as error:
            print(f"Upsell failed: {error}")
            return jsonify({'error': 'Upsell processing failed'}), 500

    def get_upsell_product(purchased_product_id: str) -> str:
        """Determine upsell product based on purchased product."""
        upsell_map = {
            'prod_basic_plan': 'prod_pro_plan',
            'prod_starter_course': 'prod_complete_bundle',
            'prod_single_license': 'prod_team_license'
        }
        return upsell_map.get(purchased_product_id)

    if __name__ == '__main__':
        app.run(port=3000)
    ```
  </Tab>
</Tabs>

## Best Practices

<AccordionGroup>
  <Accordion title="Time Your Upsells Strategically">
    Waktu terbaik untuk menawarkan upsell adalah segera setelah pembelian berhasil ketika pelanggan berada dalam pola pikir membeli. Momen efektif lainnya:

    * Setelah pencapaian penggunaan fitur
    * Saat mendekati batas paket
    * Saat menyelesaikan onboarding
  </Accordion>

  <Accordion title="Validate Payment Method Eligibility">
    Sebelum mencoba penagihan sekali klik, verifikasi metode pembayaran:

    * Kompatibel dengan mata uang produk
    * Belum kadaluarsa
    * Milik pelanggan tersebut

    API akan memvalidasi ini, tetapi memeriksa secara proaktif meningkatkan pengalaman pengguna.
  </Accordion>

  <Accordion title="Handle Failures Gracefully">
    Saat penagihan sekali klik gagal:

    1. Kembalikan ke alur checkout standar
    2. Beri tahu pelanggan dengan pesan yang jelas
    3. Tawarkan pembaruan metode pembayaran
    4. Jangan mencoba lagi berkali-kali untuk penagihan yang gagal
  </Accordion>

  <Accordion title="Provide Clear Value Proposition">
    Upsell memiliki konversi lebih baik saat pelanggan memahami nilainya:

    * Tunjukkan apa yang mereka dapatkan dibandingkan paket saat ini
    * Sorot selisih harga, bukan total harga
    * Gunakan bukti sosial (misalnya, "Upgrade paling populer")
  </Accordion>

  <Accordion title="Respect Customer Choice">
    * Selalu sediakan cara mudah untuk menolak
    * Jangan tampilkan upsell yang sama berulang kali setelah ditolak
    * Lacak dan analisis upsell mana yang berhasil untuk mengoptimalkan penawaran
  </Accordion>
</AccordionGroup>

## Webhooks to Monitor

Pantau event webhook ini untuk alur upsell dan downgrade:

| Event                       | Pemicu                                               | Aksi                                                      |
| --------------------------- | ---------------------------------------------------- | --------------------------------------------------------- |
| `payment.succeeded`         | Pembayaran upsell/cross-sell selesai                 | Kirim produk, perbarui akses                              |
| `payment.failed`            | Penagihan sekali klik gagal                          | Tampilkan kesalahan, tawarkan pengulangan atau alternatif |
| `subscription.plan_changed` | Upgrade/downgrade selesai                            | Perbarui fitur, kirim konfirmasi                          |
| `subscription.active`       | Langganan diaktifkan kembali setelah perubahan paket | Berikan akses ke tingkat baru                             |

<Card title="Webhook Integration Guide" icon="webhook" href="/developer-resources/webhooks">
  Pelajari cara mengatur dan memverifikasi endpoint webhook.
</Card>

## Related Resources

<CardGroup cols={2}>
  <Card title="Subscription Upgrade Guide" icon="arrows-rotate" href="/developer-resources/subscription-upgrade-downgrade">
    Panduan lengkap tentang perubahan paket, mode proration, dan penanganan kegagalan.
  </Card>

  <Card title="Checkout Sessions" icon="cart-shopping" href="/developer-resources/checkout-session">
    Referensi lengkap untuk membuat sesi checkout dengan semua opsi.
  </Card>

  <Card title="Customer Payment Methods API" icon="credit-card" href="/api-reference/customers/get-customer-payment-methods">
    Referensi API untuk mencantumkan metode pembayaran pelanggan.
  </Card>

  <Card title="Add-ons" icon="puzzle-piece" href="/features/addons">
    Perkuat langganan dengan add-on fleksibel untuk pendapatan tambahan.
  </Card>
</CardGroup>
