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

# Suscripciones bajo demanda

> Integra suscripciones bajo demanda autorizando mandatos, creando cargos variables, manejando webhooks e implementando políticas de reintento seguras.

## Descripción general

Las suscripciones bajo demanda te permiten autorizar el método de pago de un cliente una vez y luego cobrar montos variables cuando lo necesites, en lugar de en un horario fijo. Esta función está disponible para todas las cuentas; no se requiere aprobación.

Utiliza esta guía para:

* Crear una suscripción bajo demanda (autorizar un mandato con un precio inicial opcional)
* Activar cargos posteriores con montos personalizados
* Rastrear resultados utilizando webhooks

Para una configuración general de suscripción, consulta la [Guía de Integración de Suscripciones](/developer-resources/subscription-integration-guide).

## Requisitos previos

* Cuenta de comerciante de Dodo Payments y clave API
* Secreto de webhook configurado y un endpoint para recibir eventos
* Un producto de suscripción en tu catálogo

<Tip>
  Si deseas que el cliente apruebe el mandato mediante el pago alojado, establece `payment_link: true` y facilita un `return_url`.
</Tip>

## Cómo funciona bajo demanda

1. Creas una suscripción con el objeto `on_demand` para autorizar un método de pago y, opcionalmente, cobrar un cargo inicial.
2. Más adelante, creas cargos contra esa suscripción con importes personalizados usando el endpoint dedicado de cargos.
3. Escuchas webhooks (p. ej., `payment.succeeded`, `payment.failed`) para actualizar tu sistema.

## Crear una suscripción bajo demanda

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

Campos clave de la solicitud (cuerpo):\
Consúltalos en [Crear Sesión de Pago](/api-reference/checkout-sessions/create)

### Crear una suscripción bajo demanda

