Billing & Subscriptions

Membership billing without a subscriptions platform: proration, pauses, and renewals

Not every recurring-revenue business fits Stripe Billing's assumptions cleanly. Here's how we handled tiered memberships, mid-cycle changes, and pauses for a subscription rental library on plain payment-gateway primitives.

By Rehan Idrisi · · Updated · 6 min read

Part of: Payments & Gateways

Off-the-shelf subscription billing tools handle the common case well. One plan, one price, upgrade or cancel any time. Real membership businesses rarely stay that simple for long: tiered packages, items on loan that have to come back before a plan can downgrade, pauses for a family going on holiday, referral credits that offset a renewal. Once the rules stop being generic, building billing logic on a payment gateway's raw primitives usually beats fighting a subscriptions platform's opinions.

We built exactly that for a subscription toy-and-book rental library with catalogued products, tiered membership packages and a referral engine. The billing rules there were specific enough to the business that no off-the-shelf platform matched them without workarounds of its own.

A subscriptions platform is optimised for the subscriptions its designers imagined. The moment your billing rules are specific to your business, that's a sign to own the state machine yourself.

Model membership state, not just payment status

A membership has a tier, a renewal date, a set of items currently on loan, and a history of pauses and referral credits. Paid or not paid barely scratches it. So we modelled membership as its own state machine (ACTIVE, PAUSED, PAST_DUE, CANCELLED), independent of any single payment gateway's subscription object. Business logic such as whether this member can downgrade then checks our own rules against our own data, never a third party's schema.

Proration is arithmetic

Mid-cycle tier changes need a fair adjustment, and the maths is simpler than it looks. Take the unused portion of the current cycle, days remaining divided by cycle length times the current tier's price, as a credit. Then charge the difference against the new tier's prorated cost for that same remaining period. We calculate this server-side at the moment of the change and show the member the exact number before they confirm, because people trust proration far more when they can see the working, not just a total.

function prorate(currentTierPrice, newTierPrice, daysRemaining, cycleDays) {
  const unusedCredit = (daysRemaining / cycleDays) * currentTierPrice;
  const newTierCost = (daysRemaining / cycleDays) * newTierPrice;
  return Math.max(0, newTierCost - unusedCredit); // charge now, or 0 if downgrading
}

A downgrade can't outrun a physical return

Here is the constraint a generic subscriptions platform has no concept of at all. A member on the top tier might have several items physically on loan, and a downgrade to a tier with a lower loan allowance cannot take effect instantly if it would leave them holding more items than the new tier permits. So a downgrade's effective date is gated on one of two things: the member returning enough items to fit the new allowance, or the change landing at the next renewal instead of immediately. The member picks. The system never lets loan count and tier allowance contradict each other.

Pauses need a real hold state

Cancelling a membership and asking the member to resubscribe later loses their tenure, their referral history, and frequently the member themselves. Signing up again is real friction. We built an explicit PAUSED state instead. Billing stops, outstanding rentals are governed by their own return-by rules independent of billing, and resuming reactivates the same membership record with its history intact, on the next cycle boundary instead of an arbitrary resume date.

Keep the payment gateway as a dumb charging engine

  1. Store a reusable payment method token against the member. Not a recurring subscription object owned by the gateway.
  2. A scheduled job charges the correct amount against that token on each member's actual renewal date, whether that amount is full price, prorated or referral-adjusted.
  3. Failed charges go through your own dunning logic: a short grace period, a retry, then a move to PAST_DUE. Whatever default the gateway applies is not it.
  4. Reconcile nightly. Every ACTIVE membership should have a matching successful charge in the expected window, and any gap gets flagged before a member notices.

Referral credits are a ledger, not a coupon code

A referral programme that simply discounts the next invoice breaks the moment credits need to roll over, split across two referred members, or get reversed after a refund. We track referral credit as its own ledger, with each grant and each redemption as a separate row. A renewal then asks one question: what is this member's current credit balance? No coupon bookkeeping required.

Let the payment gateway own charging money. Let your own database own what membership means for this business.

Dunning that doesn't lose a member over an expired card

Most failed renewal charges are not fraud, and not a member dodging payment. They are an expired card or a temporary bank-side decline. Cancel a membership on the first failed charge and you treat both cases identically, losing members you never needed to lose. Our dunning sequence opens with a short grace period, membership still ACTIVE, then a retry a few days later, with a reminder email or SMS prompting a card update in between. Only once the retry also fails does the membership move to PAST_DUE, with access paused and the membership itself not cancelled outright. Cancellation stays a separate, explicit step, never an automatic side effect of one bad charge.

Testing billing logic without charging a real card every time

Proration, pause and resume, and dunning transitions all involve dates and cycle boundaries that are awkward to test by waiting for a real renewal to happen. The state machine and the proration maths get tested against a set of fixed reference dates and cycle configurations, with the payment gateway out of the picture entirely. The gateway integration itself is exercised separately in sandbox mode, for the charge-and-webhook path alone. That split keeps the bulk of the billing logic fast and deterministic to test, while the real integration still gets verified on its own terms.

How do you handle mid-cycle tier changes and pauses in custom membership billing?

Model membership as its own state machine, active, paused, past-due and cancelled, independent of the payment gateway, and compute proration server-side as unused-cycle credit set against the new tier's cost. Give pauses a real hold state that preserves tenure and history, then reconcile charges against expected renewals every night.

Share this guideLinkedInXWhatsAppFacebook
All guides