Subscription billing patterns

A subscription isn't one payment — it's a recurring process with its own lifecycle events, its own failure-retry logic, and a real gap between what a gateway thinks is true and what a merchant's own database currently says.

Intermediate

3 min read

A subscription is a separate object from any single payment

A one-time charge and a subscription are different things a gateway tracks separately: the subscription is the ongoing agreement to bill on a schedule; each individual successful (or failed) charge against it is its own transaction-level event. A webhook-driven system has to listen for both kinds of events — subscription lifecycle events (created, renewed, cancelled, payment failed) and the underlying transaction events for each individual charge — and these commonly arrive as genuinely different event shapes, sometimes even signed differently (a detail covered in the previous lesson).

The recurring-charge lifecycle

  • Trial period: access granted before any charge occurs; the first real charge happens when the trial ends, not at signup.
  • Renewal: the gateway attempts to charge the customer's saved payment method automatically, on schedule, without the customer doing anything.
  • Dunning: the retry process after a failed renewal — a sequence of retry attempts (often with increasing delay) before the subscription is finally cancelled or marked past-due, since a single declined card is frequently a transient problem (insufficient funds that get topped up, an expired card the customer hasn't updated yet), not a certain sign the customer wants to leave.

Proration: the part that surprises people building this for the first time

If a customer upgrades or downgrades mid-cycle, the fair charge for that cycle isn't the full new price — it's some blend of the old and new price, weighted by how much of the cycle has already elapsed at each rate. This calculation ("proration") is easy to get subtly wrong by hand; most gateways compute and expose it, but it's worth understanding conceptually rather than treating it as an opaque number, since edge cases (mid-cycle plan changes, mid-cycle cancellations with a refund for unused time) are exactly where hand-rolled proration logic tends to drift from what customers actually expect to be billed.

Why a reconciliation job exists even though webhooks already update local state

Webhooks are the authoritative event stream (per an earlier lesson) — but "authoritative" doesn't mean "guaranteed to arrive." A webhook can be lost outright if the gateway's retries are exhausted before a merchant's endpoint recovers from an outage. This is why systems billing on subscriptions commonly run a periodic reconciliation job: pull each active subscription's current status directly from the gateway's API, and if it disagrees with the locally stored status, treat the gateway's answer as correct and update the local record. A well-designed version of this job also has a fallback: if the gateway itself is unreachable when the job runs, keep the last-known local status rather than treating a network failure as "the subscription must have been cancelled."

The state a subscription record actually needs

status              - TRIALING | ACTIVE | PAST_DUE | CANCELLED
currentPeriodStart  - the current billing cycle's start
currentPeriodEnd    - when the next renewal attempt happens
cancelAtPeriodEnd   - a customer-requested cancellation that takes
                       effect at the end of the period they already
                       paid for, not immediately

That last field matters for a specific, common product decision: when a customer cancels, do they lose access immediately, or keep it until the period they already paid for actually ends? Most subscription products choose the latter — cancelAtPeriodEnd: true plus continued access until currentPeriodEnd, rather than an immediate cutoff — since the customer already paid for that remaining time.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. A customer's card is declined on their subscription's renewal date. What should happen immediately, per standard dunning practice?

2. A customer upgrades their subscription plan halfway through a billing cycle. What should they be charged for that cycle?

3. A subscription's webhook events were lost during an extended endpoint outage that outlasted the gateway's retry window. What's the correct way to recover the accurate subscription status?

4. A reconciliation job runs, but the gateway's API is temporarily unreachable. What should happen to the locally stored subscription status?