Planes personales

Un plan gratuito y planes pagos para cada usuario, con vuelta al gratuito al cancelar o dejar de pagar.

Para un producto donde cada persona paga lo suyo. El ejemplo tiene tres planes: Hobby gratis, Plus y Pro pagos.

El modelo

En tu aplicaciónEn Billete
El usuario user_42Un cliente con externalId: "user_42"
Su planUna sola suscripción, que cambia de plan
HobbyUn plan con precio 0
Plus y ProPlanes pagos con fallbackPlanId apuntando a Hobby
  • Una suscripción por usuario, para siempre. Subir, bajar, cancelar o dejar de pagar cambian el plan de esa misma suscripción; nunca se crea otra. Su ID no cambia, así que puedes guardarlo en tu usuario.
  • Hobby también es una suscripción. Así todo usuario tiene sus cuotas en Billete desde que se registra, y subir de plan es un cambio sobre ella, no una compra aparte.
  • El respaldo hace la vuelta a Hobby. Si el usuario cancela o no paga la renovación, Billete pasa la suscripción a Hobby sin que tu aplicación intervenga (vuelta al plan gratuito).

1. Crea los planes

Créalos una vez, desde el dashboard o con el SDK, y guarda sus IDs en la configuración de tu aplicación. Las cuotas (aquí, mensajes) deben existir antes.

const hobby = await billete.plans.create({
  name: "Hobby",
  lookupKey: "hobby",
  currency: "USD",
  prices: [{ months: 1, amount: 0 }],
  features: { mensajes: 50 },
});

const plus = await billete.plans.create({
  name: "Plus",
  lookupKey: "plus",
  currency: "USD",
  prices: [{ months: 1, amount: 1000 }, { months: 12, amount: 10000 }],
  gracePeriodDays: 3,
  fallbackPlanId: hobby.id,
  features: { mensajes: 2000 },
});
// Pro igual que Plus, con otro precio y más cuota.

El respaldo tiene que ser gratuito, con puestos incluidos y sin respaldo propio; un plan con respaldo no puede además suspender. Mira puestos y respaldo.

2. Activa Hobby al registrar al usuario

const subscription = await billete.subscriptions.create({
  customer: { externalId: user.id, email: user.email },
  plan: PLANS.hobby,
  months: 1,
}, { idempotencyKey: `hobby-${user.id}` });

await db.users.update(user.id, { subscriptionId: subscription.id });

customer como objeto crea el cliente o reutiliza el que ya tiene ese externalId. Un plan gratuito queda active en el acto, sin cobro. La clave de idempotencia hace seguro reintentar; sin ella, una segunda suscripción a Hobby responde subscription_exists.

3. Sube de plan

const result = await billete.subscriptions.changes.create(user.subscriptionId, {
  plan: PLANS.pro,
  months: 12,
  when: "now",
  successUrl: "https://tuapp.com/ajustes/plan",
});

if (result.type === "checkout") {
  return redirect(result.checkout.url);
}

Una subida que cuesta algo devuelve un checkout alojado: el usuario conserva su plan actual hasta que paga, y el cambio se aplica al confirmarse el pago. Entre Plus y Pro, lo que queda del periodo pagado se descuenta del precio nuevo. Para mostrar cuánto pagará antes de enviarlo, usa changes.preview con los mismos datos.

Durante los 3 días previos a la renovación de un plan pago, o en su gracia, un cambio inmediato responde subscription_renewal_open: ofrece pagar la renovación o programar el cambio.

4. Baja de plan o vuelve a Hobby

// De Pro a Plus: conserva Pro hasta el final del periodo que ya pagó.
await billete.subscriptions.changes.create(user.subscriptionId, {
  plan: PLANS.plus,
  when: "renewal",
});

// A Hobby: cancela al final del periodo y el respaldo hace el resto.
await billete.subscriptions.cancel(user.subscriptionId);

Programa las bajadas para la renovación: aplicarlas en el acto pierde lo que queda del periodo pagado, porque no hay saldo a favor (acceptUnusedCredit). Cancelar desde el portal produce lo mismo. Solo si el usuario cierra su cuenta en tu aplicación termina la suscripción del todo, con cancel(id, { atPeriodEnd: false, fallback: false }).

5. Da acceso y escucha los eventos

Lo que el usuario puede usar sale de las cuotas de su plan, así que tu aplicación consulta a Billete en vez de traducir cada plan a permisos propios:

const { allowed } = await billete.features.consume(user.id, "mensajes", 1, {
  idempotencyKey: `mensaje-${message.id}`,
});

Los eventos sirven para mostrar el plan y avisar al usuario. Las entregas pueden repetirse y llegar en otro orden, así que guarda lo que dice la suscripción al consultarla, no lo que suponga cada evento:

const subscription = await billete.subscriptions.retrieve(event.data.id);
await db.users.updateBySubscription(subscription.id, {
  planId: subscription.planId,
  status: subscription.status,
});
EventoQué pasóQué hace tu aplicación
subscription.updatedCambió el plan: una subida pagada, una cancelación programada o la vuelta a Hobby (previousPlanId dice desde cuál).Muestra el plan nuevo. Si volvió a Hobby, ofrécele volver a pagar.
subscription.renewedPagó la renovación; si había una bajada programada, ya rige.Muestra el plan y el periodo nuevos.
subscription.past_dueVenció sin pago. Conserva el acceso durante la gracia.Pídele que pague la renovación.
subscription.change_failedEl pago de una subida llegó tarde y se reembolsa.Dile que sigue en su plan y que lo intente otra vez.

En este modelo no llegan subscription.expired ni subscription.canceled: el respaldo los reemplaza por subscription.updated, salvo al cerrar la cuenta con fallback: false.

Referencia de suscripciones · Referencia de planes