• Precios
  • Blog
Iniciar sesiónAgendá una demo
Introducción

Inicio rápido

Aprender

Recibos y ciclos de cobroManejar pagos fallidos

Recursos

Referencia del SDKVersionado de APIManejo de erroresTestingCLI

Plugins

Better Auth
DocumentaciónRecursosConstruir con AIAPI ReferenceWebhooks

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 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 pagoQué significaQué hacer
requires_actionEl proveedor necesita un paso adicional del cliente, como 3D SecureMantén al cliente en el checkout y completa el flujo de autenticación
PAYMENT_FAILED con un código de rechazoEl proveedor rechazó el cobroMuestra un retry o un método de pago alternativo y guarda el código del proveedor para soporte
payment.failedFalló un cobro recurrente y el recibo ingresó en dunningMantén el servicio durante la ventana de gracia, comunica la recuperación y espera un retry o una recuperación
payment.retry_failedSe agotaron todos los retries de dunning programadosRestringe 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.

Automatic retries

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 reactivate — cuentan contra el mismo calendario: un reintento manual rechazado consume el siguiente slot programado. Cuatro reintentos rechazados cancelan la suscripción incluso antes del día 7.

Los retries usan la conexión de pago ya asociada a la suscripción. Cambiar el routing por país no mueve el retry a otro proveedor y Commet no cambia silenciosamente un método guardado a otra cuenta.

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 — falló un cobro recurrente; usa failureCode, failureMessage y recoveryUrl para explicar el siguiente paso.
  • payment.recovered — se pagó el recibo pendiente y la suscripción volvió a active.
  • 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

const subscription = await commet.subscriptions.getActive({ customerId: 'user_123' })

if (subscription?.status === 'past_due') {
  showRecoveryPrompt()
}
subscription = commet.subscriptions.get_active(customer_id='user_123')

if subscription is not None and subscription.status == 'past_due':
    show_recovery_prompt()
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()
}
var subscription = commet.subscriptions().getActive(GetActiveSubscriptionParams.builder("user_123").build());

if (subscription != null && subscription.status() == SubscriptionStatus.PAST_DUE) {
    showRecoveryPrompt();
}
$result = $commet->subscriptions->getActive('user_123');

if ($result !== null && $result->status->value === 'past_due') {
    showRecoveryPrompt();
}
curl "https://commet.co/api/v1/subscriptions/active?customerId=user_123" \
  -H "x-api-key: $COMMET_API_KEY"

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:

const subscription = await commet.subscriptions.getActive({ customerId: 'user_123' })

const hasAccess = subscription !== null &&
  (subscription.status === 'active' || subscription.status === 'trialing')
subscription = commet.subscriptions.get_active(customer_id='user_123')

has_access = subscription is not None and subscription.status in ('active', 'trialing')
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")
var subscription = commet.subscriptions().getActive(GetActiveSubscriptionParams.builder("user_123").build());

boolean hasAccess = subscription != null &&
    (subscription.status() == SubscriptionStatus.ACTIVE ||
        subscription.status() == SubscriptionStatus.TRIALING);
$result = $commet->subscriptions->getActive('user_123');

$hasAccess = $result !== null &&
    in_array($result->status->value, ['active', 'trialing'], true);
curl "https://commet.co/api/v1/subscriptions/active?customerId=user_123" \
  -H "x-api-key: $COMMET_API_KEY"

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

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.

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 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 rechazado consume el siguiente slot del calendario 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:

const portal = await commet.portal.getUrl({ customerId: 'user_123' })

redirect(portal.portalUrl)
portal = commet.portal.get_url(customer_id='user_123')

redirect(portal.portal_url)
portal, err := client.Portal.GetURL(ctx, &commet.GetPortalURLParams{
    CustomerID: "user_123",
})

// redirect(portal.PortalURL)
var portal = commet.portal().getUrl(RequestPortalAccessParams.builder().customerId("user_123").build());

// redirect(portal.portalUrl())
$portal = $commet->portal->getUrl(customerId: 'user_123');

redirect($portal->portalUrl);
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"}'

Relacionado

  • Recibos y ciclos de cobro — Tipos de recibo y momento del cobro
  • Administrar suscripciones — Crear y administrar suscripciones de clientes
  • Portal del Cliente — Portal de autogestión de cobros y pagos para clientes
  • Payment Providers — Cómo Commet enruta pagos a través de Commet, Stripe o dLocal
  • Payment Orchestration — Routing por país, defaults y métodos asociados a un proveedor

¿Cómo está esta guía?

Recibos y ciclos de cobro

Cómo Commet genera recibos, qué contienen y cuándo se cobra a los clientes.

Aceptar pagos únicos

Cobra a tus clientes una sola vez con el recurso payments de Commet — sin suscripción ni plan. Impuestos, recibo y comprobante de forma automática.

On this page

Qué pasa cuando un pago falla
Mapeo de errores del proveedor
Automatic retries
Dunning communications
Consultar el estado de la suscripción
Restringir el acceso según el estado
Recuperar una suscripción de forma programática
Reintentar el cobro de servidor a servidor
Enviar un enlace de recuperación al cliente
Actualizar el método de pago
Self-serve recovery
Invitar a actualizar el pago
Relacionado