Pulse
7 7IT Solutions
Payments

Subscriptions and Billing Done Right: A Technical Guide

Lior Aharonov Lior Aharonov 6 min read Updated 2026-06-22

A one-time payment is a moment. A subscription is a relationship that changes over time, and that is what makes recurring billing so much harder than it looks. Customers start trials, upgrade mid-cycle, downgrade, pause, cancel, and have their cards expire, and every one of those events has to be reflected in what they can access and what you charge them. Get it slightly wrong and you either lock out paying customers or keep serving people who stopped paying, and both quietly cost you.

This guide covers how to build subscriptions that stay correct as that relationship evolves. The throughline is to let your payment processor own the hard parts of recurring billing and to keep your application's idea of who has access in lockstep with it. The examples assume Stripe on a Node or Vercel app, but the patterns apply to any billing provider.

Why subscriptions are harder than one-time payments

A single charge either succeeds or fails and you are done. A subscription carries state across months: a trial that converts, a plan that changes mid-cycle and needs proration, a card that fails and recovers, a cancellation that takes effect now or at period end, a refund. Each of these is a transition your system has to handle, and the bugs live in the transitions, not the steady state. So the design question is not "how do I charge a card every month," it is "how do I keep access and billing correct through every change."

Let the processor own the recurring logic

Do not build your own billing scheduler. The retry rules, proration math, tax handling, and dunning logic are deep, regulated, and easy to get subtly wrong, and providers like Stripe Billing have solved them. Define your products and prices there, let it run the recurring charges, and let it emit webhooks as things happen. Your application's job is not to run the clock, it is to react to the processor's events and reflect them. This keeps the money logic in a system built for it and your app focused on access.

Model access from subscription state, not the checkout

Here is the rule that prevents most subscription bugs: a user's access follows their current subscription status, kept up to date by webhooks, never granted at the moment of checkout. Checkout is one event in a long relationship, so if you flip a "paid" flag there, you have no clean way to revoke it when they cancel or their payment fails. Instead, store the subscription's status and plan, update it on every subscription webhook, and read access from that.

// access follows the subscription, kept current by webhooks
stripeWebhook.on(
  ["customer.subscription.updated", "customer.subscription.deleted"],
  async (sub) => {
    const active = ["active", "trialing"].includes(sub.status);
    await db.users.setPlan(sub.customer, { plan: sub.items[0].price.id, active });
  },
);
// grant features off stored state, never off "they paid once"
if (!user.active) return redirect("/billing");

Proration, upgrades, and downgrades

Customers change plans mid-cycle, and they expect the math to be fair: pay the difference when they upgrade today, get credit when they downgrade. Let the processor handle proration rather than computing it yourself, and decide your product rules deliberately, do upgrades take effect immediately and downgrades at period end, and does a plan change adjust features right away. Whatever you choose, the webhook that reports the change is what updates the user's stored plan, so the same access-from-state pattern keeps everything consistent.

Dunning: where revenue actually leaks

Cards fail constantly, expired, insufficient funds, a bank decline, and failed renewals are the single biggest source of lost subscription revenue, called involuntary churn. Handle it as a deliberate flow rather than an afterthought. Use the processor's smart retry schedule, notify the customer to update their card, keep access during a short grace period while you retry, and only then downgrade or suspend. Recovering even a fraction of failed payments often outweighs a lot of new sales, so this flow deserves real attention.

Trials, cancellations, and a self-serve portal

Round out the lifecycle. Handle trial endings so a trial converts or lapses cleanly, and support cancellation both ways, immediately or at period end, with access following the status. For refunds, keep your records in sync through the same webhooks. Finally, give customers a self-serve billing portal, the hosted Stripe Customer Portal is the fastest path, so they can update cards, change plans, and cancel without emailing you. It cuts support load and, by making card updates easy, directly reduces involuntary churn.

A subscriptions and billing checklist

  • Let the processor run recurring charges, proration, and dunning, do not build a scheduler.
  • Store subscription status and plan, and update them on every subscription webhook.
  • Grant access from stored state, never from the checkout event.
  • Decide upgrade and downgrade timing explicitly, and let the processor prorate.
  • Build a real dunning flow: smart retries, customer notices, a grace period, then downgrade.
  • Handle trial end, cancellation timing, and refunds through webhooks.
  • Offer a self-serve billing portal to cut support and reduce involuntary churn.

