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

# On-Demand Subscriptions

> Integrate on-demand subscriptions by authorizing mandates, creating variable charges, handling webhooks, and implementing safe retry policies.

## Overview

On-demand subscriptions let you authorize a customer's payment method once and then charge variable amounts whenever you need, instead of on a fixed schedule. This feature is available for all accounts—no approval required.

Use this guide to:

* Create an on-demand subscription (authorize a mandate with optional initial price)
* Trigger subsequent charges with custom amounts
* Track outcomes using webhooks

For a general subscription setup, see the [Subscription Integration Guide](/developer-resources/subscription-integration-guide).

## Prerequisites

* Dodo Payments merchant account and API key
* Webhook secret configured and an endpoint to receive events
* A subscription product in your catalog

<Tip>
  Hướng dẫn này tạo đăng ký theo yêu cầu thông qua phiên thanh toán (`POST /checkouts`), luôn trả về một `checkout_url` được lưu trữ. Chuyển hướng khách hàng đến đó để phê duyệt yêu cầu, và đặt `return_url` tới nơi họ nên đến sau đó.
</Tip>

## How on-demand works

1. You create a subscription with the `on_demand` object to authorize a payment method and optionally collect an initial charge.
2. Later, you create charges against that subscription with custom amounts using the dedicated charge endpoint.
3. You listen to webhooks (e.g., `payment.succeeded`, `payment.failed`) to update your system.

## Create an on-demand subscription

Endpoint: [POST /checkouts](/api-reference/checkout-sessions/create)

Key request fields (body):\
Please find them in [Create Checkout Session](/api-reference/checkout-sessions/create)

### Create an on-demand subscription

<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 subscription = await client.checkoutSessions.create({
        product_cart: [{ product_id: 'pdt_123', quantity: 1 }],
        billing_address:  { city: 'SF', country: 'US', state: 'CA', street: '1 Market St', zipcode: '94105' },
        customer: { customer_id: 'cus_123' },
        return_url: 'https://example.com/billing/success',
        subscription_data: {
            on_demand: {
                mandate_only: true // set false to collect an initial charge
                // product_price: 1000, // optional: charge $10.00 now if mandate_only is false
                // product_currency: 'USD',
                // product_description: 'Custom initial charge',
                // adaptive_currency_fees_inclusive: false,
            }
        }
      });

      console.log(subscription.checkout_url);
    }

    main().catch(console.error);
    ```
  </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"
    )

    checkout = client.checkout_sessions.create(
        product_cart=[
            {"product_id": "pdt_123", "quantity": 1}
        ],
        billing_address={
            "city": "SF",
            "country": "US",
            "state": "CA",
            "street": "1 Market St",
            "zipcode": "94105",
        },
        customer={
            "customer_id": "cus_123",
        },
        return_url="https://example.com/billing/success",
        subscription_data={
          "on_demand":{
          "mandate_only": True,  # set False to collect an initial charge
          # "product_price": 1000,  # optional: charge $10.00 now if mandate_only is false
          # "product_currency": "USD",
          # "product_description": "Custom initial charge",
          # "adaptive_currency_fees_inclusive": False,
          }
        },
    )

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

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

    import (
        "context"
        "fmt"
        "os"

        "github.com/dodopayments/dodopayments-go"
        "github.com/dodopayments/dodopayments-go/option"
    )

    func main() {
        client := dodopayments.NewClient(
            option.WithBearerToken(os.Getenv("DODO_PAYMENTS_API_KEY")),
            option.WithEnvironmentTestMode(), // defaults to live mode
        )

        checkout, err := client.CheckoutSessions.New(context.TODO(), dodopayments.CheckoutSessionNewParams{
            CheckoutSessionRequest: dodopayments.CheckoutSessionRequestParam{
                ProductCart: dodopayments.F([]dodopayments.ProductItemReqParam{
                    {
                        ProductID: dodopayments.F("pdt_123"),
                        Quantity:  dodopayments.F(int64(1)),
                    },
                }),
                BillingAddress: dodopayments.F(dodopayments.CheckoutSessionBillingAddressParam{
                    City:    dodopayments.F("SF"),
                    Country: dodopayments.F(dodopayments.CountryCodeUs),
                    State:   dodopayments.F("CA"),
                    Street:  dodopayments.F("1 Market St"),
                    Zipcode: dodopayments.F("94105"),
                }),
                Customer: dodopayments.F[dodopayments.CustomerRequestUnionParam](
                    dodopayments.AttachExistingCustomerParam{
                        CustomerID: dodopayments.F("cus_123"),
                    },
                ),
                ReturnURL: dodopayments.F("https://example.com/billing/success"),
                SubscriptionData: dodopayments.F(dodopayments.SubscriptionDataParam{
                    OnDemand: dodopayments.F(dodopayments.OnDemandSubscriptionParam{
                        MandateOnly: dodopayments.F(true), // set false to collect an initial charge
                        // ProductPrice:    dodopayments.F(int64(1000)), // optional: charge $10.00 now if mandate_only is false
                        // ProductCurrency: dodopayments.F(dodopayments.CurrencyUsd),
                        // ProductDescription: dodopayments.F("Custom initial charge"),
                    }),
                }),
            },
        })
        if err != nil {
            panic(err)
        }

        fmt.Println(checkout.CheckoutURL)
    }
    ```
  </Tab>

  <Tab title="cURL">
    ```bash theme={null}
    curl -X POST "$DODO_API/checkouts" \
      -H "Authorization: Bearer $DODO_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "product_cart": [
          {
            "product_id": "pdt_123",
            "quantity": 1
          }
        ],
        "customer": {
          "customer_id": "cus_123"
        },
        "billing_address": {
          "street": "1 Market St",
          "city": "SF",
          "state": "CA",
          "country": "US",
          "zipcode": "94105"
        },
        "subscription_data": {
          "on_demand": {
            "mandate_only": true
          }
        },
        "return_url": "https://example.com/billing/success"
      }'
    ```
  </Tab>
