> ## 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 Integration Guide

> Dodo Payments subscriptions let you charge customers on a recurring schedule. Create subscriptions through checkout sessions, handle webhooks, change plans with proration, and recover failed renewals.

## Prerequisites

Before you start, you need:

* A Dodo Payments merchant account
* An API key from **Developer → API Keys** in the dashboard, stored in `DODO_PAYMENTS_API_KEY`
* A webhook secret from **Developer → Webhooks**, stored in `DODO_PAYMENTS_WEBHOOK_KEY`
* At least one subscription product created under **Products**

For more details, see [Integration Guide Prerequisites](/developer-resources/integration-guide#prerequisites).

## API Integration

### Checkout Sessions

Create a subscription by building a checkout session with your subscription product. The customer authorizes a payment method and the subscription activates when they complete checkout.

<Tip>
  You can combine a subscription product with one-time products in the same checkout session. This enables setup fees, hardware bundles with SaaS, and similar use cases. See [Checkout Sessions](/developer-resources/checkout-session) for examples.

  You can also sell two or more subscription products in one checkout. The customer pays once and gets one independent subscription per product, each with its own billing cycle. Such a cart can't hold one-time products. See [Multi-Subscription Cart](/developer-resources/checkout-session#multi-subscription-cart).
</Tip>

<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 main() {
      const session = await client.checkoutSessions.create({
        product_cart: [
          { product_id: 'pdt_subscription_monthly', quantity: 1 }
        ],
        // Optional: configure trials for subscription products
        subscription_data: { trial_period_days: 14 },
        customer: {
          email: 'subscriber@example.com',
          name: 'Jane Doe',
        },
        return_url: 'https://example.com/success',
      });

      console.log(session.checkout_url);
    }

    main();
    ```
  </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"
    )

    session = client.checkout_sessions.create(
        product_cart=[
            {"product_id": "pdt_subscription_monthly", "quantity": 1}
        ],
        subscription_data={"trial_period_days": 14},  # optional
        customer={
            "email": "subscriber@example.com",
            "name": "Jane Doe",
        },
        return_url="https://example.com/success",
    )

    print(session.checkout_url)
    ```
  </Tab>

  <Tab title="REST API">
    ```javascript theme={null}
    const response = await fetch('https://test.dodopayments.com/checkouts', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${process.env.DODO_PAYMENTS_API_KEY}`
      },
      body: JSON.stringify({
        product_cart: [
          { product_id: 'pdt_subscription_monthly', quantity: 1 }
        ],
        subscription_data: { trial_period_days: 14 }, // optional
        customer: {
          email: 'subscriber@example.com',
          name: 'Jane Doe'
        },
        return_url: 'https://example.com/success'
      })
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const session = await response.json();
    console.log(session.checkout_url);
    ```
  </Tab>
</Tabs>

### API Response

The response includes a `checkout_url`:

```json theme={null}
{
  "session_id": "cks_Gi6KGJ2zFJo9rq9Ukifwa",
  "checkout_url": "https://test.checkout.dodopayments.com/session/cks_Gi6KGJ2zFJo9rq9Ukifwa"
}
```

Redirect the customer to this URL. They authorize the payment method and the subscription activates.

### Webhooks

Webhooks notify your server when subscription events occur. Set up your endpoint under **Developer → Webhooks** in the dashboard.

To set up your webhook endpoint, see [Webhooks](/developer-resources/integration-guide#webhooks).

#### Subscription Event Types

Track these events to manage the subscription lifecycle:

1. **`subscription.active`** — Subscription is activated
2. **`subscription.updated`** — A field on the subscription changed
3. **`subscription.on_hold`** — A renewal or plan-change charge failed
4. **`subscription.failed`** — Subscription creation failed (terminal; customer must resubscribe)
5. **`subscription.renewed`** — A recurring charge succeeded
6. **`subscription.past_due`** — A renewal failed and the grace period started; the customer keeps access until `past_due_ends_at`
7. **`subscription.plan_changed`** — The plan was upgraded, downgraded, or changed
8. **`subscription.cancelled`** — The subscription was cancelled
9. **`subscription.expired`** — The subscription reached the end of its term

These are the core events. For the full list, including `paused`, `unpaused`, and `update_payment_method`, see [Subscription Webhooks](/developer-resources/webhooks/intents/subscription).

<Tip>
  Use `subscription.updated` to get real-time notifications about any subscription changes, keeping your application state in sync without polling the API.
</Tip>

#### Payment Scenarios

**Successful Payment Flow**

The webhook sequence depends on whether the subscription has a trial.

*Immediate billing (0 trial days):*

1. `subscription.active`: the mandate is authorized and the subscription is activated.
2. `payment.succeeded`: confirms the first charge. Expect this within **2–10 minutes** of checkout.

*With a trial period:*

1. **At trial start (checkout):** `subscription.active` fires once the payment method is authorized. **No recurring charge is taken yet.** The first real charge is deferred until the trial ends.
2. **At trial end:** the recurring amount is charged, and you receive `payment.succeeded` **together with** `subscription.renewed`.

*Every subsequent renewal:*

* `subscription.renewed`: fires on each billing cycle when the renewal payment is deducted, **always alongside** `payment.succeeded`. It also carries the updated `next_billing_date`.

<Info>
  Whenever money is actually deducted for a subscription product, you get `subscription.renewed` **and** `payment.succeeded`. Use `subscription.renewed` (rather than `payment.succeeded` alone) as your signal to extend access for the next cycle.
</Info>

**Payment Failure Scenarios**

1. Subscription Failure

* `subscription.failed` - Subscription creation failed due to failure to create a mandate.
* `payment.failed` - Indicates failed payment.

2. Subscription On Hold

* `subscription.on_hold` - Subscription is put on hold due to failed renewal payment or failed plan change charge. If your business has a grace period, a failed renewal first moves the subscription to `past_due` (`subscription.past_due`), and it moves to `on_hold` (or `cancelled`, depending on your grace period settings) only when the grace period ends. See [Subscription States](/features/subscription).
* When a subscription goes on hold, it will not renew automatically until the payment method is updated.

<Info>**Best Practice**: To simplify implementation, we recommend primarily tracking subscription events for managing the subscription lifecycle.</Info>

<Tip>
  For a complete walkthrough of reading `error_code`/`error_message`, deciding when to retry, and surfacing failures to customers, see [Handle Payment Failures](/developer-resources/handle-payment-failures).
</Tip>

#### `subscription.failed` vs. `subscription.on_hold`

These two events are easy to confuse, but they require very different handling:

| Event | When it fires | Status | Recoverable? | What to do |
| - | - | - | - | - |
| `subscription.failed` | The **initial** mandate could not be created at subscription **creation** | `failed` | **No (terminal)** | Do not grant access. Ask the customer to start a **new** subscription with a different payment method. |
| `subscription.on_hold` | A **renewal** payment (or plan-change charge) failed on an already-active subscription | `on_hold` | **Yes** | Recover by updating the payment method; see [Handling Subscription On Hold](#handling-subscription-on-hold) below. |

<Warning>
  `subscription.failed` is terminal. The subscription cannot be reactivated. The customer must create a new subscription. Never grant entitlements when this event fires.
</Warning>

### Handling Subscription On Hold

When a subscription enters `on_hold` state, you need to update the payment method to reactivate it. This section explains when subscriptions go on hold and how to handle them.

#### When Subscriptions Go On Hold

A subscription is placed on hold when:

* **Renewal payment fails**: The automatic renewal charge fails due to insufficient funds, expired card, or bank decline
* **Plan change charge fails**: An immediate charge during plan upgrade/downgrade fails
* **Payment method authorization fails**: The payment method cannot be authorized for recurring charges

<Warning>
  Subscriptions in `on_hold` state will not renew automatically. You must update the payment method to reactivate the subscription.
</Warning>

#### Reactivating Subscriptions from On Hold

To reactivate a subscription from `on_hold` state, use the Update Payment Method API. This automatically:

1. Creates a charge for remaining dues
2. Generates an invoice for the charge
3. Processes the payment using the new payment method
4. Reactivates the subscription to `active` state upon successful payment

<Steps>
  <Step title="Handle subscription.on_hold webhook">
    When you receive a `subscription.on_hold` webhook, update your application state and notify the customer:

    ```javascript theme={null}
    // Webhook handler
    app.post('/webhooks/dodo', async (req, res) => {
      const event = req.body;
      
      if (event.type === 'subscription.on_hold') {
        const subscription = event.data;
        
        // Update subscription status in your database
        await updateSubscriptionStatus(subscription.subscription_id, 'on_hold');
        
        // Notify customer to update payment method
        await sendEmailToCustomer(subscription.customer.customer_id, {
          subject: 'Payment Required - Subscription On Hold',
          message: 'Your subscription is on hold. Please update your payment method to continue service.'
        });
      }
      
      res.json({ received: true });
    });
    ```
  </Step>

  <Step title="Update payment method">
    When the customer is ready to update their payment method, call the Update Payment Method API:

    <CodeGroup>
      ```javascript Node.js theme={null}
      // Update with new payment method
      const response = await client.subscriptions.updatePaymentMethod(subscriptionId, {
        payment_method: { type: 'new', return_url: 'https://example.com/return' }
      });

      // For on_hold subscriptions, a charge is automatically created
      if (response.payment_id) {
        console.log('Charge created for remaining dues:', response.payment_id);
        // Redirect customer to response.payment_link to complete payment
      }
      ```

      ```python Python theme={null}
      # Update with new payment method
      response = client.subscriptions.update_payment_method(
          subscription_id=subscription_id,
          payment_method={"type": "new", "return_url": "https://example.com/return"},
      )

      # For on_hold subscriptions, a charge is automatically created
      if response.payment_id:
          print("Charge created for remaining dues:", response.payment_id)
          # Redirect customer to response.payment_link to complete payment
      ```
    </CodeGroup>

    <Info>
      You can also use an existing payment method ID if the customer has saved payment methods:

      ```javascript theme={null}
      await client.subscriptions.updatePaymentMethod(subscriptionId, {
        payment_method: { type: 'existing', payment_method_id: 'pm_abc123' }
      });
      ```
    </Info>
  </Step>

  <Step title="Monitor webhook events">
    After updating the payment method, monitor for these webhook events:

    1. **`payment.succeeded`** - The charge for remaining dues was successful
    2. **`subscription.active`** - The subscription has been reactivated

    ```javascript theme={null}
    if (event.type === 'payment.succeeded') {
      const payment = event.data;
      
      // Check if this payment is for an on_hold subscription
      if (payment.subscription_id) {
        // Wait for subscription.active webhook to confirm reactivation
      }
    }

    if (event.type === 'subscription.active') {
      const subscription = event.data;
      
      // Update subscription status in your database
      await updateSubscriptionStatus(subscription.subscription_id, 'active');
      
      // Restore customer access
      await restoreCustomerAccess(subscription.customer.customer_id);
      
      // Notify customer of successful reactivation
      await sendEmailToCustomer(subscription.customer.customer_id, {
        subject: 'Subscription Reactivated',
        message: 'Your subscription has been reactivated successfully.'
      });
    }
    ```
  </Step>
</Steps>

### Sample Subscription Event Payload

***

| Property | Type | Required | Description |
| - | - | - | - |
| `business_id` | string | Yes | The unique identifier for the business |
| `timestamp` | string | Yes | The timestamp of when the event occurred (not necessarily the same as when it was delivered) |
| `type` | string | Yes | The type of event. See [Subscription Event Types](#subscription-event-types) |
| `data` | object | Yes | The main data payload. See [Subscription webhook payloads](/developer-resources/webhooks/intents/subscription) |

## Changing Subscription Plans

You can upgrade or downgrade a subscription plan using the change plan API endpoint. This allows you to modify the subscription's product, quantity, and handle proration.

<Card title="Change Plan API Reference" icon="arrows-rotate" href="/api-reference/subscriptions/change-plan">
  For detailed information about changing subscription plans, please refer to our Change Plan API documentation.
</Card>

### Proration Options

When changing subscription plans, you have four options for handling the immediate charge:

#### 1. `prorated_immediately`

* Credits the unused portion of the current billing cycle, prorated by the time remaining. The credit covers the base plan, quantity, and any add-ons
* 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) minus (remaining fraction x full old cycle). If the credit is larger, the difference is held as subscription-scoped credit for future renewals
* During a trial period, this will immediately switch the user to the new plan, charging the customer right away

#### 2. `full_immediately`

* Charges the customer the full subscription amount for the new plan with no credit for the previous cycle
* On upgrade or downgrade alike, the customer pays the entire new plan price from scratch
* Useful when you want to charge the full amount regardless of how much time was left on the old plan

#### 3. `difference_immediately`

* The customer pays only the gap between the old plan price and the new plan price
* The amount does not depend on when in the cycle the change is made. The same upgrade costs the same on day 1 and on day 29
* When upgrading, the customer is immediately charged the difference. For example, \$30/month → \$80/month = \$50 charged instantly
* When downgrading, the price difference is stored as subscription-scoped credit and applied automatically to future renewals. For example, \$50/month → \$20/month = \$30 stored as credit

#### 4. `do_not_bill`

* Applies the plan change immediately but does **not** charge anything at the time of the change. The new plan, quantity, and add-ons are usable straight away
* Because nothing is charged now, an **upgrade** gives the customer the higher plan free for the remainder of the current cycle. A **downgrade** takes effect immediately with no credit for the unused portion of the cycle they have already paid for
* Add-ons granted through `do_not_bill` are not credited on a later plan change, because they were never billed. A subsequent change bills the new add-on quantity in full
* The updated plan (and quantity/add-ons) is billed at the **next scheduled renewal**, and the **original billing date is preserved**

<Warning>
  **All three "charge now" modes reset the billing cycle.** `prorated_immediately`, `difference_immediately`, and `full_immediately` move the subscription's `next_billing_date` to the change date. Only `do_not_bill` keeps the original renewal date, but it applies no immediate charge.
</Warning>

### Behavior

* When you invoke this API, Dodo Payments immediately initiates a charge based on your selected proration option
* With `prorated_immediately`, a credit for the unused portion of the current cycle is calculated on every change, upgrade or downgrade alike. If that credit exceeds the new cycle charge, the remainder is added to the subscription's credit balance. These credits are specific to that subscription and will only be used to offset future recurring payments of the same subscription
* With `difference_immediately`, the net is always the exact price difference. For downgrades, the excess is stored as subscription-scoped credit, the same as `prorated_immediately`
* The `full_immediately` option bypasses credit calculations and charges the complete new plan amount
* The `do_not_bill` option applies the change immediately but defers billing to the next renewal date, which is preserved

<Tip>
  **Choosing a proration mode:**

  * **`difference_immediately`** — customer pays the price difference. The most predictable option; the charge is the same regardless of when in the cycle the change is made.
  * **`prorated_immediately`** — customer is credited only for unused time on the current cycle. The charge varies depending on when in the cycle the change happens.
  * **`full_immediately`** — customer pays the full new plan amount. No credit for the previous cycle.
  * **`do_not_bill`** — no charge now. The new plan is billed at the next renewal. The only mode that preserves the original billing date.
</Tip>

### Charge Processing

* The immediate charge initiated upon plan change usually completes processing in less than 2 minutes
* If this immediate charge fails for any reason, the subscription is automatically placed on hold until the issue is resolved

## On-Demand Subscriptions

<Info>
  On-demand subscriptions let you charge customers flexibly, not just on a fixed schedule. This feature is available for all accounts.
</Info>

**To create an on-demand subscription:**

To create an on-demand subscription, use the [POST /checkouts](/api-reference/checkout-sessions/create) API endpoint and include the `subscription_data.on_demand` field in your request body. This allows you to authorize a payment method without an immediate charge, or set a custom initial price.

<Warning>
  `POST /subscriptions` is **deprecated**. It still works for existing integrations, but new integrations should create on-demand subscriptions through a [Checkout Session](/developer-resources/checkout-session) (`POST /checkouts`) with `subscription_data.on_demand`. See the [On-Demand Subscriptions Guide](/developer-resources/ondemand-subscriptions) for the current flow.
</Warning>

**To charge an on-demand subscription:**

For subsequent charges, use the [POST /subscriptions/{subscription_id}/charge](/api-reference/subscriptions/create-charge) endpoint and specify the amount to charge the customer for that transaction.

<Note>
  For a complete, step-by-step guide (including request/response examples, safe retry policies, and webhook handling), see the <a href="/developer-resources/ondemand-subscriptions">On-Demand Subscriptions Guide</a>.
</Note>

## Key Things to Know About Subscription Billing

<Warning>
  **Set the subscription period longer than the payment frequency.** If the subscription period equals the payment frequency (e.g. period = 1 month, frequency = 1 month), the subscription is valid for a **single cycle** and then moves to `expired` instead of renewing. For an ongoing monthly plan, set a long subscription period (e.g. 20 years) with a monthly payment frequency.
</Warning>

<Warning>
  **Currency locks at the first successful charge.** Always pass `billing_currency` **and** `billing_address.country` explicitly when creating the checkout. If omitted, they're detected from the customer's IP (Adaptive Currency), and once the subscription takes its first charge the currency is fixed for its lifetime. A customer who later travels can't switch it.
</Warning>

<Info>
  **Trials take a \$0 authorization, not a charge.** When a subscription has a trial, the trial start creates a **\$0 mandate authorization** to save the card; the first real charge happens when the trial ends. In the payments list, a subscription in a free trial shows exactly one payment with `total_amount` of 0. A paid trial charges its `trial_amount` upfront instead.
</Info>

<Info>
  **Subscription lifecycle:** `past_due` = a renewal failed and the grace period is running (the customer keeps access). `on_hold` = a renewal failed (recoverable: prompt the customer to update their payment method; dunning retries apply). `expired` = the term ended without renewal and **cannot be reactivated**. The customer must resubscribe. `cancelled` = ended by the customer or merchant. Most renewal failures are **issuer-side declines** (insufficient funds, card declined), not a Dodo error.
</Info>

<Warning>
  **Indian cards run on an RBI e-mandate.** Off-session charges (renewals and plan-change charges) can take **up to \~48 hours** to settle, and recurring auto-debits **above ₹15,000** require fresh customer authentication (so an upgrade crossing that limit can't ride the existing mandate). While one charge is still `processing`, a second charge on the same subscription fails with *"Cannot create new charge as previous payment is not successful yet."* Non-Indian cards confirm near-instantly.
</Warning>

<Tip>
  **Subscriptions have a \$1.00 minimum in USD.** Checkout rejects a lower total with `TOTAL_PAYMENT_AMOUNT_BELOW_MINIMUM_AMOUNT`. Currencies other than USD, EUR, and GBP must also be worth at least \$1.00; see [Minimum Amounts](/features/adaptive-currency#minimum-amounts). An on-demand charge `product_price` below `100` in the smallest currency unit is rejected with `product_price: value out of range`. A subscription product priced at exactly `$0` is allowed; see [Card-Optional at Zero Price](/features/subscription#card-optional-at-zero-price). To authorize a card without charging it, use an on-demand `mandate_only` setup.
</Tip>

## Related API Reference

<CardGroup cols={2}>
  <Card title="Create Subscription (Deprecated)" icon="code" href="/api-reference/subscriptions/post-subscriptions">
    Legacy API for creating a subscription directly. Use Checkout Sessions for new integrations
  </Card>

  <Card title="Change Subscription Plan" icon="arrows-rotate" href="/api-reference/subscriptions/change-plan">
    API reference for upgrading, downgrading, or changing subscription plans with proration options
  </Card>

  <Card title="Update Payment Method" icon="credit-card" href="/api-reference/subscriptions/update-payment-method">
    API reference for updating payment methods and reactivating on-hold subscriptions
  </Card>

  <Card title="Patch Subscription" icon="pen" href="/api-reference/subscriptions/patch-subscriptions">
    API reference for updating subscription details and configuration
  </Card>
</CardGroup>


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