FAQ

Why are subscriptions harder to build than one-time payments?

Because a subscription is stateful over time. A single charge succeeds or fails and ends, but a subscription moves through trials, upgrades, downgrades, proration, failed and retried payments, and cancellations, and each transition has to update both billing and access. The hard bugs live in those transitions, not the steady monthly charge, so the real work is keeping what a customer can access and what you charge them correct through every change.

Should I build my own recurring billing or use the processor's?

Use the processor's. The retry logic, proration, tax, and dunning rules are deep and easy to get subtly wrong, and providers like Stripe Billing have already solved them. Define your products and prices there, let it run the charges, and have your application react to its webhooks. Building your own scheduler means reimplementing regulated money logic with no upside, while leaning on the provider keeps it correct and lets you focus on access.

When should I grant and revoke a customer's access?

Drive access from the subscription's current status, updated by webhooks, not from the checkout event. Store the status and plan, set access on every subscription update, and read features from that stored state. Granting access at checkout leaves you no clean way to revoke it when a customer cancels or a payment fails, whereas access-follows-status stays correct automatically through upgrades, lapses, and cancellations.

How do I handle failed subscription payments?

Treat it as a deliberate dunning flow, because failed renewals are the biggest source of lost subscription revenue. Use the processor's smart retry schedule, notify the customer to update their card, keep access during a short grace period while retries run, and only suspend or downgrade if recovery fails. Recovering even some of these payments often beats winning new customers, so the flow is worth building carefully rather than defaulting to an instant cutoff.

Do I need a customer billing portal?

It is one of the highest-value things you can add. A self-serve portal lets customers update cards, switch plans, and cancel on their own, which cuts support requests and, because updating an expired card is easy, directly reduces involuntary churn. The hosted Stripe Customer Portal gets you there quickly without building billing screens yourself, so you get the operational and retention benefits for very little effort.

If you are launching or fixing a subscription product and want the billing to stay correct as customers come and go, tell me how your plans work and I will map out a setup that holds up.

Want a hand applying this?

Tell me where your business is stuck and I will give you a straight, useful read, no pitch.

Go deeper

Payments

Embedding Stripe in a Headless WooCommerce and Next.js Store: What Actually Matters

A practical, field-tested guide to wiring Stripe into a headless WooCommerce and Next.js storefront, covering keys and authorization, the UI layer, tokens and PaymentMethods, the backend PaymentIntent, authorize-then-capture, and the webhook details that quietly break in production.

Read →
Payments

Revolut for WooCommerce: Why I Built My Own Integration

The off-the-shelf Revolut gateway got a WooCommerce store most of the way there, but the wallets were unreliable, the checkout jumped, and the card fields were so bare they cost customer trust. Here is why I rebuilt the integration around Revolut's secure card field, and how owning the UI fixed it without touching PCI scope.

Read →
Shopify

The Customer Portal Shopify Doesn't Give You (Reorders, Invoices, Approvals)

Fast reorders, invoice and PO access, saved carts, buyer approvals, and rich order history are where Shopify's native accounts stop short. Here is what a real customer portal looks like, often headless, and how we build it in phases you can trust.

Read →
Payments

Revolut Merchant API on Vercel: Wallets, 3D Secure, and the Sync vs Async Trap

A field-tested guide to the Revolut Merchant API on Vercel, covering the order-first flow, Apple Pay and Google Pay, how 3D Secure is handled, the sync versus async reality that catches teams out, and the webhook signature details that must be exactly right on a serverless function.

Read →
Custom Software

What Custom Software Actually Costs (and What Drives the Price)

An honest look at what drives the cost of custom software development for US businesses, scope, integrations, and the choices that make a build cheaper or more expensive.

Read →
Automation

API Integrations: Why Connecting Your Stack Beats Copy-Paste

Why manually moving data between your business tools is costing more than you think, and how API integrations make your software work as one system.

Read →