</Tabs>

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

## Charge an on-demand subscription

After the mandate is authorized, create charges as needed.

Endpoint: [POST /subscriptions/\{subscription\_id}/charge](/api-reference/subscriptions/create-charge)

Key request fields (body):

<AccordionGroup>
  <Accordion title="Charge request body parameters">
    <ParamField body="product_price" type="integer" required>
      Amount to charge (in the smallest currency unit). Example: to charge \$25.00, pass <code>2500</code>.
    </ParamField>

    <ParamField body="product_currency" type="string">
      Optional currency override for the charge.
    </ParamField>

    <ParamField body="product_description" type="string">
      Optional description override for this charge.
    </ParamField>

    <ParamField body="adaptive_currency_fees_inclusive" type="boolean">
      If true, includes adaptive currency fees within <code>product\_price</code>. If false, fees are added on top.
    </ParamField>

    <ParamField body="metadata" type="object">
      Additional metadata for the payment. If omitted, the subscription metadata is used.
    </ParamField>
  </Accordion>
</AccordionGroup>

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

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

    async function chargeNow(subscriptionId) {
      const res = await client.subscriptions.charge(subscriptionId, { product_price: 2500 });
      console.log(res.payment_id);
    }

    chargeNow('sub_123').catch(console.error);
    ```
  </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'))

    response = client.subscriptions.charge(
        subscription_id="sub_123",
        product_price=2500,
    )

    print(response.payment_id)
    ```
  </Tab>

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

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

    func main() {
      client := dodopayments.NewClient(option.WithBearerToken("YOUR_API_KEY"))
      res, err := client.Subscriptions.Charge(context.TODO(), "sub_123", dodopayments.SubscriptionChargeParams{
        ProductPrice: dodopayments.F(int64(2500)),
      })
      if err != nil { panic(err) }
      fmt.Println(res.PaymentID)
    }
    ```
  </Tab>

  <Tab title="cURL">
    ```bash theme={null}
    curl -X POST "$DODO_API/subscriptions/sub_123/charge" \
      -H "Authorization: Bearer $DODO_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "product_price": 2500,
        "product_description": "Extra usage for March"
      }'
    ```
  </Tab>
</Tabs>

```json Success theme={null}
{ "payment_id": "pay_abc123" }
```

<Warning>
  Charging a subscription that is not on-demand may fail. Ensure the subscription has `on_demand: true` in its details before charging.
</Warning>

## Handling failed charges

When a charge against an on-demand subscription fails, you decide what happens next. Unlike scheduled subscriptions — where a failed renewal stops further automatic billing — **on-demand subscriptions remain chargeable after a failure**. You can call the charge endpoint again as part of your own retry logic.

### What happens on failure

<Steps>
  <Step title="Charge attempt fails">
    The `POST /subscriptions/{subscription_id}/charge` request either returns an error response or completes asynchronously and emits a `payment.failed` webhook with the decline reason.
  </Step>

  <Step title="Subscription may transition to on_hold">
    The subscription may move to the `on_hold` state and emit a `subscription.on_hold` webhook (see [Subscription States → On Hold](/features/subscription#on-hold-state)). This is a signal — not a lock. For on-demand subscriptions, `on_hold` does **not** prevent you from charging again.
  </Step>

  <Step title="Retry the charge (your call)">
    For on-demand flows, Dodo does **not** auto-retry. You can call `POST /subscriptions/{subscription_id}/charge` again at any time to retry. Apply the [safe retry policy](#payment-retries) below — use exponential backoff, skip hard declines, and avoid burst patterns — so retries are not flagged by our fraud and risk systems.
  </Step>

  <Step title="Optionally, ask the customer for a new payment method">
    Nếu các lần thử lại không thành công vì phương thức thanh toán bị lỗi (thẻ hết hạn, tài khoản đóng, v.v.), sử dụng [`POST /subscriptions/{subscription_id}/update-payment-method`](/api-reference/subscriptions/update-payment-method) để thu thập phương thức mới từ khách hàng. Khi thành công, đăng ký sẽ trở về `active` và `payment.succeeded` tiếp theo sẽ gửi các webhook `subscription.active`.
  </Step>
</Steps>

<Info>
  **On-demand vs scheduled**: For scheduled subscriptions, Dodo runs its own renewal retries and dunning. For on-demand subscriptions, you own the retry policy because only you know when the next charge should occur (it's driven by your usage events, not a calendar).
</Info>

### Webhook sequence on a failed on-demand charge

| Order | Event                  | Meaning                                                                              |
| ----- | ---------------------- | ------------------------------------------------------------------------------------ |
| 1     | `payment.failed`       | The on-demand charge attempt did not succeed (includes the decline reason)           |
| 2     | `subscription.on_hold` | The subscription was placed on hold (informational; does not block further charges)  |
| 3\*   | `payment.succeeded`    | A subsequent charge — either your retry or after a payment method update — succeeded |
| 4\*   | `subscription.active`  | The subscription returned to `active` after a successful charge                      |

<Info>
  Events 3 and 4 only fire after a follow-up charge succeeds.
</Info>

### Retry responsibility

<Warning>
  Dodo Payments **does not auto-retry failed on-demand charges**. You own the retry policy. Follow the safe retry guidelines below to avoid being flagged by our fraud detection systems as card testing.
</Warning>

[Subscription Dunning](/features/recovery/subscription-dunning) — the built-in email recovery sequence — is scoped to failed *renewal* payments on scheduled subscriptions and customer-initiated cancellations. It is not designed for on-demand charge failures. Communicate with the customer directly (e.g., transactional email or in-app prompt) when you decide the payment method needs to be updated.

## Payment retries

Our fraud detection system may block aggressive retry patterns (and can flag them as potential card testing). Follow a safe retry policy.

<Warning>
  Burst retry patterns can be flagged as fraudulent or suspected card testing by our risk systems and processors. Avoid clustered retries; follow the backoff schedule and time alignment guidance below.
</Warning>

### Principles for safe retry policies

* **Backoff mechanism**: Use exponential backoff between retries.
* **Retry limits**: Cap total retries (3–4 attempts max).
* **Intelligent filtering**: Retry only on retryable failures (e.g., network/issuer errors, insufficient funds); never retry hard declines.
* **Card testing prevention**: Do not retry failures like `DO_NOT_HONOR`, `STOLEN_CARD`, `LOST_CARD`, `PICKUP_CARD`, `FRAUDULENT`, `AUTHENTICATION_FAILURE`.
* **Vary metadata (optional)**: If you maintain your own retry system, differentiate retries via metadata (e.g., `retry_attempt`).

### Suggested retry schedule (subscriptions)

* **1st attempt**: Immediate when you create the charge
* **2nd attempt**: After 3 days
* **3rd attempt**: After 7 more days (10 days total)
* **4th attempt (final)**: After another 7 days (17 days total)

Final step: if still unpaid, mark the subscription as unpaid or cancel it, based on your policy. Notify the customer during the window to update their payment method.

### Avoid burst retries; align to authorization time

* Anchor retries to the original authorization timestamp to avoid "burst" behavior across your portfolio.
* Example: If the customer starts a trial or mandate at 1:10 pm today, schedule follow-up retries at 1:10 pm on subsequent days per your backoff (e.g., +3 days → 1:10 pm, +7 days → 1:10 pm).
* Alternatively, if you store the last successful payment time `T`, schedule the next attempt at `T + X days` to preserve time-of-day alignment.

<Note>
  Time-zone and DST: use a consistent time standard for scheduling and convert for display only to maintain intervals.
</Note>

### Decline codes you should not retry

* `STOLEN_CARD`
* `DO_NOT_HONOR`
* `FRAUDULENT`
* `PICKUP_CARD`
* `AUTHENTICATION_FAILURE`
* `LOST_CARD`

<Note>
  For a comprehensive list of decline reasons and whether they are user-correctable, see the
  [Transaction Failures](/api-reference/transaction-failures) documentation.
</Note>

<Tip>
  Only retry on soft/temporary issues (e.g., `insufficient_funds`, `issuer_unavailable`, `processing_error`, network timeouts). If the same decline repeats, pause further retries.
</Tip>

### Implementation guidelines (no code)

* Use a scheduler/queue that persists precise timestamps; compute next attempt at the exact time-of-day offset (e.g., `T + 3 days` at the same HH:MM).
* Maintain and reference the last successful payment timestamp `T` to compute the next attempt; do not bunch multiple subscriptions at the same instant.
* Always evaluate the last decline reason; stop retries for hard declines in the skip list above.
* Cap concurrent retries per customer and per account to prevent accidental surges.
* Communicate proactively: email/SMS the customer to update their payment method before the next scheduled attempt.
* Use metadata only for observability (e.g., `retry_attempt`); never try to "evade" fraud/risk systems by rotating inconsequential fields.

## Track outcomes with webhooks

Implement webhook handling to track the customer journey. See [Implementing Webhooks](/developer-resources/integration-guide#implementing-webhooks).

* **subscription.active**: Mandate authorized and subscription activated
* **subscription.failed**: Creation failed (e.g., mandate failure)
* **subscription.on\_hold**: Subscription placed on hold (e.g., unpaid state)
* **payment.succeeded**: Charge succeeded
* **payment.failed**: Charge failed

<Tip>
  For on-demand flows, focus on `payment.succeeded` and `payment.failed` to reconcile usage-based charges. When `payment.failed` is followed by `subscription.on_hold`, see [Handling failed charges](#handling-failed-charges) to recover the subscription.
</Tip>

## Testing and next steps

<Steps>
  <Step title="Create in test mode">
    Use your test API key to create the subscription with `payment_link: true`, then open the link and complete the mandate.
  </Step>

  <Step title="Trigger a charge">
    Call the charge endpoint with a small `product_price` (e.g., `100`) and verify you receive `payment.succeeded`.
  </Step>

  <Step title="Go live">
    Switch to your live API key once you have validated events and internal state updates.
  </Step>
</Steps>

## Troubleshooting

* **422 Invalid Request**: Ensure `on_demand.mandate_only` is provided on creation and `product_price` is provided for charges.
* **Currency errors**: If you override `product_currency`, confirm it's supported for your account and customer.
* **No webhooks received**: Verify your webhook URL and signature secret configuration.

Bạn có thể hủy đăng ký theo yêu cầu qua API bất kỳ lúc nào. Bạn kiểm soát việc hủy bỏ ngay lập tức hay theo lịch.

Endpoint: [PATCH /subscriptions/\{subscription\_id}](/api-reference/subscriptions/patch-subscriptions)

<Tabs>
  <Tab title="Cancel immediately">
    Đặt đăng ký `status` thành `cancelled` để kết thúc ngay lập tức. Ủy quyền bị thu hồi và không thể tạo thêm các khoản phí nào khác.

    ```javascript theme={null}
    import DodoPayments from 'dodopayments';

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

    await client.subscriptions.update('sub_123', {
      status: 'cancelled',
    });
    ```

    ```bash cURL theme={null}
    curl -X PATCH "$DODO_API/subscriptions/sub_123" \
      -H "Authorization: Bearer $DODO_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{ "status": "cancelled" }'
    ```
  </Tab>

  <Tab title="Cancel at next billing date">
    Giữ hành vi cổng khách hàng — duy trì đăng ký hoạt động cho đến ngày hủy theo lịch, rồi kết thúc.

    ```javascript theme={null}
    import DodoPayments from 'dodopayments';

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

    await client.subscriptions.update('sub_123', {
      cancel_at_next_billing_date: true,
    });
    ```

    ```bash cURL theme={null}
    curl -X PATCH "$DODO_API/subscriptions/sub_123" \
      -H "Authorization: Bearer $DODO_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{ "cancel_at_next_billing_date": true }'
    ```
  </Tab>
</Tabs>

### Webhooks khi hủy bỏ

| Sự kiện                     | Khi nó kích hoạt                                                                            |
| --------------------------- | ------------------------------------------------------------------------------------------- |
| `subscription.cancelled`    | Đăng ký bị hủy hoàn toàn và không còn có thể tính phí                                       |
| `subscription.plan_changed` | `cancel_at_next_billing_date` đã được chuyển đổi (hủy theo lịch đã thiết lập hoặc hoàn tác) |

<Tip>
  Để phân biệt giữa hủy bỏ theo yêu cầu và hủy đăng ký theo lịch trong bộ xử lý của bạn, hãy kiểm tra cờ `on_demand` của đăng ký khi xử lý webhook.
</Tip>

## Theo dõi kết quả với webhooks

Thực hiện xử lý webhook để theo dõi hành trình khách hàng. Xem [Thực hiện Webhooks](/developer-resources/integration-guide#implementing-webhooks).

* **subscription.active**: Ủy quyền được xác nhận và đăng ký được kích hoạt
* **subscription.failed**: Tạo không thành công (ví dụ, lỗi ủy quyền)
* **subscription.on\_hold**: Đăng ký bị tạm dừng (ví dụ, trạng thái chưa thanh toán)
* **subscription.cancelled**: Đăng ký bị hủy hoàn toàn (xem [Hủy Bỏ](#cancellation))
* **payment.succeeded**: Tính phí thành công
* **payment.failed**: Tính phí không thành công

<Tip>
  Đối với luồng theo yêu cầu, hãy tập trung vào `payment.succeeded` và `payment.failed` để đối chiếu các khoản phí dựa trên sử dụng. Khi `payment.failed` được theo sau bởi `subscription.on_hold`, hãy xem [Xử lý các khoản phí không thành công](#handling-failed-charges) để khôi phục đăng ký.
</Tip>

## Kiểm tra và bước tiếp theo

<Steps>
  <Step title="Create in test mode">
    Sử dụng khóa API kiểm tra của bạn để tạo đăng ký, sau đó mở `checkout_url` trả về và hoàn tất yêu cầu.
  </Step>

  <Step title="Trigger a charge">
    Gọi endpoint tính phí với một `product_price` nhỏ (ví dụ, `100`) và xác minh bạn nhận được `payment.succeeded`.
  </Step>

  <Step title="Go live">
    Chuyển sang khóa API trực tiếp của bạn sau khi bạn đã xác nhận các sự kiện và cập nhật trạng thái nội bộ.
  </Step>
</Steps>

## Khắc phục sự cố

* **422 Yêu cầu Không hợp lệ**: Đảm bảo `on_demand.mandate_only` được cung cấp khi tạo và `product_price` được cung cấp cho các khoản phí.
* **Lỗi tiền tệ**: Nếu bạn ghi đè `product_currency`, xác nhận nó được hỗ trợ cho tài khoản của bạn và khách hàng.
* **Không nhận được webhook**: Xác minh URL webhook của bạn và cấu hình ký bí mật.
