Saltar al contenido principal

Cobrar suscripciones

Suscripciones con la librería landapay: suscribe con un plan existente o definido en la misma llamada, activa el acceso con webhooks, cambia de plan con prorrateo y gestiona cancelaciones.

Cómo funciona

  1. Llamas a landapay.subscriptions.create con el plan, que puede ser un id o definirse en la misma llamada, y opcionalmente el cliente.
  2. Envías al cliente a checkoutUrl, donde registra su tarjeta, Nequi o cuenta Bancolombia.
  3. Se crea la suscripción y te llega el webhook subscription.created.
  4. Landa Pay cobra cada periodo y te avisa de cada cambio con webhooks.

Con el plan en la llamada

No necesitas crear el plan antes. Pasa sus datos en plan y la librería lo crea por ti, ideal cuando el precio depende de cada cliente, como una licencia por número de usuarios.

Suscribir con el plan en líneaTypeScript
import { landapay } from "landapay"; const { checkoutUrl, plan, customer } = await landapay.subscriptions.create({  plan: {    name: "Licencia Pro - 5 usuarios",    interval: "monthly",    amount: 4_990_000, // $49.900 COP al mes    metadata: { seats: 5 },  },  customer: { name: "María Gómez", email: "[email protected]" },  externalId: "license_8812",  metadata: { workspace: "acme" },  redirectUrl: "https://tuapp.com/suscripcion",}); res.redirect(checkoutUrl);

El resultado trae checkoutUrl, el checkout completo, el plan que se creó y el id del customer. Si no envías name, el plan se llama según su frecuencia, por ejemplo “Suscripción mensual”.

intervalFrecuencia de cobro
weeklyCada semana
quincenalCada 15 días
monthlyCada mes
quarterlyCada 3 meses
semiannualCada 6 meses
annualCada año

Con un plan existente

Si vendes los mismos planes a todos tus clientes, créalos una vez con landapay.plans.create o en el panel, y usa su id:

Suscribir con un plan existenteTypeScript
const { checkoutUrl } = await landapay.subscriptions.create({  plan: "plan_PLANCODE23456789",  customer: "cus_ABCDEFGHJKLMNPQR",  externalId: "user_42",});

Prueba gratis y precios por etapas

Con amount y trialDays defines un precio simple con prueba gratis. Con phases defines precios por etapas: cada fase lleva su duration salvo la última, que se repite hasta el final. Usa termCycleCount para terminar la suscripción tras un número de cobros.

Precios avanzadosTypeScript
// 14 días gratis y luego $49.900 al mesawait landapay.subscriptions.create({  plan: { interval: "monthly", amount: 4_990_000, trialDays: 14 },  customer: "cus_ABCDEFGHJKLMNPQR",}); // 3 meses a $19.900, luego $49.900, durante 12 cobros en totalawait landapay.subscriptions.create({  plan: {    name: "Promo lanzamiento",    interval: "monthly",    termCycleCount: 12,    phases: [      { type: "priced", amount: 1_990_000, duration: { unit: "month", count: 3 } },      { type: "priced", amount: 4_990_000 },    ],  },  customer: "cus_ABCDEFGHJKLMNPQR",});

El cliente

  • Con un id (cus_...) se usa ese cliente.
  • Con sus datos se crea el cliente, o se reutiliza el que ya tenga ese email o externalId.
  • Sin cliente, el checkout le pide sus datos.

Con un cliente, su email queda fijo en el checkout y solo se piden los datos que le falten.

Después del checkout

La suscripción se crea solo cuando el cliente completa el checkout, con el primer cobro o con el periodo de prueba del plan, y hereda el externalId y la metadata. El checkout vence en 24 horas, o en el expiresAt que envíes (máximo 7 días).

Con redirectUrl el cliente vuelve a tu URL con subscription_id y checkout_id; sin ella ve la página de resultado de Landa Pay.

Al volver del checkoutTypeScript
app.get("/suscripcion", async (req, res) => {  const checkout = await landapay.subscriptions.retrieveCheckout(String(req.query.checkout_id));   if (checkout.status === "completed" && checkout.subscription) {    const subscription = await landapay.subscriptions.retrieve(checkout.subscription);    return res.render("suscripcion-lista", { status: subscription.status });  }  res.render("suscripcion-pendiente");});

Cada cobro de la suscripción es un pago con el campo subscription, y te llega con los eventos payment.*.

Activar el acceso

No actives el acceso solo porque el cliente volvió

Espera el webhook y revisa status. Da acceso solo si es active o trialing.
Reaccionar a los eventosTypeScript
const event = landapay.webhooks.constructEvent(req.body, req.get("Landa-Signature")); switch (event.type) {  case "subscription.created":  case "subscription.activated":  case "subscription.past_due":  case "subscription.canceled":  case "subscription.completed": {    const subscription = event.data.object;    const allowed = subscription.status === "active" || subscription.status === "trialing";    if (allowed) await grantAccess(subscription.externalId, subscription.id);    else await revokeAccess(subscription.externalId);    break;  }}

No existe subscription.unpaid. Al fallar un cobro llega subscription.past_due y ahí quitas el acceso. unpaid significa que se agotaron los reintentos; el acceso ya debía estar quitado. No lo devuelvas hasta subscription.activated.

EventoCuándo ocurre
subscription.createdEl cliente completó el checkout y se creó la suscripción
subscription.activatedLa suscripción quedó activa: primer cobro aprobado, fin de la prueba o recuperación de un cobro fallido
subscription.past_dueFalló el cobro de renovación y la suscripción quedó en mora
subscription.cancel_scheduledSe programó la cancelación para el final del periodo
subscription.resumedSe quitó la cancelación programada
subscription.canceledLa suscripción terminó por cancelación
subscription.completedLa suscripción terminó al cumplir los ciclos del plan
subscription.plan_change_scheduledSe programó un cambio de plan para el final del periodo
subscription.plan_changedLa suscripción pasó al plan nuevo
subscription.plan_change_failedEl cobro del cambio de plan fue rechazado; la suscripción sigue en su plan

Aprende a configurar el endpoint y verificar la firma en Recibir webhooks.

Actualizar el método de pago

Si un cobro falla porque la tarjeta cambió o no tiene saldo, no pidas los datos en tu app. Crea una sesión y redirige al cliente a Landa Pay para ver, agregar o eliminar sus métodos. El enlace es de un solo uso, caduca en 15 minutos y la API key no sale de tu servidor.

Enviar al cliente a actualizar su métodoTypeScript
const session = await landapay.subscriptions.createPaymentMethodSession(  "sub_SUBSCRIPTION2345",  { returnUrl: "https://tuapp.com/facturacion" },); res.redirect(session.url);

returnUrl es opcional. Si no lo envías, un cobro aprobado vuelve al redirectUrl del checkout de esa suscripción. La vuelta trae subscription_id y payment_id. Confirma con payments.retrieve o con el webhook; volver no activa el acceso.

  • Si la suscripción está past_due o unpaid, guardar un método nuevo o elegir uno anula los periodos impagos y cobra de inmediato un ciclo nuevo desde ese momento. Si aprueba, llega payment.approved y subscription.activated, y ahí devuelves el acceso.
  • Si está al día, solo se guarda el método. Los próximos cobros salen de ahí y no se cobra nada en ese momento.
  • Si el cobro se rechaza, el cliente se queda en la página y puede probar otra tarjeta.

Estados

EstadoQué significa
incompleteSe está procesando el primer cobro
trialingEn periodo de prueba; el primer cobro llega al terminarlo
activeAl día con sus cobros
past_dueFalló un cobro de renovación; seguimos reintentando
unpaidSe agotaron los reintentos de cobro
canceledCancelada; ya no se cobra
completedCumplió los ciclos del plan

Los periodos que no se cobraron no se recuperan. Cuando el cliente guarda un método de pago, se abre un ciclo desde ese momento. Si ese cobro se aprueba, llega subscription.activated.

Cancelar y reanudar

Por defecto la suscripción sigue activa hasta el final del periodo pagado (subscription.cancel_scheduled) y puedes deshacerlo con resume. Con atPeriodEnd: false termina de inmediato (subscription.canceled). En ambos casos el cliente recibe un correo.

Cancelar y reanudarTypeScript
// Al final del periodo pagado; se puede deshacerawait landapay.subscriptions.cancel("sub_SUBSCRIPTION2345", { reason: "Cambio de plan" });await landapay.subscriptions.resume("sub_SUBSCRIPTION2345"); // De inmediatoawait landapay.subscriptions.cancel("sub_SUBSCRIPTION2345", { atPeriodEnd: false });

Cambiar de plan

Una suscripción active o trialing puede subir o bajar de plan. Es una subida si el plan nuevo cuesta más por día que el actual, así que se pueden comparar planes de distinta frecuencia, y un empate cuenta como subida. Antes de confirmar, muestra a tu cliente lo que pasará con previewPlanChange; no cambia nada.

Ver el efecto antes de cambiarTypeScript
const preview = await landapay.subscriptions.previewPlanChange("sub_SUBSCRIPTION2345", {  plan: "plan_PRO2345ABCDEFGH",}); preview.direction; // "upgrade" o "downgrade"preview.behavior; // el que se usará: el del panel o el que envíespreview.charge; // lo que se cobra hoy, en centavospreview.balanceDelta; // lo que queda a favor del cliente (positivo) o se suma al próximo cobropreview.effectiveAt; // cuándo entra en vigorpreview.nextBillingAt; // próximo cobro con el plan nuevopreview.nextAmount; // y su monto, ya con el saldo aplicado

Comportamientos

Ejemplo: el cliente paga $50.000 al mes por Básico, ya usó el 20 % del periodo y sube a Pro de $100.000. Le quedan $40.000 sin usar del plan actual ($50.000 × 0,8).

behaviorQué haceEn el ejemplo
prorateCambia ya y conserva la fecha de cobro. Cobra el plan nuevo por lo que falta del periodo menos lo no usado del actualCobra $40.000 hoy; el próximo cobro es de $100.000 en la fecha de siempre
resetCambia ya y empieza un ciclo nuevo hoy. Cobra el plan nuevo completo menos lo no usado del actualCobra $60.000 hoy; el próximo cobro es en un mes desde hoy
no_prorateCambia ya sin cobrar ni abonar nada. El próximo cobro ya es con el precio nuevoNo cobra hoy; el próximo cobro es de $100.000 en la fecha de siempre
period_endPrograma el cambio para el final del periodo pagado; se puede deshacerNo cobra hoy; al renovar ya cobra $100.000
  • Si el plan nuevo tiene otra frecuencia, prorate se aplica como reset porque los periodos no coinciden.
  • En prueba gratis no hay nada cobrado: el cambio se aplica sin cobro y la prueba termina en la misma fecha, salvo con period_end, que espera al final de la prueba.
  • El plan nuevo empieza en su primera fase con precio; su prueba gratis no se repite.
  • Pedir otro cambio reemplaza al que estaba programado.

Cobro y saldo a favor

Si hay algo que cobrar, se cobra en el momento al método de pago de la suscripción y el cambio solo se aplica si el cobro se aprueba. Si se rechaza, recibes un InvalidRequestError con código payment_declined y la suscripción sigue en su plan. Con Nequi o Bancolombia el cobro puede quedar pendiente: pendingPlanChange.status es pending_payment hasta que te llegue subscription.plan_changed o subscription.plan_change_failed.

El cambio genera su propia factura con kind: "plan_change". Lo no usado del plan anterior aparece como descuento en creditApplied.

Cuando el resultado favorece al cliente, por ejemplo al bajar de Pro a Básico con prorate ($40.000 − $80.000), la diferencia queda como saldo a favor en subscription.balance y se descuenta de los próximos cobros: el siguiente de Básico sería de $10.000. Un cobro menor a $2.000 COP no se hace en el momento: el cambio se aplica y el monto se suma al próximo cobro. El saldo negativo es esa deuda pendiente.

Cambiar de planTypeScript
import { InvalidRequestError, landapay } from "landapay"; try {  const subscription = await landapay.subscriptions.changePlan("sub_SUBSCRIPTION2345", {    plan: "plan_PRO2345ABCDEFGH",    behavior: "prorate", // opcional; sin él se usa lo configurado en el panel  });   if (subscription.pendingPlanChange?.status === "pending_payment") {    // Nequi o Bancolombia: el cambio se aplica cuando se apruebe el cobro  }} catch (error) {  if (error instanceof InvalidRequestError && error.code === "payment_declined") {    // El cliente sigue en su plan  }} // Plan definido en la misma llamada, como en createawait landapay.subscriptions.changePlan("sub_SUBSCRIPTION2345", {  plan: { name: "Licencia Pro - 10 usuarios", interval: "monthly", amount: 8_990_000 },}); // Deshacer un cambio programado con period_endawait landapay.subscriptions.cancelPlanChange("sub_SUBSCRIPTION2345");

Configuración del proyecto

En el panel, en Configuración › Facturación › Cambios de plan, defines el comportamiento por defecto para subidas (prorate) y bajadas (period_end), si se permite bajar de plan y si se permite cambiar a un plan con otra frecuencia. El behavior que envíes en la llamada tiene prioridad sobre el del panel.

ErrorCuándo ocurre
409 same_planLa suscripción ya está en ese plan
409 downgrade_not_allowedEl proyecto no permite bajar de plan
409 interval_change_not_allowedEl proyecto no permite cambiar de frecuencia
409 subscription_past_due, subscription_incomplete, subscription_endedSolo cambian de plan las suscripciones active o trialing
409 cancel_scheduledTiene una cancelación programada; reanúdala primero
409 plan_change_in_progressYa hay un cobro de cambio de plan esperando confirmación
409 no_plan_change_scheduledNo hay un cambio programado para deshacer
400 currency_mismatchEl plan nuevo tiene otra moneda
402 payment_declinedSe rechazó el cobro; la suscripción sigue en su plan

Webhooks

Usa subscription.plan_changed para ajustar los límites o el acceso de tu cliente; el evento trae la suscripción con su plan nuevo. El cliente también recibe un correo con el cambio.

Sincronizar el plan nuevoTypeScript
switch (event.type) {  case "subscription.plan_changed": {    const subscription = event.data.object;    const plan = await landapay.plans.retrieve(subscription.plan);    await updateLimits(subscription.externalId, plan.metadata);    break;  }  case "subscription.plan_change_failed":    await notifyUser(event.data.object.externalId, "No pudimos cobrar el cambio de plan");    break;}

Facturas de la suscripción

Cada ciclo tiene su factura, igual que en el tab Cobros del panel. Lístalas con landapay.subscriptions.listInvoices: llegan de 15 en 15 (máximo 50 con limit), la más reciente primero, con totalPages y hasMore para tu paginación. Cada factura trae su number y la url pública para mostrarla a tu cliente.

Los cobros de cambio de plan tienen su propia factura con kind: "plan_change" y cycle: null. Usa amountDue para mostrar lo cobrado: amount es el precio antes de descontar el saldo (creditApplied).

Tabla de facturas paginadaTypeScript
// Página de 15 facturas, la más reciente primeroconst page = await landapay.subscriptions.listInvoices("sub_SUBSCRIPTION2345", {  page: Number(searchParams.get("pagina") ?? 1),}); const rows = page.data.map((invoice) => ({  number: invoice.number, // FAC-00012  url: invoice.url, // ábrelo con target="_blank"  amount: invoice.amount,  status: invoice.chargeStatus,})); console.log(`Página ${page.page} de ${page.totalPages}`); // O recórrelas todas sin manejar páginasfor await (const invoice of landapay.subscriptions.listInvoices("sub_SUBSCRIPTION2345")) {  console.log(invoice.number, invoice.paidAt);}