<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"
}
```

## Cargar una suscripción bajo demanda

Después de que se autorice el mandato, crea cargos según sea necesario.

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

Campos clave de la solicitud (cuerpo):

<AccordionGroup>
  <Accordion title="Charge request body parameters">
    <ParamField body="product_price" type="integer" required>
      Importe a cobrar (en la unidad monetaria más pequeña). Ejemplo: para cobrar \$25.00, pasa <code>2500</code>.
    </ParamField>

    <ParamField body="product_currency" type="string">
      Anulación opcional de moneda para el cargo.
    </ParamField>

    <ParamField body="product_description" type="string">
      Anulación opcional de descripción para este cargo.
    </ParamField>

    <ParamField body="adaptive_currency_fees_inclusive" type="boolean">
      Si es verdadero, incluye tarifas de moneda adaptativa dentro de <code>product\_price</code>. Si es falso, las tarifas se agregan encima.
    </ParamField>

    <ParamField body="metadata" type="object">
      Metadatos adicionales para el pago. Si se omite, se utilizan los metadatos de la suscripción.
    </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>
  Cobrar una suscripción que no es bajo demanda puede fallar. Asegúrate de que la suscripción tenga `on_demand: true` en sus detalles antes de cobrar.
</Warning>

## Manejo de cargos fallidos

Cuando un cargo contra una suscripción bajo demanda falla, decides qué ocurre a continuación. A diferencia de las suscripciones programadas — donde una renovación fallida detiene la facturación automática — **las suscripciones bajo demanda permanecen cargables tras un fallo**. Puedes llamar al endpoint de carga nuevamente como parte de tu propia lógica de reintento.

### Qué sucede en caso de fallo

<Steps>
  <Step title="Charge attempt fails">
    La solicitud `POST /subscriptions/{subscription_id}/charge` ya sea devuelve una respuesta de error o se completa de forma asincrónica y emite un webhook `payment.failed` con la razón del rechazo.
  </Step>

  <Step title="Subscription may transition to on_hold">
    La suscripción puede pasar al estado `on_hold` y emitir un webhook `subscription.on_hold` (ver [Estados de Suscripción → En Espera](/features/subscription#on-hold-state)). Esto es una señal, no un bloqueo. Para suscripciones bajo demanda, `on_hold` **no** impide que vuelvas a cobrar.
  </Step>

  <Step title="Retry the charge (your call)">
    Para flujos bajo demanda, Dodo **no** realiza reintentos automáticos. Puedes llamar a `POST /subscriptions/{subscription_id}/charge` en cualquier momento para intentar de nuevo. Aplica la [política de reintentos segura](#payment-retries) a continuación: usa retroceso exponencial, omite rechazos fuertes y evita patrones de ráfaga, para que los reintentos no sean señalados por nuestros sistemas de fraude y riesgo.
  </Step>

  <Step title="Optionally, ask the customer for a new payment method">
    Si los reintentos siguen fallando porque el método de pago en sí está roto (tarjeta expirada, cuenta cerrada, etc.), utiliza [`POST /subscriptions/{subscription_id}/update-payment-method`](/api-reference/subscriptions/update-payment-method) para recopilar uno nuevo del cliente. Si tiene éxito, la suscripción vuelve a `active` y `payment.succeeded` seguido de `subscription.active` se emiten webhooks.
  </Step>
</Steps>

<Info>
  **Bajo demanda vs programadas**: Para suscripciones programadas, Dodo gestiona sus propios reintentos de renovación y gestión del cobro. Para suscripciones bajo demanda, tú controlas la política de reintentos porque solo tú sabes cuándo debe ocurrir el siguiente cobro (está impulsado por tus eventos de uso, no por un calendario).
</Info>

### Secuencia de webhook en un cobro fallido bajo demanda

| Orden | Evento                 | Significado                                                                                           |
| ----- | ---------------------- | ----------------------------------------------------------------------------------------------------- |
| 1     | `payment.failed`       | El intento de cobro bajo demanda no tuvo éxito (incluye la razón del rechazo)                         |
| 2     | `subscription.on_hold` | La suscripción fue puesta en espera (informativo; no bloquea más cargos)                              |
| 3\*   | `payment.succeeded`    | Un cobro posterior, ya sea tu reintento o después de una actualización del método de pago, tuvo éxito |
| 4\*   | `subscription.active`  | La suscripción regresó a `active` después de un cobro exitoso                                         |

<Info>
  Los eventos 3 y 4 solo se activan después de que un cobro de seguimiento tiene éxito.
</Info>

### Responsabilidad de reintentos

<Warning>
  Dodo Payments **no realiza reintentos automáticos de cargos fallidos bajo demanda**. Tú controlas la política de reintentos. Sigue las pautas de reintentos seguros a continuación para evitar ser señalado por nuestros sistemas de detección de fraude como prueba de tarjetas.
</Warning>

[Gestión del Cobro de Suscripciones](/features/recovery/subscription-dunning) — la secuencia de recuperación por correo electrónico integrada — se aplica a pagos de *renovación* fallidos en suscripciones programadas y cancelaciones iniciadas por el cliente. No está diseñada para fallos de cargos bajo demanda. Comunícate con el cliente directamente (por ejemplo, correo electrónico transaccional o mensaje en app) cuando decidas que se necesita actualizar el método de pago.

## Reintentos de pago

Nuestro sistema de detección de fraude puede bloquear patrones de reintento agresivos (y puede señalarlos como posible prueba de tarjetas). Sigue una política de reintentos segura.

<Warning>
  Patrones de reintentos en ráfaga pueden ser señalados como fraudulentos o sospechosos de ser pruebas de tarjetas por nuestros sistemas de riesgo y procesadores. Evita reintentos agrupados; sigue las guías de horario de retroceso y alineación de tiempo a continuación.
</Warning>

### Principios para políticas de reintentos seguros

* **Mecanismo de retroceso**: Usa retroceso exponencial entre reintentos.
* **Límites de reintentos**: Limita los reintentos totales (máximo 3–4 intentos).
* **Filtrado inteligente**: Reintenta solo en fallos que se pueden reintentar (por ejemplo, errores de red/emisor, fondos insuficientes); nunca reintentes rechazos fuertes.
* **Prevención de pruebas de tarjetas**: No reintentes fallos como `DO_NOT_HONOR`, `STOLEN_CARD`, `LOST_CARD`, `PICKUP_CARD`, `FRAUDULENT`, `AUTHENTICATION_FAILURE`.
* **Varía metadatos (opcional)**: Si mantienes tu propio sistema de reintentos, diferencia los reintentos mediante metadatos (por ejemplo, `retry_attempt`).

### Horario de reintentos sugerido (suscripciones)

* **1er intento**: Inmediato cuando creas el cargo
* **2º intento**: Después de 3 días
* **3er intento**: Después de 7 días más (10 días en total)
* **4º intento (final)**: Después de otros 7 días (17 días en total)

Paso final: si aún está impago, marca la suscripción como impaga o cancélala, según tu política. Notifica al cliente durante el intervalo para que actualice su método de pago.

### Evita reintentos en ráfaga; alinea con el tiempo de autorización

* Ancla los reintentos a la marca de tiempo de la autorización original para evitar comportamiento de "ráfaga" en tu portafolio.
* Ejemplo: Si el cliente inicia una prueba o mandato a la 1:10 pm hoy, programa reintentos de seguimiento a la 1:10 pm en días posteriores según tu retroceso (por ejemplo, +3 días → 1:10 pm, +7 días → 1:10 pm).
* Alternativamente, si almacenas el tiempo del último pago exitoso `T`, programa el siguiente intento en `T + X days` para preservar la alineación del tiempo del día.

<Note>
  Zona horaria y DST: usa un estándar de tiempo consistente para programar y convierte para mostrar solo para mantener los intervalos.
</Note>

### Códigos de rechazo que no deberías volver a intentar

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

<Note>
  Para una lista completa de razones de rechazo y si son corregibles por el usuario, consulta la documentación de [Fallos de Transacciones](/api-reference/transaction-failures).
</Note>

<Tip>
  Solo reintente en problemas suaves/temporales (por ejemplo, `insufficient_funds`, `issuer_unavailable`, `processing_error`, tiempos de espera de red). Si el mismo rechazo se repite, pausa más reintentos.
</Tip>

### Pautas de implementación (sin código)

* Usa un programador/cola que persista marcas de tiempo precisas; calcula el próximo intento con el mismo desfase de hora del día (por ejemplo, `T + 3 days` a la misma HH:MM).
* Mantén y consulta la última marca de tiempo de pago exitoso `T` para calcular el próximo intento; no agrupe múltiples suscripciones al mismo instante.
* Siempre evalúa la última razón de rechazo; detén los reintentos para rechazos fuertes en la lista de omisión arriba.
* Límite de reintentos concurrentes por cliente y por cuenta para prevenir aumentos accidentales.
* Comunica proactivamente: envía correo electrónico/SMS al cliente para actualizar su método de pago antes del próximo intento programado.
* Usa metadatos solo para observabilidad (por ejemplo, `retry_attempt`); nunca intentes "evadir" sistemas de fraude/riesgo rotando campos inconsecuentes.

## Cancelación

Las suscripciones a demanda siguen un flujo de cancelación diferente al de las suscripciones programadas porque no hay un ciclo de facturación fijo para determinar una fecha de finalización inmediata.

### Comportamiento del portal del cliente

Cuando un cliente cancela una suscripción a demanda desde el [Portal del Cliente](/features/customer-portal), la cancelación se **programa para la próxima fecha de facturación** por defecto. La opción **Cancelar ahora** no se muestra intencionadamente para suscripciones a demanda.

La razón: las suscripciones a demanda no tienen fechas de renovación recurrentes predecibles; el tiempo del próximo cargo se determina completamente por los eventos de uso. Programar la cancelación en la próxima fecha de facturación mantiene el mandato activo hasta el límite del período, de modo que cualquier uso en curso aún puede ser cobrado, y luego la suscripción termina limpiamente.

Después de que el cliente confirma la cancelación:

* La suscripción permanece `active` y continúa siendo cobrada a través de `POST /subscriptions/{id}/charge` hasta la fecha de cancelación programada.
* `cancel_at_next_billing_date` se establece en `true` en la suscripción.
* Se emite un webhook `subscription.cancelled` cuando la cancelación entra en vigor.

<Info>
  Si necesitas terminar la suscripción inmediatamente (por ejemplo, en respuesta a un reembolso o una solicitud de soporte), cancélala programáticamente a través de la API en lugar de depender del flujo del portal del cliente.
</Info>

### Cancelar programáticamente

Puedes cancelar una suscripción a demanda a través de la API en cualquier momento. Tú controlas si la cancelación es inmediata o programada.

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

<Tabs>
  <Tab title="Cancel immediately">
    Establece la suscripción `status` en `cancelled` para finalizarla de inmediato. El mandato es revocado y no se pueden crear más cargos.

    ```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">
    Sigue el comportamiento del portal del cliente: mantén la suscripción activa hasta la fecha de cancelación programada y luego termínala.

    ```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 en la cancelación

| Evento                      | Cuándo se dispara                                                                          |
| --------------------------- | ------------------------------------------------------------------------------------------ |
| `subscription.cancelled`    | La suscripción está completamente cancelada y ya no es cobrable                            |
| `subscription.plan_changed` | `cancel_at_next_billing_date` fue activado (cancelación programada establecida o deshecha) |

<Tip>
  Para distinguir las cancelaciones a demanda de las cancelaciones de suscripciones programadas en tu manejador, verifica la marca `on_demand` de la suscripción al procesar el webhook.
</Tip>

## Seguimiento de resultados con webhooks

Implementa el manejo de webhooks para seguir el recorrido del cliente. Consulta [Implementación de Webhooks](/developer-resources/integration-guide#implementing-webhooks).

* **subscription.active**: Mandato autorizado y suscripción activada
* **subscription.failed**: Creación fallida (por ejemplo, fallo de mandato)
* **subscription.on\_hold**: Suscripción en espera (por ejemplo, estado impago)
* **subscription.cancelled**: Suscripción completamente cancelada (ver [Cancelación](#cancellation))
* **payment.succeeded**: Cobro exitoso
* **payment.failed**: Cobro fallido

<Tip>
  Para flujos a demanda, concéntrate en `payment.succeeded` e `payment.failed` para conciliar los cargos basados en el uso. Cuando `payment.failed` es seguido por `subscription.on_hold`, consulta [Manejo de cobros fallidos](#handling-failed-charges) para recuperar la suscripción.
</Tip>

## Pruebas y próximos pasos

<Steps>
  <Step title="Create in test mode">
    Usa tu clave API de prueba para crear la suscripción con `payment_link: true`, luego abre el enlace y completa el mandato.
  </Step>

  <Step title="Trigger a charge">
    Llama al endpoint de cobro con un pequeño `product_price` (por ejemplo, `100`) y verifica que recibas `payment.succeeded`.
  </Step>

  <Step title="Go live">
    Cambia a tu clave API en vivo una vez que hayas validado eventos y actualizaciones de estado interno.
  </Step>
</Steps>

## Solución de problemas

* **422 Solicitud inválida**: Asegúrate de que `on_demand.mandate_only` se proporcione al crear y `product_price` se proporcione para los cargos.
* **Errores de moneda**: Si sobrescribes `product_currency`, confirma que está soportado para tu cuenta y cliente.
* **No se reciben webhooks**: Verifica la configuración de tu URL de webhook y el secreto de la firma.
