İçeriğe geç
wedevit

October 15, 2026 · 9 min read · software

İlhan Buğra Aslan

We moved to subscriptions and the numbers stopped adding up: building recurring billing


Subscription billing looks like one job from the outside: charge the same card every month. Inside, it is three independent mechanisms. There is a catalog that holds prices, a collection cycle that moves money, and an entitlement layer that decides what the customer can actually use. Most teams compress all three into one table, and the bill stops matching reality at the first plan change. After that come the quiet losses from reissued cards, the surprise cancellations when trials expire, and the moment finance asks which month a payment belongs to and nobody can answer. This piece covers how to separate those three mechanisms and which decisions to make before anyone writes code.

A subscription is not one column, it is three concepts

The first concept is the catalog: product, price, currency, interval. The second is the subscription itself: which customer, at which price, for which period, in which state. The third is entitlement: how many seats they get, which modules they see, what their monthly quota is.

Keeping them apart costs you an extra table or two. Merging them costs more than that. Raise the Pro plan from 400 to 500, update the single row in your plans table, and last month's invoices silently change amount too. In the same way, a system that stores a subscription as active or inactive has nowhere to express "payment failed but keep them in for three more days." The modelling decisions made in week one come back hardest exactly where these three concepts blur together.

If the price lives inside the plan, you can never raise it

Treat price records as immutable. A price increase is not an update to an existing row, it is a new price record that new subscribers point to. Existing customers stay on the old record until you deliberately migrate them. This is why Stripe, Paddle and the rest barely let you edit a price object at all.

The structure answers two questions for free: which price did this customer sign up on, and how many customers are still on the old one. Keep promotional pricing as a separate discount record rather than baking it into the price, because someone will eventually ask how many cycles the discount runs for and when it falls off. If you sell in more than one currency, define a price per currency instead of converting at charge time. The rules for storing money and exchange rates apply here without modification.

Mid-cycle plan changes: who does proration favour?

A customer on a 40 plan upgrades to 80 on the 15th of the month. Two decisions are waiting, and neither of them is technical: does the upgrade take effect immediately, and what do you charge for the remainder of the period?

Stripe calculates this to the second, and its documentation states the default is proration_behavior=create_prorations, which creates the adjustment line items without billing them. Two more sentences on that page explain most of the surprises teams hit: negative prorations are not automatically refunded, and positive prorations are not immediately billed. If you want the difference charged on the spot, you ask for always_invoice. If you want no proration at all, none.

The policy that works in practice is asymmetric. Apply upgrades immediately and invoice the difference right away; defer downgrades to the end of the period. That way you never have to manage refunds for time the customer already used. And before you show "you will be charged X now" on screen, preview the amount through the API and pass the same proration timestamp into the update call. Otherwise the number on the confirmation screen and the number on the card statement will drift apart.

What happens when a trial ends is a decision, not a default

Trials without a card convert better at signup. Trials with a card convert better into paid. Both are defensible positions. What is not defensible is leaving the no-card case undecided.

In Stripe the setting is trial_settings.end_behavior.missing_payment_method and it has three values: the default create_invoice issues an invoice and drops the subscription into past due, cancel ends the subscription, and pause freezes it until a payment method arrives. Each one produces a different customer experience and a different support queue. Reminders deserve the same deliberation. The customer.subscription.trial_will_end event fires three days before the trial ends by default, while Stripe's own reminder emails go out seven days ahead. If you send your own, pick those windows on purpose rather than inheriting them.

Revenue leaves quietly, and dunning does not run itself

In recurring billing the painful loss is rarely the customer who clicks cancel. It is the customer whose card declined and who simply disappeared. That makes retry policy a product decision rather than a settings toggle.

Stripe's recommended default is eight attempts within two weeks, and if you define your own schedule instead you get at most three retries. The part worth internalising is that not every decline is retryable. Codes such as lost_card, stolen_card and authentication_required are hard declines, and attempts made before the customer supplies a new payment method change nothing. When the schedule runs out, you choose what the subscription becomes: canceled, unpaid, left past due, or paused. Each choice implies a different recovery flow.

Half the work sits in communication. A dunning email that lands in spam is the same as an email you never sent, so why transactional mail fails to arrive is directly part of this feature, not a separate concern. Put a banner inside the product too, linking straight to the card update screen in one click.

Do not lose customers to a reissued card

