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 may need to be enabled on your account. Contact support if you don’t see it in your dashboard.
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 /subscriptions Key request fields (body):

Create an on-demand subscription

import DodoPayments from 'dodopayments';

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

async function main() {
  const subscription = await client.subscriptions.create({
    billing: { city: 'SF', country: 'US', state: 'CA', street: '1 Market St', zipcode: '94105' },
    customer: { customer_id: 'customer_123' },
    product_id: 'prod_sub_123',
    quantity: 1,
    payment_link: true,
    return_url: 'https://example.com/billing/success',
    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,
    },
  });

  // If payment_link was true, redirect the customer to authorize the mandate
  console.log(subscription.payment_link);
}

main().catch(console.error);
Set payment_link: true, redirect the customer to payment_link to complete mandate authorization.
Success
{
  "subscription_id": "sub_123",
  "payment_link": "https://pay.dodopayments.com/checkout/...",
  "customer": { "customer_id": "customer_123", "email": "alex@example.com", "name": "Alex Doe" },
  "metadata": {},
  "recurring_pre_tax_amount": 0,
  "addons": []
}

Charge an on-demand subscription

After the mandate is authorized, create charges as needed. Endpoint: POST /subscriptions/{subscription_id}/charge Key request fields (body):
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.

Payment retries

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

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.

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.

Adding retry context in charges

Include metadata in your retry charges to track attempts.
import DodoPayments from 'dodopayments';

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

const SKIP_RETRY_CODES = [
  'STOLEN_CARD',
  'DO_NOT_HONOR',
  'FRAUDULENT',
  'PICKUP_CARD',
  'AUTHENTICATION_FAILURE',
  'LOST_CARD',
];

async function retryCharge(subscriptionId, amount, attempt, lastDeclineCode) {
  if (SKIP_RETRY_CODES.includes(lastDeclineCode)) return;
  await client.subscriptions.charge(subscriptionId, {
    product_price: amount,
    metadata: { retry_attempt: attempt },
  });
}

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)
  • payment.succeeded: Charge succeeded
  • payment.failed: Charge failed
For on-demand flows, focus on payment.succeeded and payment.failed to reconcile usage-based charges.

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.