Skip to main content

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.

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.

Prerequisites

  • Dodo Payments merchant account and API key
  • Webhook secret configured and an endpoint to receive events
  • A subscription product in your catalog
If you want the customer to approve the mandate via hosted checkout, set payment_link: true and provide a return_url.

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 Key request fields (body):
Please find them in Create Checkout Session

Create an on-demand subscription

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);
Success
{
  "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 Key request fields (body):
product_price
integer
required
Amount to charge (in the smallest currency unit). Example: to charge $25.00, pass 2500.
product_currency
string
Optional currency override for the charge.
product_description
string
Optional description override for this charge.
adaptive_currency_fees_inclusive
boolean
If true, includes adaptive currency fees within product_price. If false, fees are added on top.
metadata
object
Additional metadata for the payment. If omitted, the subscription metadata is used.
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);
Success
{ "payment_id": "pay_abc123" }
Charging a subscription that is not on-demand may fail. Ensure the subscription has on_demand: true in its details before charging.

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

1

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

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). This is a signal — not a lock. For on-demand subscriptions, on_hold does not prevent you from charging again.
3

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 below — use exponential backoff, skip hard declines, and avoid burst patterns — so retries are not flagged by our fraud and risk systems.
4

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/{id}/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.
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).

Webhook sequence on a failed on-demand charge

OrderEventMeaning
1payment.failedThe on-demand charge attempt did not succeed (includes the decline reason)
2subscription.on_holdThe subscription was placed on hold (informational; does not block further charges)
3*payment.succeededA subsequent charge — either your retry or after a payment method update — succeeded
4*subscription.activeThe subscription returned to active after a successful charge
Events 3 and 4 only fire after a follow-up charge succeeds.

Retry responsibility

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

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.
Time-zone and DST: use a consistent time standard for scheduling and convert for display only to maintain intervals.

Decline codes you should not retry

  • STOLEN_CARD
  • DO_NOT_HONOR
  • FRAUDULENT
  • PICKUP_CARD
  • AUTHENTICATION_FAILURE
  • LOST_CARD
For a comprehensive list of decline reasons and whether they are user-correctable, see the Transaction Failures documentation.
Only retry on soft/temporary issues (e.g., insufficient_funds, issuer_unavailable, processing_error, network timeouts). If the same decline repeats, pause further retries.

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

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}
Set the subscription status to cancelled to end it right away. The mandate is revoked and no further charges can be created.
import DodoPayments from 'dodopayments';

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

await client.subscriptions.update('sub_123', {
  status: 'cancelled',
});
cURL
curl -X PATCH "$DODO_API/subscriptions/sub_123" \
  -H "Authorization: Bearer $DODO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "status": "cancelled" }'

Webhooks on cancellation

EventWhen it fires
subscription.cancelledSubscription is fully cancelled and no longer chargeable
subscription.plan_changedcancel_at_next_billing_date was toggled (scheduled cancellation set or undone)
To distinguish on-demand cancellations from scheduled-subscription cancellations in your handler, check the subscription’s on_demand flag when processing the webhook.

Track outcomes with webhooks

Implement webhook handling to track the customer journey. See 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)
  • payment.succeeded: Charge succeeded
  • payment.failed: Charge failed
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 to recover the subscription.

Testing and next steps

1

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

Trigger a charge

Call the charge endpoint with a small product_price (e.g., 100) and verify you receive payment.succeeded.
3

Go live

Switch to your live API key once you have validated events and internal state updates.

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.
Last modified on May 22, 2026