Cards have shorter lives than subscriptions. Every card that expires, gets lost, or gets reissued after fraud is a failed charge waiting to happen if you do nothing.

Card networks run updater services for this, and your processor can refresh stored credentials on its own. Stripe's documentation is refreshingly honest about the limits: coverage is wide in the United States, varies by issuer elsewhere, and there is no way to tell in advance which cards support automatic updates. Treat it as a probability, not a guarantee. Do two things in parallel: listen for the update events (payment_method.automatically_updated) and refresh your own records, and send your own reminder for cards approaching expiry. Store the last four digits, the expiry and the processor's reference, never the card itself.

Recurring charges need consent, and consent is a record

Charging a stored card later requires the customer's agreement, and what that agreement must contain is well defined. Stripe's SCA guidance lists three things: permission for you to initiate payments on their behalf, the anticipated frequency of those payments, and how the amount is determined. Those three statements belong on the payment screen, visible, not buried in a terms link.

Under PSD2 the first transaction carries the authentication. Once a card is saved with the customer present and properly authenticated, later charges can be flagged as merchant-initiated and run off-session, though the issuer always keeps the final say and can still ask for a step-up. The same logic appears in other jurisdictions: in Turkey, for instance, the central bank's communique requires strong customer authentication at the moment a recurring payment mandate is set up. Whatever the regime, keep a record of which customer accepted which version of the terms and when. When a chargeback arrives, that audit record is precisely what gets asked for.

Invoicing is not the same event as collecting

Teams routinely treat a successful charge as the end of the transaction. It is the middle. Invoice numbering has to be sequential and gapless in most jurisdictions, invoices generally cannot be deleted once issued (you issue a credit note instead), and the deadline for issuing them is often measured in days after the service period rather than whenever your job happens to run.

Keep invoice generation out of the payment path. If your invoicing provider is slow or returns an error, the customer's checkout should not hang on it; queue the work and make it retryable. Decide the refund and cancellation flow up front, since partial-period refunds and credit notes are where most billing systems first go inconsistent. Tax is its own layer: the rate usually depends on where the customer is and whether they are a business, which means tax status belongs on the customer record and on the invoice, frozen at issue time.

Accounting sees a different picture again. Cash collected annually is not revenue for that month; under IFRS 15 it sits as a contract liability and converts to revenue as you deliver the service. Your software does not have to do the accounting, but it does have to report which portion of which payment belongs to which period. Agree that output with your finance team before they build a spreadsheet to reconstruct it.

Do not calculate MRR in three different places

Every company that moves to subscriptions eventually sees the same scene: one MRR number in the admin panel, a second in the finance spreadsheet, a third on the processor's dashboard. None of them is wrong. They are counting different things.

The questions to settle are short. Which month does an annual plan land in, are refunds deducted, do trialing subscriptions count, do mid-cycle proration lines go to recurring revenue or one-off revenue, is the figure gross or net of tax. Write those definitions down once and generate every report from a single source. The reason every report disagrees is almost always that nobody ever wrote them down.

Subscriptions that start on the 31st, and other counting traps

A monthly subscription that starts on 31 January cannot renew on 31 February. What date it lands on in February, whether it returns to the 31st in March, what happens on 29 February: these are rules, and they are not visibly correct until tested. The time zone used for period boundaries is the same kind of trap. Customer in Istanbul, server in UTC, billing day off by one, and the cause is usually right there. Date and time zone handling touches money directly in a billing system.

Usage-based pricing adds one more. Send meter events with a unique identifier so the same usage is never billed twice. Retries and queue redeliveries mean duplicate events are normal operating conditions, not an edge case, and the rules for preventing duplicates in payment integrations carry over unchanged.

A first step that fits this week

Open a document and fill in five rows: what happens on a mid-cycle upgrade, what happens on a downgrade, what happens when a trial ends without a card, on which day repeated payment failure cuts off access, and how long a canceled customer's data is retained. The number of teams that can answer all five is noticeably smaller than the number of teams running a subscription product.

Then do one more thing. Pick five live subscriptions at random and recalculate the last three invoices by hand. If they do not match, finding where the divergence starts is far cheaper than patching the report that surfaced it. If you want help reviewing an existing billing flow or setting one up correctly for a new product, come with your plan list and the last three months of collection data, and the conversation moves much faster.


Need help with this topic?

get in touch →← all posts