> ## Documentation Index
> Fetch the complete documentation index at: https://commet.co/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Manejar pagos fallidos

> Qué pasa cuando el pago de un cliente falla y cómo puede reactivar su suscripción desde el Portal del Cliente.

Cuando falla un pago de renovación, la suscripción pasa al estado `past_due` e ingresa al proceso de reintentos (dunning). El cliente mantiene el servicio durante esta ventana de gracia mientras Commet reintenta el cobro. Los clientes pueden reactivar antes desde el Portal del Cliente reintentando el pago o actualizando su tarjeta.

## Qué pasa cuando un pago falla

1. La suscripción cambia al estado `past_due`
2. El recibo fallido se marca como `outstanding`
3. El cliente mantiene el servicio: los eventos de uso y de asientos siguen funcionando (el uso se acumula como deuda)
4. El cliente recibe una notificación por email
5. Commet reintenta el cobro según un calendario fijo (dunning)

Si un reintento tiene éxito, la suscripción vuelve a `active`. Si todos los reintentos fallan, la suscripción se cancela y el recibo se marca como `uncollectible`.

## Mapeo de errores del proveedor

Commet traduce las respuestas del proveedor a un resultado de pago común y conserva el detalle del proveedor junto a él. Para fallas recurrentes, el webhook [`payment.failed`](/docs/es/webhooks/payment-failed) expone `failureCode`, `failureMessage` y un `recoveryUrl` cuando existe un camino de recuperación. El `failureCode` exacto depende del proveedor, así que usa el resultado normalizado y el recovery URL para manejar al cliente en lugar de depender de los códigos raw de un solo proveedor.

| Resultado del pago | Qué significa | Qué hacer |
| - | - | - |
| `requires_action` | El proveedor necesita un paso adicional del cliente, como 3D Secure | Mantén al cliente en el checkout y completa el flujo de autenticación |
| `PAYMENT_FAILED` con un código de rechazo | El proveedor rechazó el cobro | Muestra un retry o un método de pago alternativo y guarda el código del proveedor para soporte |
| `payment.failed` | Falló un cobro recurrente y el recibo ingresó en dunning | Mantén el servicio durante la ventana de gracia, comunica la recuperación y espera un retry o una recuperación |
| `payment.retry_failed` | Se agotaron todos los retries de dunning programados | Restringe el acceso según la política de tu producto y pide al cliente que inicie una suscripción nueva o contacte a soporte |

Los rechazos de tarjeta durante el checkout inicial no emiten `payment.failed`; la respuesta del checkout trae directamente el estado de la falla. Las fallas recurrentes usan el flujo de dunning que sigue.

## Reintentos automáticos

Los reintentos se ejecutan el día 1, el día 3, el día 5 y el día 7 después de la falla original (4 reintentos). El calendario queda anclado a la falla y nunca se mueve. Tras el último reintento fallido, la suscripción se cancela.

