> ## 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>
  If you want the customer to approve the mandate via hosted checkout, set `payment_link: true` and provide a `return_url`.
</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 (
        "fmt"
        "os"

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

    func main() {
        client := dodo.NewClient(dodo.Config{
            BearerToken: os.Getenv("DODO_PAYMENTS_API_KEY"),
            Environment: dodo.TestMode, // defaults to LiveMode
        })

        checkout, err := client.CheckoutSessions.Create(dodo.CheckoutSessionCreateParams{
            ProductCart: []dodo.ProductCartItem{
                {
                    ProductID: "pdt_123",
                    Quantity:  1,
                },
            },
            BillingAddress: &dodo.BillingAddress{
                City:    "SF",
                Country: "US",
                State:   "CA",
                Street:  "1 Market St",
                Zipcode: "94105",
            },
            Customer: &dodo.CustomerRef{
                CustomerID: "cus_123",
            },
            ReturnURL: "https://example.com/billing/success",
            SubscriptionData: &dodo.SubscriptionData{
                OnDemand: &dodo.OnDemandSubscription{
                    MandateOnly: true, // set false to collect an initial charge
                    // ProductPrice: 1000,  // optional: charge $10.00 now if mandate_only is false
                    // ProductCurrency: "USD",
                    // ProductDescription: "Custom initial charge",
                    // AdaptiveCurrencyFeesInclusive: false,
                },
            },
        })
        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">
    If retries keep failing because the payment method itself is broken (expired card, closed account, etc.), use [`POST /subscriptions/{subscription_id}/update-payment-method`](/api-reference/subscriptions/update-payment-method) to collect a new one from the customer. On success, the subscription returns to `active` and `payment.succeeded` followed by `subscription.active` webhooks are emitted.
  </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.

## Cancellation

On-demand subscriptions follow a different cancellation flow from scheduled subscriptions because there is no fixed billing cycle to anchor an immediate end date.

### Customer portal behavior

When a customer cancels an on-demand subscription from the [Customer Portal](/features/customer-portal), the cancellation is **scheduled for the next billing date** by default. The **Cancel Now** option is intentionally not shown for on-demand subscriptions.

The reason: on-demand subscriptions do not have predictable recurring renewal dates — the next charge time is driven entirely by your usage events. Scheduling cancellation at the next billing date keeps the mandate active until the period boundary so any in-flight usage can still be charged, then ends the subscription cleanly.

After the customer confirms cancellation:

* The subscription stays `active` and remains chargeable via `POST /subscriptions/{id}/charge` until the scheduled cancellation date.
* `cancel_at_next_billing_date` is set to `true` on the subscription.
* A `subscription.cancelled` webhook is emitted when the cancellation takes effect.

<Info>
  If you need to end the subscription immediately (for example, in response to a refund or a support request), cancel it programmatically via the API instead of relying on the customer portal flow.
</Info>

### Cancel programmatically

You can cancel an on-demand subscription via the API at any time. You control whether the cancellation is immediate or scheduled.

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

<Tabs>
  <Tab title="Cancel immediately">
    Set the subscription `status` to `cancelled` to end it right away. The mandate is revoked and no further charges can be created.

    ```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">
    Mirror the customer portal behavior — keep the subscription active until the scheduled cancellation date, then end it.

    ```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 on cancellation

| Event                       | When it fires                                                                    |
| --------------------------- | -------------------------------------------------------------------------------- |
| `subscription.cancelled`    | Subscription is fully cancelled and no longer chargeable                         |
| `subscription.plan_changed` | `cancel_at_next_billing_date` was toggled (scheduled cancellation set or undone) |

<Tip>
  To distinguish on-demand cancellations from scheduled-subscription cancellations in your handler, check the subscription's `on_demand` flag when processing the webhook.
</Tip>

## 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)
* **subscription.cancelled**: Subscription fully cancelled (see [Cancellation](#cancellation))
* **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.
