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

Change a customer's subscription plan to move them between tiers, adjust quantity for seat-based products, or migrate to a new product. The API automatically calculates prorations and charges based on your chosen billing mode.

<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 the [Integration Guide](/developer-resources/subscription-integration-guide).
</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 that want to credit unused time on the old plan.

        * Credits the unused portion of the current cycle, prorated by time remaining
        * Then charges a **full** cycle at the new plan — the new plan price is never prorated
        * Net charge = full new cycle − (remaining fraction × full old cycle)
      </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 body="product_id" type="string" required>
      The new product ID to change the subscription to.
    </ParamField>

    <ParamField body="quantity" type="integer" required>
      Number of units for the new plan (for seat-based products).
    </ParamField>

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

    <ParamField body="addons" type="array">
      Optional addons for the new plan. Omitting this field, sending `null`, or sending an empty array removes any existing addons, so include current addons to keep them.
    </ParamField>

    <ParamField body="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 body="collect_via_payment_link" type="boolean">
      Collect the plan-change amount with a payment link instead of charging the subscription's saved payment method. The customer pays on a hosted checkout page.

      Requires the business's `allow_plan_change_via_payment_link` capability (**Settings → Subscriptions → Collect Plan Change Payments by Payment Link**), `effective_at: immediately`, and `on_payment_failure: prevent_change`. See [Collecting Payment via a Checkout Link](#collecting-payment-via-a-checkout-link). Ignored by the preview route.
    </ParamField>

    <ParamField body="return_url" type="string">
      The URL that receives the customer after they pay the payment link. The redirect adds `subscription_id`, `payment_id`, and `status`. Requires `collect_via_payment_link: true`; otherwise the request fails with `422`. See [Redirecting the Customer After Payment](#redirecting-the-customer-after-payment).
    </ParamField>

    <ParamField body="cancel_older_payment_link" type="boolean" default="false">
      Cancel the unpaid payment link of a pending plan change, so that this request replaces it. A paid or in-progress payment returns `409`. See [Replacing a Pending Payment Link](#replacing-a-pending-payment-link). Ignored by the preview route.
    </ParamField>

    <ParamField body="discount_codes" type="array">
      Optional **stacked** discount codes to apply to the new plan (max 20, applied in array order). Behavior depends on what you pass:

      * **Not provided / `null`** — existing discounts with `preserve_on_plan_change=true` are preserved if applicable to the new product.
      * **`[]` (empty array)** — removes all existing discounts from the subscription.
      * **`["CODE_A", "CODE_B", ...]`** — replaces any existing discounts with this stacked set.
    </ParamField>

    <ParamField body="discount_code" type="string" deprecated>
      **Deprecated** — prefer `discount_codes` for new integrations. This field still works for backward compatibility, but cannot be combined with `discount_codes` in the same request.
    </ParamField>

    <ParamField body="effective_at" type="string" default="immediately">
      When to apply the plan change:

      * `immediately` (default): Apply the plan change right away
      * `next_billing_date`: Schedule the change for the next billing date. The customer retains their current plan until the billing period ends. Use this for downgrades so customers keep their current plan benefits until the end of the billing period.
    </ParamField>
  </Step>

  <Step title="Handle Webhook Events">
    Set up webhook handling to track plan change outcomes:

    * `subscription.active`: Plan change successful, subscription updated
    * `subscription.plan_changed`: Subscription plan changed (upgrade/downgrade/addon update)
    * `subscription.on_hold`: Plan change charge failed, renewals stopped
    * `payment.succeeded`: Immediate charge for plan change succeeded
    * `payment.failed`: Immediate charge failed

    <Warning>
      Always verify webhook signatures and implement idempotent event processing.
    </Warning>
  </Step>

  <Step title="Update Your Application State">
    Based on webhook events, update your application:

    * Grant/revoke features based on new plan
    * Update customer dashboard with new plan details
    * Send confirmation emails about plan changes
    * Log billing changes for audit purposes
  </Step>

  <Step title="Test and Monitor">
    Thoroughly test your implementation:

    * Test all proration modes with different scenarios
    * Verify webhook handling works correctly
    * Monitor plan change success rates
    * Set up alerts for failed plan changes

    <Check>
      Your subscription plan change implementation is now ready for production use.
    </Check>
  </Step>
</Steps>

## Preview Plan Changes

Before committing to a plan change, use the Preview API to show customers exactly what they'll be charged:

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

    // Show customer the charge before confirming
    console.log('Immediate charge:', preview.immediate_charge.summary.total_amount, preview.immediate_charge.summary.currency);
    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="pdt_pro",
        quantity=1,
        proration_billing_mode="prorated_immediately"
    )

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

<Tip>
  Use the preview API to build confirmation dialogs that show customers the exact amount they'll be charged before they confirm a plan change.
</Tip>

<Note>
  In `immediate_charge.summary`, `customer_credits` is the net change to the customer's credit balance, in the currency given by `customer_credits_currency`. This is the currency of the customer's credit wallet, which is the subscription currency. It can differ from the summary's `currency`, which applies to `total_amount` and `tax`. For example, a customer paying in INR on a USD subscription sees credits in USD.
</Note>

## Change Plan API

Use the Change Plan API to modify product, quantity, and proration behavior for an active subscription.

### Quick Start Examples

<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() {
      // change-plan returns 200 with a ChangePlanResponse body (payment_id, payment_link,
      // client_secret, expires_on) — all null for an ordinary off-session change.
      await client.subscriptions.changePlan('sub_123', {
        product_id: 'pdt_new',
        quantity: 3,
        proration_billing_mode: 'prorated_immediately',
        on_payment_failure: 'prevent_change', // Optional: control behavior on payment failure
      });
      console.log('Plan change accepted; awaiting webhooks');
    }

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

    # change_plan returns 200 with a ChangePlanResponse body (payment_id, payment_link,
    # client_secret, expires_on) — all None for an ordinary off-session change.
    client.subscriptions.change_plan(
        subscription_id="sub_123",
        product_id="pdt_new",
        quantity=3,
        proration_billing_mode="prorated_immediately",
        on_payment_failure="prevent_change",  # Optional: control behavior on payment failure
    )
    print("Plan change accepted; awaiting webhooks")
    ```
  </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 a SubscriptionChangePlanResponse alongside the error.
      _, err := client.Subscriptions.ChangePlan(context.TODO(), "sub_123", dodopayments.SubscriptionChangePlanParams{
        UpdateSubscriptionPlanReq: dodopayments.UpdateSubscriptionPlanReqParam{
          ProductID:            dodopayments.F("pdt_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": "pdt_new",
        "quantity": 3,
        "proration_billing_mode": "prorated_immediately",
        "on_payment_failure": "prevent_change"
      }'
    ```
  </Tab>
</Tabs>

A successful plan change returns `200 OK` immediately — before any charge has actually settled. What the body (`ChangePlanResponse`) contains depends on how the change was collected:

| Case | `payment_id` / `payment_link` / `client_secret` / `expires_on` |
| - | - |
| Scheduled change (`effective_at: next_billing_date`) | All `null` — nothing is charged yet |
| `proration_billing_mode: do_not_bill` | All `null` — nothing to charge |
| Ordinary immediate charge (saved payment method) | All `null` |
| `collect_via_payment_link: true` (link issued) | All **populated** — checkout handles for the customer |

<Note>
  This response confirms the request was accepted, not that a charge succeeded. For an ordinary immediate charge, the outcome resolves off-session right after the call. For a `collect_via_payment_link` request, the subscription stays on its current plan until the customer completes payment.

  Confirm the outcome via webhook (`payment.succeeded`, `payment.failed`, `subscription.plan_changed`) or by re-reading the subscription with `GET /subscriptions/{subscription_id}` — see [What Happens While the Link Is Unpaid](#what-happens-while-the-link-is-unpaid) for the payment-link case.
</Note>

<Warning>
  If the immediate charge fails, the subscription may move to `subscription.on_hold` until payment succeeds.
</Warning>

## Collecting Payment via a Checkout Link

By default, an immediate plan change charges the subscription's saved payment method directly. Set `collect_via_payment_link: true` to send the customer to a hosted checkout page instead — useful when there's no saved payment method or you want the customer to actively confirm the new price.

<Info>
  This powers the **Collect Plan Change Payments by Payment Link** toggle in **Settings → Subscriptions**, which routes the [Customer Portal](/features/customer-portal#collecting-plan-change-payments-by-payment-link)'s plan-change flow through checkout.
</Info>

### Requirements

`collect_via_payment_link: true` only succeeds when **all** of the following hold — otherwise the request fails with `422`:

* The business has the `allow_plan_change_via_payment_link` capability enabled (**Settings → Subscriptions → Collect Plan Change Payments by Payment Link**).
* `effective_at` is `immediately` (the default). A scheduled change (`next_billing_date`) never needs a checkout page.
* The request sets `on_payment_failure: prevent_change`. An explicit `apply_change` fails with `422`.

<Info>
  `collect_via_payment_link` applies to any immediate change that results in a charge, downgrades included, as long as the requirements above are met.
</Info>

If the change nets to zero or a credit, no payment link is issued: `payment_link` and the other checkout fields come back `null`, and the change applies immediately. This is not a `422`. Call [Preview Plan Change](/api-reference/subscriptions/preview-change-plan) first to check the amount before requesting a link.

<Tabs>
  <Tab title="Node.js SDK">
    ```javascript theme={null}
    const response = await client.subscriptions.changePlan('sub_123', {
      product_id: 'pdt_pro',
      quantity: 1,
      proration_billing_mode: 'prorated_immediately',
      effective_at: 'immediately',
      on_payment_failure: 'prevent_change',
      collect_via_payment_link: true,
    });

    // Hand response.payment_link to your frontend and send the customer there
    console.log(response.payment_link);
    ```
  </Tab>

  <Tab title="Python SDK">
    ```python theme={null}
    response = client.subscriptions.change_plan(
        subscription_id="sub_123",
        product_id="pdt_pro",
        quantity=1,
        proration_billing_mode="prorated_immediately",
        effective_at="immediately",
        on_payment_failure="prevent_change",
        collect_via_payment_link=True,
    )

    # Hand response.payment_link to your frontend and send the customer there
    print(response.payment_link)
    ```
  </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": "pdt_pro",
        "quantity": 1,
        "proration_billing_mode": "prorated_immediately",
        "effective_at": "immediately",
        "on_payment_failure": "prevent_change",
        "collect_via_payment_link": true
      }'
    ```
  </Tab>
</Tabs>

A successful request returns the checkout handles:

```json theme={null}
{
  "payment_id": "<payment_id>",
  "payment_link": "https://checkout.dodopayments.com/...",
  "client_secret": "<client_secret>",
  "expires_on": "<expires_on>"
}
```

### What Happens While the Link Is Unpaid

* The subscription stays on its **current** plan — `product_id`, `recurring_pre_tax_amount`, and `next_billing_date` are all untouched until the link is paid.
* A further `change-plan` request is rejected with `409 PendingPlanChangeExists` while the link is pending. To replace the pending change, send the new request with `cancel_older_payment_link: true`. See [Replacing a Pending Payment Link](#replacing-a-pending-payment-link).
* To retry after a declined payment, call `change-plan` again to get a new link.
* If the link is never paid, it stops working after `expires_on` — the subscription automatically becomes free to accept a new plan-change request shortly after.
* If a scheduled change already existed and you replace it with `cancel_scheduled_change_plan: true`, the original schedule stays in place while the link is unpaid and is only cancelled once the link is paid — in the same transaction that applies the new plan.

<Warning>
  Once an immediate payment-link change is issued, every further plan-change request on that subscription — including the side-effect-free preview — is blocked until the link resolves. A `change-plan` request can replace the link by setting `cancel_older_payment_link: true`. The preview ignores this field, so a preview stays blocked.
</Warning>

### Redirecting the Customer After Payment

Set `return_url` to send the customer back to your site after they pay the link:

```json theme={null}
{
  "product_id": "pdt_pro",
  "quantity": 1,
  "proration_billing_mode": "prorated_immediately",
  "on_payment_failure": "prevent_change",
  "collect_via_payment_link": true,
  "return_url": "https://example.com/billing/done"
}
```

After checkout, the customer goes to `return_url` with these query parameters:

| Parameter | Value |
| - | - |
| `subscription_id` | The subscription that the plan change is for. |
| `payment_id` | The plan-change payment. |
| `status` | The status of the plan-change payment, not of the subscription: `active` when paid, `failed` when declined, `pending` while in progress. |

When the payment fails, the subscription stays active on its current plan. The new plan can apply shortly after the redirect, when the payment webhook arrives, so read the subscription again before you show the new plan.

`return_url` needs `collect_via_payment_link: true`. Without it, the request fails with `422`.

### Replacing a Pending Payment Link

If the customer leaves the checkout without paying, set `cancel_older_payment_link: true` on the next `change-plan` request. Dodo Payments cancels the unpaid link, and the new plan change replaces the pending one. The cancelled link no longer accepts a payment, even in a checkout page that is already open.

```json theme={null}
{
  "product_id": "pdt_pro",
  "quantity": 1,
  "proration_billing_mode": "prorated_immediately",
  "on_payment_failure": "prevent_change",
  "collect_via_payment_link": true,
  "cancel_older_payment_link": true
}
```

The request is validated before the link is cancelled, so an invalid request keeps the customer's link. The link is cancelled only if the customer has not started to pay:

| Previous payment | Response |
| - | - |
| Not opened, opened but not paid, or declined | `200` with the new link. |
| Customer is paying now, for example during 3D Secure | `409` `PLAN_CHANGE_PAYMENT_IN_PROGRESS`. Retry after the payment completes or fails. |
| Already paid | `409` `PLAN_CHANGE_PAYMENT_ALREADY_COMPLETED`. Don't retry: the previous plan change applies. |
| The link could not be cancelled | `503` `PLAN_CHANGE_LINK_CANCEL_FAILED`. Nothing changed, so you can retry. |

Each `change-plan` request issues its own invoice. The replaced plan change and its invoice are cancelled. If no plan change is pending, `cancel_older_payment_link` has no effect.

<Info>
  A check that runs after validation, for example the minimum charge amount, can still fail after the link is cancelled. The subscription then stays on its current plan with no open link. Send the request again to issue a new link.
</Info>

## Managing Addons

When changing subscription plans, you can also modify addons:

```javascript theme={null}
// Add addons to the new plan
await client.subscriptions.changePlan('sub_123', {
  product_id: 'pdt_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: 'pdt_new',
  quantity: 1,
  proration_billing_mode: 'difference_immediately',
  addons: [] // Empty array removes all existing addons
});
```

<Info>
  Addons are included in the proration calculation and will be charged according to the selected proration mode.
</Info>

## Applying Discount Codes

Apply one or more stacked discount codes when changing subscription plans (max 20, applied in array order):

<Tabs>
  <Tab title="Node.js SDK">
    ```javascript theme={null}
    // Apply stacked discount codes during plan change
    await client.subscriptions.changePlan('sub_123', {
      product_id: 'pdt_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="pdt_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": "pdt_pro",
        "quantity": 1,
        "proration_billing_mode": "prorated_immediately",
        "discount_codes": ["UPGRADE20"]
      }'
    ```
  </Tab>
</Tabs>

### Discount Behavior on Plan Change

| `discount_codes` value | Behavior |
| - | - |
| Not provided / `null` | Existing discounts with `preserve_on_plan_change=true` are automatically preserved if applicable to the new product. |
| `[]` (empty array) | **All** existing discounts are removed from the subscription. |
| `["CODE_A", "CODE_B", ...]` | Replaces any existing discounts with this stacked set, validated and applied in array order. |

<Info>
  The singular `discount_code` field on this endpoint is **deprecated** but still works for backward compatibility — existing integrations don't need to change immediately. It cannot be combined with `discount_codes` in the same request. Migrate to the array form when convenient.
</Info>

<Tip>
  Use the [Preview Plan Change API](/api-reference/subscriptions/preview-change-plan) with `discount_codes` to show customers exactly how much they'll save before confirming the plan change.
</Tip>

## Proration Modes

Choose how to bill the customer when changing plans:

#### `prorated_immediately`

* Credits the unused portion of the current cycle — base plan, quantity, and add-ons — prorated by time remaining
* Then charges a **full** cycle at the new plan, quantity, and add-ons. The charge itself is never prorated
* Net immediate charge = (full new cycle) − (remaining fraction × full old cycle)
* If the credit exceeds the new cycle charge (common on downgrades), the difference is held as subscription-scoped credit for future renewals
* If in trial, charges immediately and switches to the new plan now

#### `full_immediately`

* Charges the full amount of the new plan immediately
* Ignores remaining time from the old plan — no credit for the current cycle

<Info>
  Credits created by <code>prorated\_immediately</code> and by downgrades using <code>difference\_immediately</code> are subscription-scoped and distinct from <a href="/features/credit-based-billing">Credit-Based Billing</a> entitlements. They automatically apply to future renewals of the same subscription and are not transferable between subscriptions.
</Info>

#### `difference_immediately`

* Upgrade: immediately charge the price difference between old and new plans
* Downgrade: add remaining value as internal credit to the subscription and auto-apply on renewals

#### `do_not_bill`

* No charges or credits are calculated
* Customer switches to the new plan immediately without any billing adjustment
* Billing cycle remains unchanged
* Best for courtesy migrations, free plan switches, or absorbing cost differences

| Feature | `prorated_immediately` | `difference_immediately` | `full_immediately` | `do_not_bill` |
| - | - | - | - | - |
| **Upgrade charge** | Full new cycle, minus credit for unused time | Full price difference between plans | Full new plan price | No charge |
| **Downgrade credit** | Credit for unused time, minus a full new cycle | Full price difference as credit | No credit | No credit |
| **Billing cycle** | Resets to today | Resets to today | Resets to today | Unchanged |
| **Trial behavior** | Ends trial, charges immediately | Ends trial, charges immediately | Ends trial, charges full amount | Keeps the trial end date, no charge |
| **Best for** | Crediting unused time | Simple upgrade/downgrade math | Resetting billing cycles | Free migrations or courtesy switches |
| **Complexity** | Medium (day calculation) | Low (simple subtraction) | Low (full charge) | None |

```mermaid theme={null}
flowchart TD
    A{Credit unused time on the old plan?} -->|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]
```

### Example Scenarios

Use these canonical numbers consistently:

* Current plan: **Basic** at **\$30/month**
* Upgrade target: **Pro** at **\$80/month**
* Downgrade target (from Pro): **Starter** at **\$20/month**
* Billing cycle: **30 days**, started on **January 1**
* Plan change happens on **January 16** (15 days remaining, 15 days used)

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

    Step 2: Charge a full cycle at the new plan
      New plan cost = $80.00 (never prorated)

    Step 3: Calculate immediate charge
      Charge = Full new cycle − Credit
      Charge = $80.00 − $15.00 = $65.00

    → Customer pays $65.00 now
    → Customer starts a full month of Pro today
    → 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: 'pdt_pro',
      quantity: 1,
      proration_billing_mode: 'prorated_immediately'
    })
    ```
  </Accordion>

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

    Step 2: Charge a full cycle at the new plan
      New plan cost = $20.00 (never prorated)

    Step 3: Net the two
      Net = $20.00 − $40.00 = −$20.00

    → No charge — $20.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 (credit exhausted)
    → Following renewal (Mar 15): $20.00
    ```

    ```javascript theme={null}
    await client.subscriptions.changePlan('sub_123', {
      product_id: 'pdt_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: 'pdt_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: 'pdt_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: 'pdt_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 the unused time on the current plan
      Credit = $30 × (15/30) = $15.00

    Step 2: Charge a full cycle at the new plan + add-ons
      New plan = $80.00
      Add-ons  = $30.00
      Total new = $110.00 (never prorated)

    Step 3: Immediate charge
      Charge = $110.00 − $15.00 = $95.00

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

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

### How Each Mode Processes Billing

```mermaid theme={null}
flowchart LR
    A[Plan Change] --> B{Proration Mode}
    B -->|prorated| C[Credit unused time]
    C --> D[Charge full new cycle]
    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>
  Pick `prorated_immediately` to credit unused time on the old plan while charging a full cycle of the new one; choose `full_immediately` to restart billing; use `difference_immediately` for simple upgrades and automatic credit on downgrades; or use `do_not_bill` to switch plans without any billing adjustment.
</Tip>

## Handling Payment Failures

Control what happens when a plan change payment fails using the `on_payment_failure` parameter.

### Payment Failure Modes

<Tabs>
  <Tab title="prevent_change (Recommended for critical upgrades)">
    **Behavior**: Keep the subscription on its current plan until payment succeeds.

    * Plan change is marked as "pending"
    * Customer retains access to their current plan
    * Subscription moves to `active` state only after successful payment
    * Useful when you want to ensure payment before granting upgraded features

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

  <Tab title="apply_change (Default)">
    **Behavior**: Apply the plan change immediately regardless of payment outcome.

    * Plan change is applied even if payment fails
    * Customer gets immediate access to the new plan
    * Subscription may move to `on_hold` if payment fails
    * Good for non-critical upgrades or when you trust the customer

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

<Info>
  If not specified, the `on_payment_failure` parameter uses your business-level default setting configured in the dashboard.
</Info>

### When to Use Each Mode

| Scenario | Recommended Mode | Reason |
| - | - | - |
| Upgrading to premium features | `prevent_change` | Ensure payment before granting access |
| Quantity increase (more seats) | `prevent_change` | Prevent usage without payment |
| Downgrading plans | `apply_change` | Customer is reducing spend |
| Trusted enterprise customers | `apply_change` | Lower risk of non-payment |
| Trial to paid conversion | `prevent_change` | Critical payment moment |

## Business & Collection Defaults

Set default upgrade and downgrade behavior at the business level under **Settings → Subscriptions**. These defaults apply to all customer-portal plan changes and can be overridden per product collection.

Separate defaults exist for upgrades and downgrades:

| Setting | Field (upgrade / downgrade) | Default (upgrade) | Default (downgrade) |
| - | - | - | - |
| When the new plan starts | `effective_at_on_upgrade` / `effective_at_on_downgrade` | `immediately` | `next_billing_date` |
| How the customer is charged | `proration_billing_mode_on_upgrade` / `proration_billing_mode_on_downgrade` | `difference_immediately` | `difference_immediately` |
| If the customer's payment fails | `on_payment_failure` | `apply_change` | `apply_change` |

Configure business defaults under **Settings → Subscriptions**, and collection overrides on each product collection. Each collection field is independent — leave it unset to inherit from the business default, or set a value to override it.

### Resolution Order

For any given plan change, each setting resolves in this order:

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

<Info>
  A value passed explicitly to the [Change Plan API](/api-reference/subscriptions/change-plan) always wins. The business and collection defaults only take effect when no explicit value is supplied — which is the case for all plan changes initiated from the customer portal.
</Info>

<Tip>
  A common setup: keep upgrades `immediately` + `difference_immediately` so customers pay the difference and get access right away, and keep downgrades on `next_billing_date` so customers keep their current plan until the cycle ends.
</Tip>

## Handling Webhooks

Track subscription state through webhooks to confirm plan changes and payments.

### Event Types to Handle

* `subscription.active`: subscription activated
* `subscription.plan_changed`: subscription plan changed (upgrade/downgrade/addon changes)
* `subscription.on_hold`: charge failed, renewals stopped
* `subscription.renewed`: renewal succeeded
* `payment.succeeded`: payment for plan change or renewal succeeded
* `payment.failed`: payment failed

<Info>
  Drive business logic from subscription events and use payment events for confirmation and reconciliation.
</Info>

### Verify Signatures and Handle Intents

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

      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_PAYMENTS_WEBHOOK_KEY;
      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>
  For detailed payload schemas, see the <a href="/developer-resources/webhooks/intents/subscription">Subscription webhook payloads</a> and <a href="/developer-resources/webhooks/intents/payment">Payment webhook payloads</a>.
</Note>

## Best Practices

### Plan Change Strategy

* **Test thoroughly**: Always test plan changes in test mode before production
* **Choose proration carefully**: Select the proration mode that aligns with your business model
* **Handle failures gracefully**: Implement proper error handling and retry logic
* **Monitor success rates**: Track plan change success/failure rates and investigate issues

### Webhook Implementation

* **Verify signatures**: Always validate webhook signatures to ensure authenticity
* **Implement idempotency**: Handle duplicate webhook events gracefully
* **Process asynchronously**: Don't block webhook responses with heavy operations
* **Log everything**: Maintain detailed logs for debugging and audit purposes

### User Experience

* **Communicate clearly**: Inform customers about billing changes and timing
* **Provide confirmations**: Send email confirmations for successful plan changes
* **Handle edge cases**: Consider trial periods, prorations, and failed payments
* **Update UI immediately**: Reflect plan changes in your application interface

## Common Issues and Solutions

Resolve typical problems encountered during subscription plan changes:

<AccordionGroup>
  <Accordion title="Charge created but subscription not updated">
    **Symptoms**: API call succeeds but subscription remains on old plan

    **Common causes**:

    * Webhook processing failed or was delayed
    * Application state not updated after receiving webhooks
    * Database transaction issues during state update

    **Solutions**:

    * Implement webhook handling with retry logic
    * Use idempotent operations for state updates
    * Add monitoring to detect and alert on missed webhook events
    * Verify webhook endpoint is accessible and responding correctly
  </Accordion>

  <Accordion title="Credits not applied after downgrade">
    **Symptoms**: Customer downgrades but doesn't see credit balance

    **Common causes**:

    * Proration mode expectations: downgrades credit the full plan price difference with `difference_immediately`, while `prorated_immediately` credits the unused time on the old cycle and then charges a full cycle at the new plan — so a credit balance only remains when that credit exceeds the new plan price
    * Credits are subscription-specific and don't transfer between subscriptions
    * Credit balance not visible in customer dashboard

    **Solutions**:

    * Use `difference_immediately` for downgrades when you want automatic credits
    * Explain to customers that credits apply to future renewals of the same subscription
    * Implement customer portal to show credit balances
    * Check next invoice preview to see applied credits
  </Accordion>

  <Accordion title="Webhook signature verification fails">
    **Symptoms**: Webhook events rejected due to invalid signature

    **Common causes**:

    * Incorrect webhook secret key
    * Raw request body modified before signature verification
    * Wrong signature verification algorithm

    **Solutions**:

    * Verify you're using the correct `DODO_PAYMENTS_WEBHOOK_KEY` from dashboard
    * Read raw request body before any JSON parsing middleware
    * Use the standard webhook verification library for your platform
    * Test webhook signature verification in development environment
  </Accordion>

  <Accordion title="Plan change fails with 422 error">
    **Symptoms**: API returns 422 Unprocessable Entity error

    **Common causes**:

    * Invalid subscription ID or product ID
    * Subscription not in active state
    * Missing required parameters
    * Product not available for plan changes

    **Solutions**:

    * Verify subscription exists and is active
    * Check product ID is valid and available
    * Ensure all required parameters are provided
    * Review API documentation for parameter requirements
  </Accordion>

  <Accordion title="Immediate charge fails during plan change">
    **Symptoms**: Plan change initiated but immediate charge fails

    **Common causes**:

    * Insufficient funds on customer's payment method
    * Payment method expired or invalid
    * Bank declined the transaction
    * Fraud detection blocked the charge

    **Solutions**:

    * Handle `payment.failed` webhook events appropriately
    * Notify customer to update payment method
    * Implement retry logic for temporary failures
    * Consider allowing plan changes with failed immediate charges
  </Accordion>

  <Accordion title="Subscription on hold after plan change">
    **Symptoms**: Plan change charge fails and subscription moves to `on_hold` state

    **What happens**:
    When a plan change charge fails, the subscription is automatically placed in `on_hold` state. The subscription will not renew automatically until the payment method is updated.

    **Solution**: Update the payment method to reactivate the subscription

    To reactivate a subscription from `on_hold` state after a failed plan change:

    1. **Update the payment method** using the Update Payment Method API
    2. **Automatic charge creation**: The API automatically creates a charge for remaining dues
    3. **Invoice generation**: An invoice is generated for the charge
    4. **Payment processing**: The payment is processed using the new payment method
    5. **Reactivation**: Upon successful payment, the subscription is reactivated to `active` state

    <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, {
          payment_method: { 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, {
          payment_method: { 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,
              payment_method={"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,
              payment_method={"type": "existing", "payment_method_id": payment_method_id},
          )
          
          # Monitor webhooks for payment.succeeded and subscription.active
          return response
      ```
    </CodeGroup>

    **Webhook events to monitor**:

    * `subscription.on_hold`: Subscription placed on hold (received when plan change charge fails)
    * `payment.succeeded`: Payment for remaining dues succeeded (after updating payment method)
    * `subscription.active`: Subscription reactivated after successful payment

    **Best practices**:

    * Notify customers immediately when a plan change charge fails
    * Provide clear instructions on how to update their payment method
    * Monitor webhook events to track reactivation status
    * Consider implementing automatic retry logic for temporary payment failures

    <Card title="Update Payment Method API Reference" icon="code" href="/api-reference/subscriptions/update-payment-method">
      View the complete API documentation for updating payment methods and reactivating subscriptions.
    </Card>
  </Accordion>
</AccordionGroup>

## Testing Your Implementation

Thoroughly test your subscription plan change implementation:

<Steps>
  <Step title="Set up test environment">
    * Use test API keys and test products
    * Create test subscriptions with different plan types
    * Configure test webhook endpoint
    * Set up monitoring and logging
  </Step>

  <Step title="Test different proration modes">
    * Test `prorated_immediately` with various billing cycle positions
    * Test `difference_immediately` for upgrades and downgrades
    * Test `full_immediately` to reset billing cycles
    * Test `do_not_bill` for no-charge/no-credit plan switches
    * Verify credit calculations are correct
  </Step>

  <Step title="Test webhook handling">
    * Verify all relevant webhook events are received
    * Test webhook signature verification
    * Handle duplicate webhook events gracefully
    * Test webhook processing failure scenarios
  </Step>

  <Step title="Test error scenarios">
    * Test with invalid subscription IDs
    * Test with expired payment methods
    * Test network failures and timeouts
    * Test with insufficient funds
  </Step>

  <Step title="Monitor in production">
    * Set up alerts for failed plan changes
    * Monitor webhook processing times
    * Track plan change success rates
    * Review customer support tickets for plan change issues
  </Step>
</Steps>

## Error Handling

Handle common API errors gracefully in your implementation:

### HTTP Status Codes

<AccordionGroup>
  <Accordion title="200 OK">
    Plan change request processed successfully. The response body is a `ChangePlanResponse` with `payment_id`, `payment_link`, `client_secret`, and `expires_on`. All four are nullable, so the body serializes as `{}` for an ordinary off-session change; they are populated for a successful `collect_via_payment_link` request, which returns checkout handles — see [Collecting Payment via a Checkout Link](#collecting-payment-via-a-checkout-link). If `on_payment_failure=prevent_change`, the plan change stays pending until payment succeeds.
  </Accordion>

  <Accordion title="400 Bad Request">
    Invalid request parameters. Check that all required fields are provided and properly formatted.
  </Accordion>

  <Accordion title="401 Unauthorized">
    Invalid or missing API key. Verify your `DODO_PAYMENTS_API_KEY` is correct and has proper permissions.
  </Accordion>

  <Accordion title="409 Conflict">
    A pending plan change already exists for this subscription (`PendingPlanChangeExists`). For a *scheduled* change, cancel it with `DELETE /subscriptions/{subscription_id}/change-plan/scheduled` before submitting a new one. For a pending *payment-link* change, send the new request with `cancel_older_payment_link: true` to replace it. See [Replacing a Pending Payment Link](#replacing-a-pending-payment-link).

    With `cancel_older_payment_link: true`, a `409` means the customer is paying the previous link now (`PLAN_CHANGE_PAYMENT_IN_PROGRESS`) or has already paid it (`PLAN_CHANGE_PAYMENT_ALREADY_COMPLETED`).
  </Accordion>

  <Accordion title="503 Service Unavailable">
    With `cancel_older_payment_link: true`, the previous payment link could not be cancelled (`PLAN_CHANGE_LINK_CANCEL_FAILED`). Nothing changed, so you can retry the request.
  </Accordion>

  <Accordion title="422 Unprocessable Entity">
    The subscription is inactive or on-demand, or the request is not eligible for `collect_via_payment_link` — the business doesn't have the capability enabled, `effective_at` isn't `immediately`, or `on_payment_failure` isn't `prevent_change`. See [Requirements](#requirements). A subscription ID that doesn't exist or doesn't belong to your account returns `404` with the `NOT_FOUND` code.
  </Accordion>

  <Accordion title="500 Internal Server Error">
    Server error occurred. Retry the request after a brief delay.
  </Accordion>
</AccordionGroup>

### Error Response Format

Errors return a JSON body with a `code` and a human-readable `message`:

```json theme={null}
{
  "code": "INACTIVE_SUBSCRIPTION_PLAN_CHANGE_NOT_SUPPORTED",
  "message": "Changing plans is not supported for inactive subscriptions"
}
```

See [Error Codes](/api-reference/error-codes) for the full list.

## Next Steps

* Review the <a href="/api-reference/subscriptions/change-plan">Change Plan API</a>
* Explore <a href="/features/credit-based-billing">Credit-Based Billing</a>
* Implement alerts for `subscription.on_hold`
* Check out the <a href="/developer-resources/webhooks">Webhook Integration Guide</a>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.