Los reintentos manuales desde el Portal del Cliente o la [API](#reintentar-el-cobro-de-servidor-a-servidor) usan el método predeterminado y tienen límites diarios separados. No consumen reintentos del calendario automático ni inician otro cobro mientras haya un reintento automático en curso.

### Métodos de pago guardados

Commet intenta cobrar primero con el método predeterminado de la conexión de pago de la suscripción. Ante un rechazo confirmado, prueba los demás métodos disponibles de esa conexión, del más nuevo al más antiguo. OXXO queda fuera de estos reintentos. Cambiar el routing por país no cambia la conexión usada para reintentar.

Probar varios métodos cuenta como un solo reintento del calendario. Commet se detiene si el pago se confirma. Si el resultado está pendiente o todavía no se conoce, espera a resolver ese cobro antes de probar otro método.

Si el cobro con otro método funciona, el predeterminado del cliente no cambia.

## Dunning communications

Commet envía una notificación de falla cuando un cobro recurrente entra en dunning. Para mensajes específicos de tu producto, suscríbete a estos webhooks:

* [`payment.failed`](/docs/es/webhooks/payment-failed) — falló un cobro recurrente; usa `failureCode`, `failureMessage` y `recoveryUrl` para explicar el siguiente paso.
* [`payment.recovered`](/docs/es/webhooks/payment-recovered) — se pagó el recibo pendiente y la suscripción volvió a `active`.
* [`payment.retry_failed`](/docs/es/webhooks/payment-retry-failed) — se agotaron los retries y la suscripción fue cancelada.

Envía tu propio email, SMS o mensaje dentro del producto cuando necesites un copy específico. No crees un segundo calendario de retries en tu aplicación; usa los eventos de Commet para cerrar el ciclo de comunicación.

## Consultar el estado de la suscripción

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    const subscription = await commet.subscriptions.getActive({ customerId: 'user_123' })

    if (subscription?.status === 'past_due') {
      showRecoveryPrompt()
    }
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    subscription = commet.subscriptions.get_active(customer_id='user_123')

    if subscription is not None and subscription.status == 'past_due':
        show_recovery_prompt()
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    subscription, err := client.Subscriptions.GetActive(ctx, &commet.GetActiveSubscriptionParams{CustomerID: "user_123"})
    if err != nil {
        log.Fatal(err)
    }
    if subscription != nil && subscription.Status == "past_due" {
        showRecoveryPrompt()
    }
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    var subscription = commet.subscriptions().getActive(GetActiveSubscriptionParams.builder("user_123").build());

    if (subscription != null && subscription.status() == SubscriptionStatus.PAST_DUE) {
        showRecoveryPrompt();
    }
    ```
  </Tab>

  <Tab title="PHP">
    ```php theme={null}
    $result = $commet->subscriptions->getActive('user_123');

    if ($result !== null && $result->status->value === 'past_due') {
        showRecoveryPrompt();
    }
    ```
  </Tab>

  <Tab title="cURL">
    ```bash theme={null}
    curl "https://commet.co/api/v1/subscriptions/active?customerId=user_123" \
      -H "x-api-key: $COMMET_API_KEY"
    ```
  </Tab>
</Tabs>

## Restringir el acceso según el estado

Commet sigue dando servicio a los clientes en `past_due` durante la ventana de dunning: los eventos de uso y de asientos siguen funcionando. Tú decides si restringes tu propio producto cuando la suscripción está en `past_due`. Para dar acceso solo mientras el cobro está sano, trata `active` y `trialing` como los estados con acceso:

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    const subscription = await commet.subscriptions.getActive({ customerId: 'user_123' })

    const hasAccess = subscription !== null &&
      (subscription.status === 'active' || subscription.status === 'trialing')
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    subscription = commet.subscriptions.get_active(customer_id='user_123')

    has_access = subscription is not None and subscription.status in ('active', 'trialing')
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    subscription, err := client.Subscriptions.GetActive(ctx, &commet.GetActiveSubscriptionParams{CustomerID: "user_123"})
    if err != nil {
        log.Fatal(err)
    }
    hasAccess := subscription != nil &&
        (subscription.Status == "active" || subscription.Status == "trialing")
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    var subscription = commet.subscriptions().getActive(GetActiveSubscriptionParams.builder("user_123").build());

    boolean hasAccess = subscription != null &&
        (subscription.status() == SubscriptionStatus.ACTIVE ||
            subscription.status() == SubscriptionStatus.TRIALING);
    ```
  </Tab>

  <Tab title="PHP">
    ```php theme={null}
    $result = $commet->subscriptions->getActive('user_123');

    $hasAccess = $result !== null &&
        in_array($result->status->value, ['active', 'trialing'], true);
    ```
  </Tab>

  <Tab title="cURL">
    ```bash theme={null}
    curl "https://commet.co/api/v1/subscriptions/active?customerId=user_123" \
      -H "x-api-key: $COMMET_API_KEY"
    ```
  </Tab>
</Tabs>

## Recuperar una suscripción de forma programática

El SDK expone tres primitivas de recuperación del lado del servidor. Para suscripciones en `past_due`, todas operan sobre el mismo recibo de renovación `outstanding`: ninguna lo anula. `reactivate` además reactiva suscripciones `canceled`.

### Reintentar el cobro de servidor a servidor

`reactivate` cobra el método de pago guardado de la suscripción. Funciona tanto en suscripciones `past_due` como `canceled`, con efectos distintos:

* `past_due`: reintenta el mismo recibo de renovación pendiente. El ancla de facturación se mantiene fija. Si tiene éxito, la suscripción vuelve a `active` y se emite `payment.recovered`.
* `canceled`: genera un recibo nuevo, reinicia el ancla del período de facturación a ahora y cobra la tarjeta guardada. Si tiene éxito, la suscripción vuelve a `active` y se emite `subscription.reactivated`. Requiere que el plan siga disponible en la moneda de la suscripción; de lo contrario devuelve `PLAN_UNAVAILABLE` (422).

```typescript theme={null}
const result = await commet.subscriptions.reactivate({ id: 'sub_123' })

// result.retryInitiated === true
```

Si el cobro se rechaza o no hay tarjeta guardada, la respuesta devuelve un `recoveryUrl` en los detalles del error: una página hospedada donde el cliente agrega una tarjeta nueva y paga. Esto importa para suscripciones canceladas por dunning: llegaron a `canceled` precisamente porque la tarjeta guardada seguía fallando.

### Enviar un enlace de recuperación al cliente

`createRecoveryLink` devuelve un enlace hospedado y firmado para que el cliente pague la renovación pendiente por su cuenta. Entrégalo a través de tu propio email, SMS o dashboard. El enlace permanece válido hasta que el cobro se paga o la suscripción deja de estar en `past_due`.

El [webhook `payment.failed`](/docs/es/webhooks/payment-failed) ya incluye un `recoveryUrl`: la URL del checkout para un primer cobro fallido, un enlace de recuperación firmado para una renovación fallida. Si consumes webhooks, no necesitas llamar a `createRecoveryLink`.

```typescript theme={null}
const recovery = await commet.subscriptions.createRecoveryLink({ id: 'sub_123' })

// recovery.url   → página de pago hospedada
// recovery.token → token firmado incrustado en la URL
```

### Actualizar el método de pago

`updatePaymentMethod` devuelve un checkout hospedado donde el cliente actualiza el método de pago predeterminado de la suscripción.

```typescript theme={null}
const paymentMethodUpdate = await commet.subscriptions.updatePaymentMethod({
  id: 'sub_123',
  successUrl: 'https://yourapp.com/billing',
})

// redirect(paymentMethodUpdate.checkoutUrl)
```

## Self-serve recovery

Los clientes en `past_due` ven su suscripción en el [Portal del Cliente](/docs/es/customer-portal) con un botón **Reactivar suscripción**. Pueden elegir:

* **Reintentar con su tarjeta actual** — útil cuando la falla fue temporal (fondos insuficientes que ya están disponibles, una retención bancaria que se liberó).
* **Actualizar su método de pago** — ingresar una nueva tarjeta a través del proveedor de pago de la suscripción y reintentar en el mismo paso.

Un reintento exitoso liquida el recibo pendiente, devuelve la suscripción a `active` y emite un evento `payment.recovered`. Un reintento manual rechazado mantiene el calendario automático de dunning. Los reintentos están limitados a 3 por día por cliente.

## Invitar a actualizar el pago

Redirige a los clientes al Portal del Cliente para reactivar:

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    const portal = await commet.portal.getUrl({ customerId: 'user_123' })

    redirect(portal.portalUrl)
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    portal = commet.portal.get_url(customer_id='user_123')

    redirect(portal.portal_url)
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    portal, err := client.Portal.GetURL(ctx, &commet.GetPortalURLParams{
        CustomerID: "user_123",
    })

    // redirect(portal.PortalURL)
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    var portal = commet.portal().getUrl(RequestPortalAccessParams.builder().customerId("user_123").build());

    // redirect(portal.portalUrl())
    ```
  </Tab>

  <Tab title="PHP">
    ```php theme={null}
    $portal = $commet->portal->getUrl(customerId: 'user_123');

    redirect($portal->portalUrl);
    ```
  </Tab>

  <Tab title="cURL">
    ```bash theme={null}
    curl -X POST https://commet.co/api/v1/portal/request-access \
      -H "x-api-key: $COMMET_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"customerId": "user_123"}'
    ```
  </Tab>
</Tabs>

## Relacionado

* [Recibos y ciclos de cobro](/docs/es/invoices-and-billing-cycles) — Tipos de recibo y momento del cobro
* [Administrar suscripciones](/docs/es/manage-subscriptions) — Crear y administrar suscripciones de clientes
* [Portal del Cliente](/docs/es/customer-portal) — Portal de autogestión de cobros y pagos para clientes
* [Payment Providers](/docs/es/payment-providers) — Cómo Commet enruta pagos a través de Commet, Stripe o dLocal
* [Payment Orchestration](/docs/es/payment-orchestration) — Routing por país, defaults y métodos asociados a un proveedor


## Related topics

- [Recibos y ciclos de cobro](/docs/es/invoices-and-billing-cycles.md)
- [Transacciones, Refunds y Retries](/docs/es/transactions-refunds-and-retries.md)
- [Períodos de prueba](/docs/es/trial-periods.md)
- [Payment Orchestration](/docs/es/payment-orchestration.md)
- [Payment Providers](/docs/es/payment-providers.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.