What a Successful Checkout Actually Proves
A successful subscription checkout proves that a payment method was accepted, that an amount was authorized and captured under a particular set of conditions, and that a recurring object now exists at the payment provider. That is a genuinely useful thing to have proven. It is also a narrow claim.
It says nothing about what happens when the same card is declined in four weeks. It says nothing about whether the customer keeps access during the decline. It says nothing about what the account should look like if the customer upgrades eleven days into a period they already paid for, or what the finance team should see when a support agent refunds half of an invoice, or what the product should do when the same event arrives twice because a worker timed out and the provider retried delivery.
Those questions are not exotic. They describe the ordinary operating conditions of any subscription product that has been alive for a year. The checkout integration is the part of billing that is easiest to specify, easiest to test, and easiest to demonstrate. Everything that follows it is harder to specify, harder to test, and much more expensive to get wrong, because errors in that territory affect revenue records, customer access, and support workload simultaneously.
Most teams begin with a mental model that looks roughly like this:
customer → checkout → payment provider → payment → subscription active
It is a reasonable starting point. It is also a model with exactly one success path and no memory. The production model that emerges over the following months is not a longer version of the same line. It is a different shape entirely, with several systems holding different fragments of truth about the same commercial relationship, and with asynchronous messages moving between them.
pricing configuration
→ checkout
→ payment provider
→ subscription state
→ invoice state
→ entitlement state
→ event delivery
→ application database
→ accounting records
→ support tooling
→ reporting
→ reconciliation
[Figure 1: Two side-by-side architecture views. On the left, the "checkout-centric" model drawn as a single left-to-right arrow from customer to active subscription, with four boxes. On the right, the production model drawn as a hub-and-spoke system in which the payment provider and the application database are both stateful nodes, connected by an asynchronous event channel, with additional edges to entitlement service, invoicing, accounting export, support console, and a reconciliation job that reads from both stateful nodes. The intent of the figure is to show that the second diagram is not the first diagram with more steps, but a system with two centers of gravity.]
The distinction that runs through the rest of this article is simple to state and slow to absorb. A payment provider executes financial operations and maintains authoritative records of those operations. A software product decides what those operations mean for a customer's access, obligations, and history. Stripe has built an unusual amount of the second category into its platform, which is one reason the boundary is easy to misjudge. Stripe Billing can hold your product catalog, calculate prorations, retry failed payments on a configurable schedule, apply coupons, compute tax through Stripe Tax, aggregate metered usage, grant billing credits, and emit entitlement summaries that tell your application which features a customer should currently have. That is a large surface. It is still not the same as owning the commercial semantics of your own product.
The Model That Fits on a Whiteboard
Start with the smallest complete example.
Plan: Pro
Price: $99.00 / month
Customer: one
Payment: succeeded
Result: account has Pro access
The state required to support this is almost nothing. A customer record, a reference to the provider's customer and subscription identifiers, and a plan name on the account row. Many products ship exactly this and operate on it for a long time without visible problems, which is appropriate. Building elaborate billing architecture before there are paying customers is a good way to build the wrong abstraction.
What deserves attention is the set of assumptions the example silently makes. Each one is a decision that has been deferred rather than avoided:
- there is one currency
- there is one price for the plan, and it never changes for anyone
- there is one billing interval
- activation is immediate and synchronous with payment
- the payment succeeded on the first attempt
- there are no discounts, coupons, or negotiated rates
- tax is either zero or somebody else's problem
- no period is ever partial
- nothing is ever refunded
- no account carries a credit balance
- there is no contract that differs from the published price
- the number of seats does not change
- payment is never delayed or made by bank transfer
- no invoice is ever disputed
- account ownership never transfers between people or companies
- no operation is ever retried
- no event is ever delivered twice
It is tempting to file these under edge cases. That classification is wrong in a way that has architectural consequences. In a subscription business of any maturity, a meaningful proportion of accounts are in one of these conditions at any given moment. Failed payments are not rare; they are a steady monthly percentage of the base, driven by expiring cards and issuer behavior. Mid-cycle plan changes are not rare; they are the mechanism by which expansion revenue happens. Negotiated pricing is not rare; it is what the sales team does for a living. Duplicate event delivery is not rare; it is a documented property of the delivery mechanism.
A system designed around a happy path with exceptions bolted on tends to encode the happy path in the schema. A system designed around the understanding that plans change, money moves backward, access decouples from payment, and events arrive twice tends to encode transitions and history instead. The second design costs more at the start and much less at every point afterward.
Payment, Subscription, and Invoice Are Different Objects With Different Lifetimes
Several distinct concepts get collapsed into the word "subscription" in ordinary conversation. Keeping them separate in code is one of the highest-leverage decisions in the domain, because each one changes state independently and on its own schedule.
A customer is the party the company has a commercial relationship with. In B2B software this is frequently not the person who typed in the card. The billing customer may be a company; the user who checked out may leave the company; the invoice recipient may be an accounts payable mailbox that has never logged into the product.
A payment method is an instrument attached to that customer. It expires, gets replaced, gets declined, and may not exist at all for a customer who pays by bank transfer against an invoice.
A payment is a single attempt to move a specific amount of money at a specific moment. It succeeds, fails, is pending, is refunded, or is disputed. It is an event with a lifecycle of its own that can extend months past the moment it appeared to complete.
An invoice is a statement of what is owed for a defined period, composed of line items, taxes, discounts, and credits. It has a legal and accounting character that a payment does not. It exists whether or not payment has occurred, which is precisely the point of an invoice.
A subscription is a recurring commitment: a customer has agreed to be billed for something on a cadence until one party ends it. It survives individual payment failures. It has a renewal date, a set of items, and a status that is not the same as "the last payment worked."
A product and a price are catalog concepts. One product may have many prices across currencies, intervals, and generations of the pricing page. A subscription references a price, and that reference is a historical fact about what the customer agreed to, not a pointer to whatever the current pricing page says.
An entitlement is what the customer is allowed to do inside the application. It is downstream of all of the above and is not always a pure function of them.
Confusing these produces a specific family of bugs, and they are recognizable in the wild:
Payment succeeded, but the subscription should not necessarily become active. In flows requiring additional authentication, or where the first payment is captured before an onboarding requirement is satisfied, a successful charge is a necessary and insufficient condition for provisioning.
Payment failed, but access should not necessarily disappear. Nearly every B2B product deliberately continues service through some window of failure, because immediately locking a paying customer out of their own data over an expired card is worse for both parties than carrying the risk for a week.
The subscription is canceled, but access should continue. A customer who cancels a monthly plan on day three has usually paid through day thirty. Canceling the commercial arrangement and revoking product access are different operations with different effective dates.
An invoice exists, but no payment has occurred. This is the normal state of enterprise billing on payment terms, and it lasts for weeks by design.
One customer has several subscriptions. Two products, a base platform plus an add-on, or one workspace per business unit. An account-level boolean cannot represent this.
One commercial agreement covers several products. The customer thinks of it as one contract with one renewal date, and the provider may represent it as several subscription items or several subscriptions.
The following table separates three status dimensions that are commonly stored in a single field. The point is not that these combinations are unusual but that all of them are legitimate and need defined behavior.
| Payment status | Subscription status | Correct entitlement state | Typical situation |
|---|---|---|---|
| Succeeded | Active | Full access | Ordinary renewal |
| Succeeded | Trialing | Trial access, may differ from paid tier | Paid trial, or card captured during trial |
| Failed | Active (in retry window) | Full access, dunning notice shown | First decline of the cycle |
| Failed | Past due | Reduced access or read-only | Retry schedule exhausted for this attempt |
| Failed | Unpaid | Suspended, data retained | Recovery abandoned, account not yet closed |
| Not attempted | Active (invoice open, Net 30) | Full access | Enterprise agreement on payment terms |
| Succeeded | Canceled at period end | Full access until period end | Voluntary churn, prepaid period remaining |
| Refunded | Canceled | Access removed, data retained | Refund granted for unused service |
| Disputed | Active | Policy decision required | Chargeback filed against a live account |
| Succeeded | Active | No access | A defect, and one of the most common in this domain |
The last row is worth pausing on. A customer who has paid and cannot use the product will contact support within minutes, and the incident is expensive in a way that is disproportionate to the size of the bug that caused it.
[Figure 2: An entity relationship diagram showing Customer at the center, with one-to-many edges to Payment Method, Subscription, and Invoice. Subscription connects to Subscription Item, which connects to Price, which connects to Product. Invoice connects to Invoice Line and to Payment. A separate Entitlement box sits to the right, fed by dashed lines from Subscription, from manual grants, and from contract terms, indicating that entitlement is derived from multiple sources rather than owned by any single one. Annotate each edge with its cardinality.]
Entitlements Live Outside the Payment Processor
Collecting money and deciding what a customer is allowed to do are related responsibilities that fail in completely different ways. The first is a financial operation with a clear success signal. The second is an authorization decision made thousands of times a second inside the application, in code paths that have nothing to do with payments.
A typical catalog looks like this:
Starter 5 users, 10 projects, 1 GB storage
Pro 25 users, unlimited projects, API access, 100 GB storage
Enterprise negotiated seats, SSO, audit logs, custom retention, premium support
The tempting implementation is a plan check at each decision point:
if account.plan == "pro" or account.plan == "enterprise":
allow_api_access()
This works until the first exception, and the first exception always arrives. A customer on Starter negotiates API access as part of an annual deal. A legacy plan from two pricing generations ago included unlimited projects and the current Pro tier does not. A support agent grants temporary Enterprise features during an evaluation. A partner account gets SSO without paying for the Enterprise tier. Each exception, handled locally, adds another clause to the conditional, and those conditionals are scattered across the codebase in whatever files happened to need them.
The structural problem is that commercial configuration has been embedded in application control flow. Changing a price becomes a code change. Grandfathering a cohort becomes a migration. Answering the question "which customers currently have audit logs" requires reading source code rather than querying data.
A durable entitlement model separates three things:
The capability, which is a stable identifier the application checks: api_access, sso, audit_log_export, projects.max, seats.included. Application code asks whether the account has the capability and, for quantitative capabilities, what the limit is. It never asks what plan the account is on.
The grant, which is a record of why the account has the capability. A grant has a source (subscription item, contract term, manual concession, trial, promotion), an effective window, and an actor if a human created it. Multiple grants for the same capability can coexist, and resolution rules decide which wins. The usual rule is that the most permissive active grant applies, but a product may deliberately choose otherwise for quotas.
The resolved entitlement, which is what the application reads. It is computed from active grants and cached with an explicit invalidation path, because it is read constantly and changes rarely.
Stripe has built support for the first two of these into Billing. Features can be defined in the product catalog, attached to products, and when a customer subscribes, Stripe creates active entitlements for the associated features and emits an event when the summary changes. Stripe's entitlements documentation describes this as mapping the features of your internal service to Stripe products, after which Stripe signals when to provision or de-provision access based on the customer's subscription status. That is a real reduction in glue code, and for products whose entitlements are a clean function of the purchased plan it may be most of what is needed.
What remains with the application is everything that is not a function of the purchased plan. Manual grants issued by support. Grandfathered capabilities that no current product carries. Contract terms negotiated per customer. Promotional access with an expiry. Restricted states during dunning, where the account still has an active subscription but the product deliberately reduces what it permits. Quota accounting, where the entitlement is a ceiling and the application must track consumption against it. And the resolution logic that decides what happens when a customer has both a subscription grant of five seats and a contract addendum granting three more.
There is also a reliability argument for keeping a local entitlement record regardless of where grants originate. Authorization decisions happen on the request path. Making them dependent on a synchronous call to an external service couples product availability to that service's availability and latency, and puts a network hop inside a permission check. The provider is an authoritative source of billing-derived grants; the application still needs its own durable, queryable copy that it can read without leaving the process.
The design signal to watch for is directional leakage. If adding a new pricing tier requires touching authorization code, the boundary is in the wrong place. If a support agent granting a temporary feature requires an engineer, the boundary is in the wrong place. If the answer to "what does this customer have access to and why" cannot be produced from data, the boundary does not exist yet.
[Internal link opportunity: software architecture testing]
Proration Turns Time Into Money
Proration is the mechanism that converts a mid-period change into a monetary amount. It is arithmetically simple and commercially contentious, which is an unusual combination and the reason it generates so many support tickets.
Take a customer paying $120 per month who upgrades to a $200 plan exactly halfway through the period.
Old plan: $120 / month, 15 of 30 days remaining
New plan: $200 / month, 15 of 30 days to be served
Unused portion of old plan: $120 × 15/30 = $60 credit
Remaining portion of new plan: $200 × 15/30 = $100 charge
Net adjustment: $40
The provider can compute this. Stripe Billing generates proration line items automatically when a subscription item changes mid-cycle, with the behavior controlled by a parameter that lets you create prorations, suppress them entirely, or create them and invoice immediately. The calculation is not where teams get into trouble.
The trouble is that the calculation has several defensible answers and the company has to pick one. Should the $40 be charged now, or added to the next invoice? If it is charged now, does the billing period reset, so the customer's renewal date moves to today? If the renewal date moves, has the customer effectively lost the fifteen days they already paid for, or has that been credited? If the customer downgrades the following week, do they receive a credit for the unused Business time, and can that credit be withdrawn as cash?
None of these has a universally correct answer. They are pricing policy decisions with implementation consequences, and they must be written down before they can be built or tested.
The complications compound quickly:
Interval changes. Monthly to annual is not a proration of the same shape as Pro to Business. An annual plan is a different commitment length, usually at a discounted effective rate. Does the annual term start today or at the next monthly renewal? If today, the unused monthly time becomes a credit against a much larger charge. If at the next renewal, the customer waits for the discount they just agreed to buy.
Annual to monthly. The customer prepaid twelve months and wants to move to monthly after five. The remaining seven months represent real money. Refund, credit, or neither is a policy question with revenue recognition implications, and different answers are appropriate for self-service and negotiated contracts.
Quantity changes. Adding eight seats on day nine of a thirty-day period produces a partial charge. Removing eight seats on day nine produces a partial credit, and the credit may exceed the value of the next invoice. Some products deliberately do not credit seat reductions mid-period, and that is a legitimate policy as long as it is stated.
Discounts and proration together. If the customer has a 20% coupon on the old plan, does it apply to the prorated charge for the new plan? Does it apply to the credit? Coupons scoped to a specific price behave differently from coupons scoped to the customer.
Tax on prorated amounts. A proration credit reduces a taxable amount, which changes the tax line, which may change the total in a way the customer notices and questions.
Negative invoice totals. A downgrade credit larger than the next charge produces an invoice that owes the customer money. That does not become a payment; it becomes a balance carried forward. Which means the system now has a customer credit balance, whether or not anyone designed one.
| Change | Common policy options | Money effect | Access effect | State the application must record |
|---|---|---|---|---|
| Upgrade (same interval) | Immediate with proration invoiced now; immediate with proration on next invoice; effective next cycle | Net charge, or credit if new plan is cheaper per day | Usually immediate | New entitlement effective timestamp, proration reference |
| Downgrade (same interval) | Effective at period end; immediate with credit; immediate without credit | Credit or none | Deferred or immediate | Scheduled change with effective date, prior entitlement retained until then |
| Monthly to annual | Start annual term now; start at next renewal | Large charge, offset by unused monthly credit | Immediate | New term start and end, renewal date migration |
| Annual to monthly | Effective at annual term end; immediate with credit; immediate with refund | Credit, refund, or forfeiture | Usually deferred | Scheduled downgrade, remaining term value |
| Seat increase | Immediate, prorated | Charge now or at next invoice | Immediate | Seat allowance change, effective timestamp |
| Seat decrease | Immediate with credit; immediate without credit; effective next cycle | Credit or none | Immediate or deferred | Committed minimum, if any |
| Add-on purchase | Immediate, prorated to align with parent period | Partial charge | Immediate | Add-on entitlement, period alignment |
| Plan change during dunning | Blocked; allowed with immediate payment; allowed with deferred effect | Depends | Depends | Interaction with retry schedule |
Two properties of this table matter more than its contents. The first is that a deferred change is itself a piece of durable state: a scheduled mutation that must survive process restarts, be visible to support, be cancelable, and be applied exactly once when its time arrives. The second is that immediate access changes and immediate money changes do not have to move together, and in most well-designed products they do not.
[Figure 3: A timeline diagram of a mid-cycle upgrade. A horizontal bar represents a 30-day billing period, shaded for days 1 to 15 as "paid at $120/mo" and days 16 to 30 as "served at $200/mo". Below the bar, two vertical callouts show the $60 unused credit and the $100 new-plan charge, converging on a $40 net line item. To the right, a branch shows two alternative renewal-date outcomes: period preserved (renews on original date) versus period reset (renews 30 days from the change), with the resulting invoice for each.]
The general point extends past proration. A payment provider gives you a correct calculation from a configuration. It cannot tell you which configuration matches the promise your pricing page makes, and the gap between arithmetic correctness and policy correctness is where customer disputes originate.
Failed Payments Are a State Machine, Not a Retry
Card payments fail routinely. Cards expire, balances run out, issuers decline transactions for risk reasons, and authentication challenges go unanswered. Treating this as a background retry loop misses that failed payment recovery is a multi-week process involving the product, the customer, and several internal teams.
Stripe Billing handles a substantial part of the mechanics. Retry schedules are configurable, including Smart Retries that time attempts based on signals about when a given payment is more likely to succeed. Dunning emails, hosted payment update pages, and end-of-recovery behavior can all be configured. Configuring that correctly is real work with real revenue impact, and it is not the same work as deciding what the product does to the account meanwhile.
The failure reason matters, because the recovery paths genuinely differ:
Soft declines such as insufficient funds or a temporary issuer decline may succeed on a later attempt with no customer action. Retrying is appropriate.
Hard declines such as a card reported lost or an account closed will not succeed on retry. Continuing to retry wastes attempts and can attract issuer scrutiny. The correct action is to ask the customer for a new payment method immediately.
Expired cards are predictable in advance. Network-level card updater services replace many of them silently, but not all, and a product can notify before expiry rather than after failure.
Authentication requirements are a distinct category. The payment did not fail; it requires the cardholder to complete a challenge. The subscription sits in a state that requires customer action, and the correct product response is a prompt, not a dunning email about a failed payment.
Transient provider or network failures are not customer-facing failures at all. They require retry with backoff and should not trigger customer notifications.
The state machine that sits on top of this is where product policy lives:
active
→ payment_failed (attempt 1)
→ notify customer, retain full access
→ payment_failed (attempt n)
→ grace_period: full access, escalating notification
→ grace expired
→ restricted: read-only, exports allowed, writes blocked
→ restriction window expired
→ suspended: login allowed, product blocked, data retained
→ retention window expired
→ canceled: subscription ended, data scheduled for deletion
Each arrow is a decision with a duration attached, and the durations are business decisions, not technical ones.
| Dimension | Consumer subscription product | Business-critical B2B platform |
|---|---|---|
| Access during retry window | Often continues briefly, then stops | Continues in full, frequently for weeks |
| Notification channel | In-app and email to the account holder | Email to billing contact and admins, sometimes account manager outreach |
| Restricted state | Rarely used; access is binary | Common; read-only and export-only states are typical |
| Time to suspension | Days | Weeks, sometimes governed by contract |
| Data retention after suspension | Short | Long, often contractual |
| Cost of wrongly removing access | Churn of one subscription | Production outage for the customer's own business, plus contractual exposure |
| Cost of wrongly retaining access | Small revenue leakage | Larger absolute amounts, but usually recoverable |
The asymmetry in the last two rows explains why B2B products tolerate longer unpaid windows. Wrongly cutting off a platform a customer's own operations depend on creates an incident on their side, and the commercial damage exceeds the disputed invoice by a wide margin.
Recovery is also organizationally distributed in a way that pure engineering ownership does not survive. Support fields the confused customer. Customer success reaches out before an important account is suspended. Finance wants to know how much of the receivable is at risk and when to stop pursuing it. Product owns the restricted-state experience. The engineering artifact that ties these together is the account's recovery state, which needs to be a first-class, queryable, observable thing rather than a derived guess from the last failed charge.
[Internal link opportunity: release reliability]
Cancellation Has More Than One Meaning
The word covers a family of operations that share almost nothing except a button label.
- Cancel at period end: the commercial relationship ends on the renewal date; access continues until then.
- Cancel immediately: the subscription ends now; whether access ends now is a separate decision.
- Cancel immediately with a refund of the unused period: money moves backward, and access almost always ends now.
- Cancel renewal only: no further invoices, but the current term runs to completion. Common in annual contracts.
- Pause: billing stops, access is reduced or frozen, and the arrangement is expected to resume. Stripe supports paused collection on subscriptions, which is not the same as canceling and re-subscribing, because it preserves the subscription and its history.
- Cancel one item: one add-on or one seat bundle ends while the parent subscription continues.
- Suspend for non-payment: an involuntary state that looks like cancellation to the customer but is a recovery state commercially.
- Terminate for cause: an administrative action with different notification, data, and refund handling.
These differ on four independent axes, and collapsing them into one field is what eventually forces a rewrite:
Commercial status is whether the customer owes anything and whether renewal will occur. Payment status is whether outstanding invoices exist and whether they will be pursued. Entitlement status is what the product currently permits. Data status is what is retained, for how long, and whether it can be restored.
A single is_subscription_active boolean cannot express "canceled, no further billing, full access for eleven more days, data retained for ninety days after that, one open invoice still being collected." That sentence describes an ordinary Tuesday for a customer support team, and it requires at least four fields plus dates to represent.
Reactivation deserves explicit design too. A customer who cancels and returns within the prepaid window should resume, not start a new subscription with a new renewal date and a fresh charge. A customer returning six months later is a new subscription but an existing customer, with existing invoice history, an existing tax profile, and possibly a legacy price they will expect to still have.
Refunds Move Backward Through More Than One System
A refund is not the inverse of a payment. It is a new financial event that references an old one, and it propagates into systems that a payment never touched.
The forms it takes are varied enough to matter:
- Full refund of the most recent invoice
- Partial refund of a specific amount
- Refund of one line item on a multi-line invoice, leaving the rest intact
- Refund issued after cancellation, referencing a period already served
- Refund after partial usage, where the fair amount is a judgment call
- Refund where tax was collected, which reverses a tax liability that may already have been remitted
- Service credit issued instead of a refund, which keeps the money and creates an obligation
The last option is frequently the right one commercially, and it is the one that most often has no implementation, so support ends up issuing cash refunds because credits are not supported by the tooling.
The propagation is where drift starts. A refund affects the payment record at the provider, the invoice's paid amount, the internal ledger and revenue reports, subscription and entitlement state if the refund accompanies a cancellation, tax records, support history so the next agent knows what happened, analytics and cohort revenue, and in some organizations sales commission calculations.
A refund performed directly in the provider's dashboard, which is a fast and reasonable thing for a support agent to do under time pressure, moves the money and updates the provider's records. If the application learns about it only through an event handler that was written to handle subscription lifecycle and not refunds, or if it learns and does nothing because no downstream action was defined, the internal reporting continues to count the original payment. Nobody notices until a monthly close, at which point the discrepancy is a research project rather than an alert.
The architectural response is not to prohibit dashboard refunds. It is to treat every provider-originated financial event as a legitimate input to the application's own ledger, and to build the reconciliation that detects the case where it was not applied. That principle recurs later in this article, because it applies to every manual action, not just refunds.
Credits Behave Like a Ledger
Account credits appear innocuous. A customer had an outage, a support agent offers $50 off the next invoice, and the request seems like a one-line feature. It is not, because a credit is a persistent obligation with rules that must be answered consistently for the rest of the product's life.
Credits arise from many sources with different semantics:
- Service level concessions after an incident
- Promotional credits given at signup or in a campaign
- Customer success concessions to resolve a dispute
- Overpayment on an invoice, which creates a balance not a gift
- Proration credits from downgrades
- Contractual credits negotiated as part of a renewal
- Prepaid usage credits purchased in advance, which are a pricing model rather than a concession
Stripe supports several of these through different mechanisms: customer balance transactions for general account credit, credit notes for adjusting a finalized invoice, and billing credit grants that apply to metered usage and can carry expiry dates and priorities. Choosing which mechanism represents which business concept is an architectural decision, not a lookup.
The rules that must be defined, and that the application will be asked about eventually:
Expiry. Do promotional credits expire? Do concession credits? A credit with no expiry is a liability with no end date.
Ordering. Does the credit apply before or after a percentage discount? Before or after tax? A $10 credit against a $100 invoice with a 20% discount and 20% tax produces different totals depending on where it lands, and both orderings can be defended.
Scope. Does the credit apply to any charge, only to subscription charges, only to a specific product, only to usage overages? A credit granted to offset an outage on one product that silently pays for a different product is a commercial surprise.
Refundability. Can a credit be converted to cash? Most companies say no. That answer needs to exist in the system, not only in an agent's head.
Sign. Can the balance go negative, representing amounts owed rather than owed to? Stripe's customer balance can hold both directions, and treating one as an error condition when the other is normal creates confusing behavior.
Portability. If a customer has two subscriptions, does a credit apply to whichever invoice arrives first? If they cancel one, does the credit follow?
Visibility. Does the customer see the balance? On the invoice, in the billing portal, both, or neither? A credit the customer cannot see generates support contacts asking where their promised discount went.
Every one of these is small. Collectively they define whether the credit system is coherent or a set of accumulated special cases, and the difference shows up as arithmetic the customer disputes.
Discounts and the Order of Operations
Discount mechanics multiply because sales and marketing need them to. The catalog of forms is predictable:
percentage discounts, fixed-amount discounts, first-period-only discounts, multi-month promotions, coupon codes redeemed at checkout, customer-specific negotiated rates, partner and reseller pricing, lifetime discounts granted during an early-access period, and legacy pricing that functions as a permanent implicit discount.
The mechanics are provider territory. Stripe supports coupons and promotion codes with duration rules, and negotiated rates can be modeled as distinct prices rather than discounts, which is often cleaner because it produces an honest invoice line.
The part that stays with the application is calculation ordering, and it must be explicit. Consider a $100 monthly subscription with a 20% discount, a $10 account credit, and a 20% tax rate.
Ordering A: discount → tax → credit
100 − 20 = 80
80 + 16 tax = 96
96 − 10 credit = 86
Ordering B: discount → credit → tax
100 − 20 = 80
80 − 10 = 70
70 + 14 tax = 84
Ordering C: credit → discount → tax
100 − 10 = 90
90 − 18 = 72
72 + 14.40 tax = 86.40
Three orderings, three totals, a $2.40 spread on a $100 invoice. At scale that difference is material, and more importantly it is visible: a customer who computes their expected charge and gets a different number contacts support.
This article does not tell you which ordering is correct, because the correct ordering depends on jurisdiction, on what a credit legally represents in your accounting treatment, and on what your invoices claim. What can be said without qualification is that the ordering must be an explicit, documented decision, implemented in one place rather than reconstructed at each call site, and covered by tests that assert exact totals rather than approximate ones.
Proration interacts with all of it. A mid-cycle upgrade on a discounted subscription raises the question of whether the discount applies to the proration line, and whether it applies to the credit line as well. The answer changes the customer's bill by more than they expect.
Trials Are Entitlements With an Expiry, and an Ending
Trial models differ in ways that change the state machine:
Trial without a payment method. The lowest friction option and the one with the most complicated ending, because conversion requires a separate act by the customer. The subscription may not exist at the provider at all during the trial, which means the trial is entirely the application's state to manage.
Trial with a payment method captured up front. The subscription exists in a trialing status with a known conversion date. Conversion is automatic and the failure mode moves to the first charge.
Paid trial. A nominal charge that establishes a valid payment method and filters low-intent signups. It is a real payment with real refund implications.
Extended and manually granted trials. Sales extends a trial by two weeks. This is a routine request and one of the most common reasons engineers get pulled into commercial operations, because the extension has to be applied somewhere.
Early conversion. A customer on a fourteen-day trial who converts on day four. Does billing start immediately, or on day fourteen as promised? Charging early for time that was advertised as free is a support ticket; deferring the charge means the subscription and the entitlement have different effective dates.
Conversion with a failed first payment. The trial ended, the charge failed, and the account is now in a state that resembles dunning but has never been a paying account. Whether it gets the same grace period as an established customer is a policy decision, and the answer is often no.
What happens at trial end is a product design question with data consequences. If the trial created twelve projects, invited nine users, generated API credentials, and connected two integrations, and the paid tier the customer declines to buy allows five users and three projects, the application has to resolve an over-quota state. The options are to block writes while retaining data, to force the customer to choose what to keep, to downgrade to a free tier with reduced limits, or to retain everything and let the customer stay over quota indefinitely. All are used in practice. What is not viable is having no defined behavior, because the resulting failure surfaces as data loss or as a customer permanently over their paid limits.
API credentials issued during a trial deserve their own decision. Revoking them breaks the customer's integration, which is a strong conversion incentive and a strong source of resentment. Leaving them active means trial access continues after the trial. Whichever is chosen, integrations built during a trial are exactly the artifacts most likely to be forgotten by the entitlement layer, because they authenticate outside the normal session path.
An Invoice Is a Record, Not a Receipt
An invoice is often treated as the PDF that gets emailed after a successful charge. In a subscription business it is a stateful object with a lifecycle that can span weeks and a legal character that outlives the payment.
The lifecycle in Stripe Billing runs roughly: an invoice is created as a draft and can be modified; it is finalized, at which point its line items and numbering are fixed and it becomes open; it becomes paid when payment succeeds, void if it is canceled after finalization, or uncollectible if the business gives up on collecting it. Draft invoices can be deleted; finalized ones cannot, which is the point of finalization.
That transition from mutable to immutable is the conceptually important one. Before finalization, the invoice is a working calculation. After finalization, it is a record that has been presented to a customer and may already be in their accounts payable system. Changing it afterward is not an edit; it is a credit note, a void and reissue, or a separate adjustment. Applications that model invoices as ordinary mutable rows tend to discover this when a customer's accountant asks why invoice INV-2041 now shows a different amount than the copy they filed.
The contents carry requirements that go well past the amount:
Numbering. Sequential, gapless numbering is a requirement in some jurisdictions. Once a number is issued it belongs to that document.
Line items. A useful invoice shows what was charged and why: base subscription, seat changes with quantities and periods, proration adjustments with their effective dates, usage lines with quantities, discounts, credits applied, and tax. A single line reading "Subscription — $1,847.32" generates a support contact from anyone whose job includes approving invoices.
Legal entity and address. Which of the vendor's entities issued this, and to which of the customer's entities. In multinational customers these differ from the entity whose users log in.
Tax identifiers. VAT numbers, GST registration, and equivalent identifiers, which affect whether tax is charged and how the document must be formatted.
Purchase order references and payment terms. An enterprise invoice without the customer's PO number frequently cannot be paid, regardless of its correctness.
Billing contacts. The recipient of the invoice is often not a product user, and the routing of billing email is a distinct piece of customer data.
The gap between a $29 self-service subscription and a $100,000 annual agreement is not primarily a difference in amount. It is a difference in how many parties must agree that the document is correct before money moves. The self-service invoice is generated, charged, and emailed within a second. The enterprise invoice is reviewed by the customer's procurement function, matched against a purchase order, routed for approval, and paid by bank transfer four weeks later, during which time the invoice is open, the subscription is active, and the account is fully entitled.
Enterprise Billing Breaks the Clean Model
Sales-assisted revenue introduces requirements that self-service billing has no reason to support, and most SaaS companies end up operating two commercial systems that must resolve to the same product behavior.
The list of enterprise-specific requirements is stable across the industry:
- Annual or multi-year terms with a negotiated total that does not correspond to any published price
- Custom rate cards, sometimes per business unit
- Purchase order numbers required on the invoice
- Net 30, Net 45, or Net 60 payment terms, with the subscription fully active throughout
- Payment by bank transfer or check rather than card
- Multiple subsidiaries under a parent agreement, with consolidated or split invoicing
- A named invoice contact distinct from any product user
- Tax exemption certificates
- Minimum commitments with true-up at term end
- Prepaid usage pools with defined overage rates
- Contractual service credits triggered by availability commitments
- Renewal terms negotiated months before the renewal date
- Custom entitlements that no packaged tier includes
| Dimension | Self-service | Sales-assisted |
|---|---|---|
| Price source | Public catalog | Negotiated per agreement |
| Term | Rolling monthly or annual | Fixed term with defined start and end |
| Payment timing | Charged at period start | Invoiced, paid on terms |
| Payment method | Card or wallet on file | Bank transfer, sometimes card |
| Failure handling | Automated retries and dunning | Collections process, account manager involvement |
| Plan changes | Self-service, immediate | Amendment, often with legal review |
| Entitlements | Derived from purchased plan | Derived from contract terms |
| Cancellation | Customer-initiated in product | Non-renewal notice within a contractual window |
| Source of commercial truth | Provider subscription | Signed agreement, with the provider reflecting it |
| Typical failure mode | Payment declined | Invoice unpaid because a PO number is missing |
The architecture question is where these two paths converge. Converging at the payment provider means representing negotiated agreements as subscriptions with custom prices, which works and keeps invoicing unified, but pushes contract semantics into a system designed around catalog pricing. Converging at the entitlement layer means both paths produce grants in the same internal model, and the product does not know or care which path produced them. The second approach tends to hold up better as the enterprise motion grows, because contract terms rarely map cleanly to catalog objects and because the product should not contain conditionals distinguishing enterprise customers from self-service ones.
What consistently causes pain is having no convergence at all: enterprise entitlements applied by an engineer running an update statement, invisible to the same tooling that manages self-service accounts, and absent from any reconciliation. The customer with the largest contract then becomes the customer whose configuration is least understood by the system.
Usage-Based Billing Is a Measurement Problem First
Usage pricing looks like the simplest possible model. Two cents per API call. Multiply and invoice.
The multiplication is the easy part, and Stripe Billing handles most of it: meters aggregate reported events over a billing period, meter events carry a customer reference and a value, and metered prices attached to a meter produce invoice lines. Stripe also supports deduplication of meter events through an identifier and adjustments to correct previously reported usage. Prepaid usage can be modeled with billing credit grants that burn down against metered charges.
The hard part is upstream of all of that, and it is a product definition problem.
What counts as a billable API call?
- A request that returns 200 clearly counts.
- A request that returns 400 because the customer sent malformed input: the work was done, the validation ran, and the customer learned something. Many platforms bill it. Many do not.
- A request that returns 500 because of a defect on the vendor's side should not be billed, which means the metering pipeline must be able to distinguish the vendor's failures from the customer's.
- A request that returns 429 because the customer exceeded a rate limit performed no work.
- A client-side retry after a timeout, where the original request may or may not have executed.
- A response served from cache, which cost the vendor almost nothing but delivered the same value.
- An internal service-to-service call made by the vendor's own systems using a customer's credentials.
- Traffic from a compromised credential, which the customer will dispute and will usually win.
- Requests made by the vendor's monitoring and health checks.
Each answer is a policy that must be encoded at the point of measurement, documented publicly if customers are going to plan around it, and tested. The distinction between "we bill successful requests" and "we bill all requests" is a pricing decision that looks like an implementation detail.
Then the pipeline itself introduces its own class of problems:
Deduplication. At-least-once delivery in the emitting service means the same usage event can be reported twice. Every event needs a stable identifier derived from the work performed, not generated at emission time, so that a retried emission produces the same identifier.
Late events. An event generated at 23:58 on the last day of the period arrives at 00:03 the next day, after the period has closed. Whether it belongs to the closed period or the new one is a decision, and the decision has to be consistent across every meter or customers will find the inconsistency.
Events arriving after invoice finalization. Once the invoice is finalized, the amount cannot change. Late usage becomes either a line on the next invoice, a credit note plus a reissue, or a write-off. The threshold at which each applies should be defined rather than decided per incident.
Corrections. Usage reported in error must be reversible. A correction mechanism that only adds is not a correction mechanism.
Clock and time zone semantics. The timestamp that matters is the one at which the usage occurred, in the time zone in which the billing period is defined. If the emitting service records local time and the billing period is defined in UTC, a portion of every period is misattributed.
Aggregation semantics. Sum, maximum, last value during the period, and unique count produce different bills from the same events. Peak seat count and average seat count are both defensible; they are not the same number.
Tiers and volume pricing. Graduated tiers charge each unit at the rate of the tier it falls into; volume tiers charge every unit at the rate determined by the total. The same usage and the same published rates produce different totals, and customers do read the difference carefully.
Caps and minimums. A committed minimum means the invoice does not fall below a floor. A cap means it does not rise above a ceiling, which requires the system to stop counting or to keep counting and stop charging, and those differ when the customer asks how much they used.
[Figure 4: A usage metering pipeline drawn left to right. Application services emit usage events with a stable idempotency key into an ingestion buffer. The buffer feeds a deduplication stage keyed on that identifier, then an enrichment stage that attaches customer, meter, and period based on event timestamp. Aggregation writes period totals to an internal usage ledger, which is the system of record for what the customer consumed. A separate reporting path forwards events to the billing provider's meter. At the bottom, a reconciliation arrow compares internal ledger totals against provider meter summaries per period, with a discrepancy queue. Annotate the point of invoice finalization as a cut-off line, with a "late events" branch showing the correction path.]
The internal usage ledger in that figure matters. Reporting usage exclusively to the provider makes the provider's aggregate the only record of what happened, which means customer disputes cannot be investigated at the event level, corrections cannot be reasoned about, and there is nothing to reconcile against. Keeping a queryable internal record of the raw events, at least for the retention period during which disputes occur, is what makes the pricing model defensible when a customer asks why their bill tripled.
[Internal link opportunity: API testing]
Seats Require a Definition Before They Require a Price
Per-seat pricing raises a question that sounds administrative and is actually the core of the pricing model: when does a user become billable?
The candidate answers are all in production somewhere:
| Definition | Billable when | Behavior it encourages | Problem it creates |
|---|---|---|---|
| Invited user | Invitation is sent | Simple to implement, immediate revenue | Customers billed for invitations that were never accepted |
| Accepted user | Invitation is accepted | Aligns billing with real users | Delay between provisioning and revenue; invitation spam is free |
| Active user | User authenticated within the period | Customer-friendly, matches perceived value | Bill varies unpredictably; requires activity tracking as billing input |
| Provisioned seat | Admin allocates a seat, occupied or not | Predictable bill, matches enterprise procurement | Customers pay for empty seats and will notice |
| Named license | Seat assigned to a specific identity | Clean audit trail | Reassignment rules become complex |
Adjacent categories need explicit treatment. Deactivated users who retain access to their historical data. Pending invitations that have been outstanding for six weeks. Guest or external collaborators with restricted permissions, common in collaboration products, where the entire commercial argument is that they are free. Service accounts and API-only identities that consume no interface but do consume the platform. Temporary contractors provisioned for a month. Users belonging to two workspaces under the same parent account, who may or may not be one billable seat.
Seat changes interact with proration in ways that are frequently under-specified. An admin adds twelve seats on day nine, removes five on day fourteen, and adds three on day twenty-two. If every change triggers an immediate prorated invoice, the customer receives three invoices in a month and their accounts payable function is unhappy. If changes accumulate and are invoiced at the next cycle, the system must track the value of each change against its effective window. If the model bills the peak seat count during the period, none of the intermediate invoicing applies but the customer needs to be able to see when the peak occurred and why.
The system-level consequence is that seat count is not one number. There is the number the billing provider is charging for, the number the application permits, and the number currently occupied. Keeping the first two synchronized is the integration's job. Preventing the third from exceeding the second is the product's job. When these three diverge, the visible symptoms are either a customer who cannot add a user they are paying for or a customer using more seats than they are being charged for, and the second is discovered much later than the first.
Webhooks Make Billing Asynchronous
Subscription billing is asynchronous by nature, not by implementation choice. A renewal happens on the provider's schedule, not in response to a request from your application. A card is retried three days after a failure. A bank transfer settles on the customer's timetable. A dispute is filed by an issuer sixty days after the charge. None of these can be modeled as a synchronous call, which is why event delivery is a load-bearing part of the architecture rather than a convenience.
It is worth being precise about the chain of things that must succeed, because collapsing them is the source of most webhook defects:
- The business event occurs at the provider (an invoice is paid).
- An event object is created describing it.
- An HTTP request is dispatched to the configured endpoint.
- The request reaches the application and passes signature verification.
- The application accepts responsibility for the event and acknowledges it.
- The application processes the event.
- The resulting database mutation commits.
- The downstream business process reaches its intended state (entitlement updated, email sent, ledger entry written).
Steps 3 through 8 can each fail independently, and the failure of step 8 is invisible to the provider because the provider's only feedback channel is the HTTP status code returned at step 5.
The documented delivery semantics matter here and are frequently misremembered. Stripe attempts delivery for up to three days with exponential backoff when an endpoint does not return a success status. Stripe explicitly does not guarantee that events arrive in the order they were generated, and its documentation recommends that endpoints not depend on ordering and instead retrieve current objects from the API when necessary. Endpoints can receive the same event more than once. None of this indicates unreliability; it is the standard behavior of an at-least-once delivery system operating across the public internet, and it describes what any comparable platform would do.
The consequence for handler design is that the handler must be safe to run repeatedly, in unexpected order, and possibly long after the event was generated.
Acknowledging Is Not Processing
A common structure puts business logic directly in the request handler: verify the signature, update the subscription, adjust entitlements, write ledger entries, send a receipt, return 200. This works in development and degrades in production for two reasons. The handler's latency becomes the delivery timeout budget, so a slow downstream dependency turns into delivery failures. And a partial failure halfway through leaves the application in an intermediate state while the provider retries the whole event, running the successful parts again.
The more durable structure separates receipt from processing:
POST /webhooks/billing
verify signature against raw request body
parse minimal envelope: event id, type, created timestamp
INSERT INTO billing_events (provider_event_id, type, payload, received_at, status='received')
ON CONFLICT (provider_event_id) DO NOTHING
return 200
worker:
claim next unprocessed billing_event
process within a transaction
mark processed, or record failure and schedule retry
after N failures, move to dead-letter for human review
The endpoint's only responsibility is durable receipt. Once the event row is committed, the acknowledgment is honest: the application has the event and will not lose it. Processing happens in a worker where retries, backoff, and dead-lettering are under the application's control rather than the provider's.
This also creates the artifact that makes billing incidents investigable. A persisted event log with received, processed, and failed timestamps answers questions that are otherwise unanswerable: whether an event arrived, when, how many times, what the payload said, which handler version processed it, and what it changed.
Signature verification deserves one specific warning because the failure is subtle. Verification must run against the exact raw request body. Middleware that parses JSON and re-serializes it, or that modifies encoding, produces a body that no longer matches the signature, and the resulting failures look like an attack rather than a framework configuration issue.
[Figure 5: A webhook processing pipeline. Left: provider emits event. Center-left: HTTPS endpoint performing signature verification against the raw body, then an atomic insert into an events table with a uniqueness constraint on provider event id, then an immediate 200 response drawn as a short return arrow. Center-right: a worker pool claiming rows, with a transaction boundary drawn around "apply business effect + mark processed". Right: three terminal paths, success, retry with backoff, and dead-letter queue with an alert. Below the pipeline, a separate reconciliation arrow labeled "periodic sweep: provider events list vs local events table" showing how missed events are detected independently of delivery.]
| Failure mode | What the provider sees | What the customer experiences | Detection | Mitigation |
|---|---|---|---|---|
| Endpoint down during delivery | Non-2xx, retries scheduled | Nothing, if recovery is within the window | Delivery attempt logs, endpoint error rate | Retries plus a catch-up sweep against the events API |
| Handler times out mid-processing | Delivery marked failed, retried | Partial state, possibly duplicated on retry | Latency alerts, duplicate effect detection | Acknowledge on receipt, process asynchronously |
| Duplicate delivery | Success both times | Double provisioning or double credit | Effect-level idempotency violations | Uniqueness constraint on event id and on effect keys |
| Out-of-order delivery | Success, in the order sent | Stale state overwriting fresh state | Version or timestamp regression checks | Compare object version, or fetch current state from API |
| Signature verification failure | Non-2xx, retries | No state change at all | Verification failure counter | Verify against raw body before any parsing |
| Handler crashes after external side effect | Success or failure depending on placement | Duplicate emails, duplicate provisioning calls | Side-effect audit log | Make external calls idempotent, or record intent before acting |
| Event processed but effect failed silently | Success | Payment taken, access not granted | Reconciliation of payment state against entitlement state | Treat business outcome, not HTTP status, as the success condition |
| Event never generated for a manual action | No event to see | Application unaware of a dashboard change | Periodic state comparison | Reconciliation, not event handling |
The last row is the one that reconciliation exists for. Not every change to the commercial relationship is announced.
Idempotency Applies to Money and to Provisioning Separately
Duplication happens in billing for mundane reasons. A customer double-clicks a subscribe button. A mobile client times out and retries a request that actually succeeded. A load balancer replays a request. A webhook is delivered twice. A worker is killed after committing an external call but before recording that it did. None of these are unusual, and all of them can produce a duplicate.
Two kinds of duplication matter and they need different defenses.
Duplicate money movement is charging the customer twice, refunding twice, or applying a credit twice. This is prevented at the boundary with the provider. Stripe supports idempotency keys on API requests: a request carrying a key that has already been seen returns the original response rather than performing the operation again. The key must be derived from the intent, not generated fresh on each attempt, otherwise the retry carries a new key and performs a new operation. A useful pattern is to generate the key when the user's intent is first recorded and store it alongside that intent, so every retry of the same intent reuses it.
intent = record_intent(account_id, action="upgrade", target_price="business_annual")
# intent.id is stable across retries; use it as the idempotency key
provider.update_subscription(sub_id, price=..., idempotency_key=intent.id)
Duplicate provisioning is applying the same business effect twice inside the application: granting 100 credits twice for a single purchase, adding the same seat allowance twice, sending the same invoice email twice, or writing two ledger entries for one payment. This cannot be prevented by the provider, because the provider does not know what the application did. It is prevented by making effects keyed and conditional:
apply_credit_grant(account_id, amount, source_ref):
INSERT INTO credit_ledger (account_id, amount, source_ref)
ON CONFLICT (source_ref) DO NOTHING
The source_ref is the event or invoice identifier that caused the grant. Running the handler five times produces one ledger row. Note that this is stronger than checking whether an event was processed before acting, because a crash between "check" and "act" reintroduces the duplicate. The uniqueness constraint is enforced by the database at the moment of the write.
The two are genuinely independent. A perfectly idempotent charge with a non-idempotent provisioning handler gives the customer one charge and two grants. A non-idempotent charge with a careful handler gives them two charges and one grant. Both are incidents; they are handled in different layers.
External side effects, particularly email, need the same treatment. A dunning notice sent four times because a worker retried is a support contact and an erosion of trust in every subsequent notice. Recording notification sends against the event that triggered them, with a uniqueness constraint, costs one table and eliminates the class.
Ordering Cannot Be Assumed, So State Must Be Verified
Consider the events generated by a mid-cycle upgrade with immediate invoicing:
customer.subscription.updated
invoice.created
invoice.finalized
invoice.payment_succeeded
customer.subscription.updated (again, reflecting the settled state)
The intuitive processing order matches the causal order. Delivery does not guarantee it, and parallel workers make reordering more likely rather than less, because two events dispatched close together will be processed concurrently and finish in whatever order the database and network decide.
The failure this produces is usually a stale overwrite. A worker processing the first subscription update writes the pre-payment status; a worker processing the later update writes the post-payment status; if they complete in the reverse order, the account is left describing a state the provider abandoned seconds ago. The customer's access reflects an intermediate state indefinitely, because nothing further will happen to correct it.
Several approaches address this, and they are not mutually exclusive:
Treat events as triggers, not as data. On receiving a subscription event, retrieve the current subscription from the provider's API and reconcile local state against it. The event tells you something changed; the API tells you what is true now. This is the approach Stripe's own guidance points toward for ordering-sensitive cases. It costs an API call per event and eliminates an entire class of ordering bug.
Compare versions or timestamps before writing. Reject a write whose source event is older than the last event applied to that object. This requires storing the applied version on the object and is cheap, but it depends on having a monotonic version to compare, and event creation timestamps at second granularity can tie.
Validate transitions rather than assigning states. If local state is canceled and an event would set it to past_due, that transition is not legal, and the correct response is to log a discrepancy and re-fetch rather than to apply it. State machine validation catches ordering problems as a side effect of catching everything else.
Persist every event before acting. Even when out-of-order processing produces a wrong intermediate state, having the full ordered event history makes the situation reconstructible and correctable after the fact.
The right combination depends on volume, on how much provider API latency the system can absorb, and on how many event types drive business-critical transitions. What does not work is assuming the intuitive order and discovering the assumption through a support ticket.
Source of Truth Is a Per-Fact Question
"Stripe is our source of truth" is a sentence that ends architectural discussions without resolving them. It is too coarse to be an engineering decision, because a subscription business contains several categories of truth and different systems are authoritative for different ones.
| Fact | Natural owner | Why | How other systems learn it |
|---|---|---|---|
| Did this payment succeed | Payment provider | Only the provider interacts with card networks and banks | Events plus API reads |
| What is owed on this invoice | Payment provider | Invoice generation, tax, and credits are computed there | Events, invoice API, exports |
| Is the payment method valid | Payment provider | Instrument state lives with the provider | Events |
| Which features can this account use | Application | Authorization is enforced on the request path | Derived from provider grants plus internal grants |
| How much has this account consumed | Application | Usage is measured where it occurs | Reported to provider for billing |
| What did the customer agree to | Contract or CRM, for negotiated deals | The signed document is the commercial artifact | Manual or automated propagation into provider and application |
| What are the account's product limits | Application | Enforced in product logic, may exceed purchased plan | Entitlement records |
| Recognized revenue for the period | Accounting system | Recognition rules are an accounting function | Exports from provider and application |
| Tax calculated and collected | Tax engine, with the provider recording it | Rates and rules are maintained by the tax system | Invoice records |
| Who changed this account and why | Application audit log | Only the application sees internal actors | Not propagated; queried directly |
Two things follow from reading this as a table rather than as a slogan.
First, a fact that has no owner will be inconsistent. Entitlement state is the most common orphan: the provider knows what was purchased, the application knows what is enforced, and nobody owns the mapping, so it exists implicitly in whichever code path last touched it.
Second, a fact with two owners will drift. If both the application and the provider can independently change a subscription's price, and neither treats the other as authoritative, the two records will disagree eventually, and the disagreement will be discovered by a customer.
Eventual consistency is the realistic target, not strong consistency, because these systems are separated by networks and by human beings. What matters is that the convergence is designed rather than assumed: a defined direction of propagation for each fact, a defined maximum lag, and a mechanism that detects when convergence did not happen.
[Figure 6: A source-of-truth boundary diagram. Three vertical lanes labeled Payment Provider, Application, and Finance Systems. Facts are drawn as labeled tokens placed in the lane that owns them: payment outcome, invoice amount, payment method validity in the first lane; entitlement state, usage ledger, audit trail, internal account model in the second; recognized revenue, tax remittance, receivables ageing in the third. Solid arrows show authoritative propagation between lanes; dashed arrows show read-only copies. A dotted vertical band across all three lanes represents the reconciliation process that verifies the copies still match their sources.]
Reconciliation Assumes Systems Can Drift
Reconciliation is not a testing activity or a one-time cleanup. It is a permanent operational process that exists because independently maintained systems, connected by asynchronous messages and modified by humans, will diverge at a low but non-zero rate.
Two concrete shapes of the problem:
Application: 10,142 accounts with active entitlements
Billing provider: 10,137 subscriptions in an active or trialing status
Difference: 5
The count tells you nothing useful. The identity of the five is the entire value. Perhaps three are accounts with manually granted access pending a signed contract, which are correct and expected. Perhaps one is a customer who canceled last week whose entitlement was never revoked. Perhaps one is an event that dead-lettered eleven days ago and nobody looked at the queue.
Provider: invoices marked paid in the period total $842,000
Internal: revenue report for the same period totals $838,400
Difference: $3,600
Again, the aggregate is a symptom. The causes are usually a small number of identifiable items: a refund issued in the dashboard that never reached the internal ledger, a currency conversion applied at a different rate or on a different date, an invoice with a credit applied that the internal report treated as revenue, or a timing difference where a payment settled after the period boundary in one system and before it in the other. Some of these are defects and some are legitimate differences that should be classified and excluded rather than investigated monthly.
A working reconciliation layer compares specific object classes on a schedule and produces a queue of discrepancies rather than a number:
- Customers: provider customers without a local account, and local accounts referencing a provider customer that no longer exists
- Subscriptions: status mismatches, price mismatches, quantity mismatches, renewal date drift
- Entitlements: accounts whose resolved entitlements do not match what their subscription and contract grants imply
- Invoices: invoices present in one system and absent in the other, and amount differences on invoices present in both
- Payments and refunds: money movements at the provider without a corresponding internal ledger entry
- Usage: internal ledger totals versus provider meter summaries for each closed period
- Events: provider event ids in a time window that have no corresponding row in the local events table
That last check is quietly one of the most valuable. It detects missed events without relying on the delivery mechanism that missed them, and it catches changes made in the dashboard that generated events nobody handled.
The output should be actionable per item: what differs, since when, the identifiers on both sides, and a suggested classification. Discrepancies that are expected need a way to be acknowledged so that the queue stays small enough that a non-empty queue means something. A reconciliation report that always shows two hundred items is functionally the same as no reconciliation at all.
[Figure 7: A reconciliation loop drawn as a cycle. Extract from provider (subscriptions, invoices, payments, events) and extract from application (accounts, entitlements, ledger, usage) both feeding a comparison stage keyed on shared identifiers. The comparison emits three outputs: matched, expected difference with a rule reference, and unexplained discrepancy. Unexplained items flow into a review queue with owner and age. Resolved items feed back into a rules layer that classifies future occurrences of the same pattern. Show the loop running on a schedule with a lag window, since recent items may simply not have converged yet.]
[Internal link opportunity: integration testing]
Support Eventually Needs Billing Infrastructure Too
Nobody plans an internal billing administration tool. It arrives anyway, one urgent request at a time, and the only question is whether it arrives as a designed system or as an accumulation of engineer-executed database updates.
The requests are predictable enough to be listed in advance:
Support needs to extend a trial by seven days, issue a $40 credit, resend an invoice to a different address, look at why a payment failed, correct a company name on future invoices, trigger a retry after the customer updated their card, manually activate an account whose provisioning failed, and undo a change a customer made by mistake ten minutes ago.
Finance needs a list of open invoices by age, refunds issued in a period with their reasons, tax summaries by jurisdiction, the reconciliation queue, and an export that matches what the accounting system expects.
Customer success needs the contract context, the current entitlements and why the account has them, the renewal date, seat utilization against seat entitlement, and the account's credit balance before offering another one.
The provider's dashboard covers part of this and is a capable operational tool. It covers the part that is about payments and subscriptions. It does not know about the application's entitlement grants, its usage ledger, its internal audit trail, or the relationship between a provider customer and an internal account hierarchy. An agent working only in the dashboard can change billing without changing the product, and an agent working only in the internal tool can change the product without changing billing. Both are ways to create the drift discussed above.
Routing every commercial correction through an engineer is worse than either. It is slow for the customer, it consumes engineering time on operations, it produces changes with no record of who requested them or why, and it makes the correction path itself untested, since ad hoc statements are written under time pressure against production.
What distinguishes a designed admin capability from a dangerous one is not its feature list but its constraints:
Every powerful action is a defined operation, not a data edit. "Extend trial" is an operation with parameters and validation. Editing a trial_ends_at column is not, because it bypasses whatever else should happen when a trial is extended.
Every action records actor, timestamp, reason, before state, and after state. The reason field should be required and free-text, because six months later the question will be why, not what.
Actions have permission boundaries proportional to their blast radius. Resending an invoice and writing off a $30,000 receivable are both administrative actions, and they should not require the same authority. Monetary actions benefit from thresholds above which a second approval is required.
Actions are reversible or explicitly marked irreversible. An agent who applies a credit to the wrong account should be able to reverse it through the tool, with both the original and the reversal in the record. A refund that has already left the account is irreversible, and the interface should say so before it happens.
Actions that touch both systems do so atomically or fail visibly. An operation that updates the provider and then the application must handle the case where the second step fails, either by retrying durably or by surfacing the partial state for correction. Silently succeeding on one side is how the reconciliation queue fills up.
Unrestricted administrative access is worth naming specifically as a risk. An interface where any agent can set any account to any state, with no record and no limits, concentrates the ability to cause financial and access-control damage in the least monitored part of the system. OWASP's application security guidance treats broken access control and insufficient logging as leading categories of real-world weakness, and internal tooling is where both tend to be weakest because it is assumed to be used only by trusted people.
Manual Changes Are Part of the System
A related principle deserves stating directly: manual intervention in billing is normal and will not stop. A sales representative applies a negotiated discount in the dashboard because the deal closed on Friday. A finance analyst voids an invoice with the wrong PO number. A support lead extends a payment deadline for an important account. These are the correct actions in those situations, and prohibiting them would make the business slower without making it safer.
The danger is not the manual change. It is an architecture that assumes all changes originate from application code. Under that assumption, the application's state is treated as authoritative because it is the only writer, caches are invalidated only on application-initiated writes, and audit trails only record application actions. Every one of those assumptions breaks the first time somebody uses the dashboard.
The system-level responses are the ones already described, applied deliberately rather than incidentally. Provider events should be treated as authoritative inputs regardless of who caused them, including events generated by dashboard actions. Reconciliation should compare states rather than trusting that all changes were observed. The internal audit trail should ingest provider-side changes so that the account history is complete even when the actor was working in another tool. And where a manual action has product consequences the provider cannot know about, such as a negotiated entitlement, the operation should exist in the internal tool so that the product consequence is applied along with the billing change.
Tax Adds Data Before It Adds Arithmetic
Tax calculation is largely a solved problem in the sense that specialized systems exist to do it. Stripe Tax calculates and can collect tax on subscriptions and invoices, maintains rates and rules, and supports registration-based logic. Other tax engines integrate similarly. Delegating the calculation is usually correct.
What cannot be delegated is the data the calculation depends on, and that data is the application's responsibility.
Tax determination generally requires knowing where the customer is, what they are, and what they bought. Concretely:
Customer location, established from a billing address, and often corroborated by other evidence. Which address governs is a rules question, and the application must collect and retain the ones the rules require.
Business versus consumer status, which changes the treatment entirely in many jurisdictions. This is a field the application must capture at signup or in the billing settings, and it changes over time as sole traders incorporate and companies restructure.
Tax identification numbers, which must be stored, validated where validation services exist, and reflected on the invoice document.
Product tax classification, since software delivered as a service is categorized differently across jurisdictions, and a product with mixed components (software plus support plus training) may not have one classification.
Exemption status and supporting documentation, with an expiry date, because certificates lapse and an expired certificate silently changes the correct treatment.
Effective dating, because all of these can change. An invoice issued in March must reflect the customer's status and rates as of March, not as of whenever someone opens the record.
That last point is the one that most often becomes a defect. If a finance team corrects a customer's tax details, and the application stores those details as mutable fields on the customer record that historical invoices read at render time, then correcting the data retroactively alters documents that were already issued and possibly filed. Historical invoices need to carry a snapshot of the tax-relevant facts as they were at finalization, not a reference to current values.
Tax rules are jurisdiction-specific, change frequently, and depend on facts about a specific business. Nothing in this section is tax or legal advice. The engineering point stands independently: whatever the rules require, they require reliable, well-typed, effective-dated customer and transaction data from the application, and no tax engine can compensate for data the application never collected.
Currency Is Not a Display Setting
Multi-currency billing introduces distinctions that are easy to conflate:
Display currency is what the pricing page shows. It may be chosen by geography, by user preference, or by both.
Billing currency is the currency in which the subscription is denominated and the invoice is issued. Once set for a subscription it is generally fixed, because changing it mid-term makes prorations and credits incoherent.
Settlement currency is what the business receives in its bank account after the provider converts, if conversion occurs.
Reporting currency is what internal financial reporting uses, requiring conversion at a defined rate on a defined date.
Two architectural approaches exist and they behave differently. A price catalog with an explicit price per currency means someone decided that the plan costs $99, €99, and £89, and those numbers are stable, marketable, and comparable across customers. Real-time conversion from a base currency means the customer's charge moves with the exchange rate, which produces invoices that differ month to month for a customer who changed nothing. Most subscription businesses choose explicit per-currency catalogs for exactly that reason, accepting that the currencies are not economically equivalent.
The consequences that reach the application:
A customer with a credit balance in euros and a subscription in dollars raises the question of whether the credit applies, and at what rate. The usual answer is that balances are per-currency, which means the credit does not apply and the customer needs an explanation.
Refunds are issued in the original charge currency. A customer charged €100 who is refunded €100 may receive a different amount in their own currency than they paid, because the rate moved between the two dates, and the difference is a support conversation.
Reporting totals across currencies requires a stated rate and date convention. Summing amounts across currencies without conversion produces a meaningless number, and different conventions produce different totals from identical data, which is a reconciliation difference that is not a defect.
Zero-decimal currencies break arithmetic that assumes two decimal places. Amounts are represented in the smallest currency unit, and for currencies such as the Japanese yen the smallest unit is the currency itself. Code that divides by 100 to display an amount produces a hundredfold error in those currencies, in a code path that tests denominated in dollars will never exercise.
Disputes Reopen Settled Payments
A payment that succeeded months ago can be reversed by the cardholder's bank. The funds are withdrawn, a fee is applied, and the business has a limited window to submit evidence. Disputes are less common in B2B subscriptions than in consumer commerce, but they are not absent, and the state they create is unusual because a transaction that was final becomes provisional again.
The product questions that need answers before the first dispute rather than during it:
Does access continue while the dispute is open? The money is gone, but the customer may be disputing a single charge on an otherwise healthy multi-year relationship, and cutting off access mid-dispute makes the outcome worse.
Does the account become restricted, and at what point? Some businesses restrict on dispute, some on dispute loss, and some not at all for accounts above a value threshold.
What evidence is retained, and can it be produced quickly? Evidence submission typically requires records of what the customer agreed to, when they agreed, what they used, and what communication occurred. A product that does not retain acceptance timestamps, usage records, and support history cannot assemble a response, regardless of the merits.
How does finance learn about it in time? Dispute deadlines are short relative to monthly financial processes, and an event that only appears in a monthly report will be discovered after the window closes.
How is the customer contacted? A dispute is frequently a mistake or an internal miscommunication at the customer, and direct contact resolves a meaningful share of them before the formal process concludes.
The engineering artifact is a dispute state on the payment that is visible in support tooling, that has defined effects on entitlements, and that emits a notification with enough lead time to act. The lifecycle state matters more than any fraud logic, which is a separate discipline and outside the scope of a billing architecture.
A Conceptual Billing Domain Model
No schema is universally correct, and copying one from an article is how products end up with entities they do not need and none of the ones they do. What follows is a set of concepts with the relationships that recur across subscription businesses, offered as a vocabulary rather than a specification.
Account internal organization or workspace; what the product is scoped to
Customer billing party; has address, tax status, contacts, currency
PaymentMethod instruments on file
Subscription recurring commitment; status, current period, renewal date
SubscriptionItem price reference + quantity
Invoice statement for a period; status, numbering, totals, snapshot of tax facts
InvoiceLine description, quantity, unit amount, period, tax, discount allocation
Payment attempt against an invoice; status, method, timestamps
Refund reversal referencing a payment, with reason and actor
CreditLedgerEntry signed amount with source, scope, expiry
Dispute reference to a payment, status, deadlines, evidence links
Product what is sold
Price currency + interval + amount + tier structure; immutable once used
Feature capability identifier the application understands
Entitlement resolved capability for an account
EntitlementGrant source (subscription item, contract, manual, trial, promo),
window, actor, reason
UsageRecord measured event with stable identifier, timestamp, meter, quantity
UsagePeriodTotal aggregated total per meter per period, with a finalized flag
BillingEvent received provider event: id, type, payload, received/processed/failed
AuditEntry actor, action, target, reason, before, after, timestamp
Several design points are worth drawing out.
Account and Customer are separate. The thing the product is scoped to and the thing that pays are often the same and sometimes are not. A parent company paying for four workspaces, a reseller paying on behalf of an end customer, and a workspace transferring to a new owner all require the distinction. Merging them is cheap early and expensive to unwind.
Prices are immutable once referenced. Changing the amount on a price that existing subscriptions point to changes what those customers are charged. New prices should be created instead, with the old ones archived rather than deleted, so that historical subscriptions and invoices continue to reference something that explains them.
Provider identifiers coexist with internal identifiers. Every entity that has a counterpart at the provider carries a provider_customer_id, provider_subscription_id, provider_invoice_id, or equivalent. What it should not do is use the provider's identifier as its own primary key, or model itself as a mirror of the provider's object shape. The internal model needs to express concepts the provider does not have (accounts, grants, contract terms, internal actors) and needs to survive changes in provider object models, and possibly a change of provider. Provider identifiers are foreign references, and treating them that way keeps the domain model about the business rather than about the integration.
Entitlement grants record their source. A grant that says only "this account has SSO" cannot be revoked safely, because nothing knows whether it came from the subscription, a contract, or a support agent. A grant that records source, window, actor, and reason can be revoked, audited, and explained.
Ledger-shaped things stay append-only. Credits, usage, and audit entries are histories, not current values. Storing a credit_balance field that gets incremented and decremented loses the ability to answer why the balance is what it is, and that question is asked whenever the balance is disputed.
Subscription State Machines
Billing state is not a set of independent flags; it is a small number of state machines with defined transitions, and treating it that way makes the invalid combinations impossible to represent rather than merely undesirable.
A commonly useful subscription state set, with the caveat that implementation names differ and providers use their own vocabulary:
trialing
active
past_due
grace_period
restricted
suspended
cancel_at_period_end
canceled
Stripe's own subscription statuses include incomplete, incomplete_expired, trialing, active, past_due, canceled, unpaid, and paused. They are not identical to the list above, and that is expected: the provider's states describe the collection relationship, while the application's states describe the commercial and access relationship. Mapping one onto the other explicitly, in one place, is preferable to letting each handler infer it.
The transitions carry the meaning:
trialing → active conversion succeeded
trialing → canceled trial ended without conversion
trialing → past_due conversion charge failed
active → past_due renewal payment failed
past_due → active payment recovered
past_due → grace_period retry schedule exhausted, policy grace begins
grace_period → restricted grace elapsed, access reduced
restricted → active payment recovered
restricted → suspended restriction window elapsed
suspended → active payment recovered, reactivation permitted
suspended → canceled recovery abandoned
active → cancel_at_period_end customer canceled, period remains
cancel_at_period_end → active customer reversed the cancellation
cancel_at_period_end → canceled period ended
active → canceled immediate cancellation, usually with refund logic
canceled → active not a transition; this is a new subscription
That final line is a policy encoded as a constraint. Allowing a canceled subscription to become active again produces subscriptions whose period boundaries and invoice history no longer describe a continuous relationship. Requiring a new subscription for a returning customer keeps the history interpretable, at the cost of handling reactivation explicitly.
The distinction between active → canceled and active → cancel_at_period_end → canceled is worth stating because the two are often implemented as the same code path. The first ends the relationship now and raises questions about the prepaid remainder. The second ends it on a known future date with no monetary consequence. Customers who click the same button expect the second and are surprised by the first.
[Figure 8: A subscription state machine diagram. Nodes for trialing, active, past_due, grace_period, restricted, suspended, cancel_at_period_end, and canceled, with directed edges labeled by the triggering event. Use distinct visual treatments for three categories of edge: customer-initiated, provider-event-driven, and time-driven. Mark canceled as terminal. Annotate each node with the entitlement level that applies in that state, so the diagram doubles as the entitlement policy reference.]
The connection to testing is direct. A state machine defines a finite set of legal transitions and a much larger set of illegal ones. Both are testable. A subscription model expressed as five independent booleans defines thirty-two combinations, most of which are meaningless, none of which are enumerable, and all of which are reachable.
Time Is a Billing Dependency
Billing is one of the few domains where the calendar is an input to correctness rather than a display concern.
Month lengths vary, so a subscription created on the 31st has no equivalent date in most months. Providers apply a rule, typically billing on the last day of shorter months, and the application must agree with that rule when it computes period boundaries locally. February compounds it, and leap years add a day that annual subscriptions created on February 29 will encounter every fourth year.
Time zones determine when a period ends, and the answer differs by observer. A billing period that closes at midnight UTC closes at 7pm in New York and 9am the following day in Tokyo. A customer looking at their usage dashboard in local time and an invoice generated on a UTC boundary will disagree about which day certain usage belongs to unless the dashboard states the boundary explicitly.
Daylight saving transitions make some local days 23 or 25 hours long. Proration computed in local hours produces slightly different results across those boundaries, and scheduled jobs that run at a fixed local time either skip or repeat an hour.
Annual anniversaries drift when the renewal logic adds 365 days rather than one year. Over several renewals the date moves, and a customer whose contract says the term runs to March 14 finds an invoice dated March 11.
Testing any of this by waiting is impractical, and manipulating system time is unreliable because the provider's clock is not yours. Stripe addresses this with test clocks: in a sandbox, a test clock is attached to customers and their subscriptions, and advancing the clock causes the provider to generate the events and invoices that would occur at that point in time. Stripe's documentation describes using them to test subscription renewals, trial conversion, plan changes mid-cycle, and payment failure handling, and notes that advancing the clock produces the corresponding lifecycle webhooks. There are practical constraints worth knowing before designing a test suite around them, including granularity limits on how finely time can be advanced.
The application side needs an equivalent capability. Any code that reads the current time directly is untestable at period boundaries. Injecting a clock, so that tests can position the system at 23:59:59 on the last day of February in a specific time zone, is a small change with a large effect on which defects can be caught before release.
[Internal link opportunity: test automation]
Testing Billing Means Testing Transitions
Automating a successful checkout is worth doing and proves the narrowest part of the system. The states that generate incidents are the ones that appear after the first payment, and they are reached by transitions rather than by page flows.
A useful way to organize billing test coverage is by transition and by the layer at which each is verifiable. The following table is a starting inventory rather than a complete suite.
| Transition or condition | What can go wrong | Most effective layer | What the assertion should check |
|---|---|---|---|
| First subscription created | Entitlement not granted; wrong plan mapped | Integration with provider sandbox | Entitlement rows match purchased price, effective immediately |
| Renewal succeeds | Period boundaries wrong; entitlement expires early | Provider test clock + integration | New period dates, entitlement continuity, one invoice |
| Renewal payment fails | Access removed too early or not at all | Integration + state machine unit tests | State is past_due, entitlement matches policy, notification queued once |
| Retry succeeds after failure | Account stuck in restricted state | Integration | Return to active, restrictions lifted, no duplicate charge |
| Upgrade mid-cycle | Wrong proration; renewal date unexpectedly reset | Unit for arithmetic, integration for effect | Exact amounts, entitlement effective time, renewal date policy |
| Downgrade at period end | Access removed immediately | Unit + integration | Scheduled change stored, entitlement unchanged until effective date |
| Quantity increase and decrease | Seat allowance and billed quantity diverge | Integration | Provider quantity equals internal allowance |
| Cancellation at period end | Access removed on cancel rather than at period end | Integration | Entitlement persists to period end, no renewal invoice |
| Reactivation within period | Duplicate subscription created | Integration | Original subscription resumed, no new charge |
| Refund, full and partial | Internal ledger unchanged | Contract test against event payloads | Ledger entry created, revenue report reflects reversal |
| Credit applied | Wrong calculation order | Unit with exact totals | Invoice total matches the defined ordering |
| Coupon applied and expiring | Discount persists past its duration | Integration with clock | Discount present for defined periods and absent after |
| Trial expiry without payment method | Over-quota state undefined | End-to-end | Defined product behavior, no data loss |
| Tax status change | Historical invoice mutated | Unit + data assertions | Prior invoices unchanged, new invoices reflect new status |
| Zero-decimal currency charge | Amount off by 100 | Unit | Minor-unit handling per currency |
| Duplicate webhook delivery | Double provisioning or double credit | Integration replaying the same event | Single effect, second delivery is a no-op |
| Out-of-order webhook delivery | Stale state overwrites current | Integration replaying in reverse | Final state matches provider, or event rejected as stale |
| Delayed webhook (hours late) | Handler assumes freshness | Integration | State converges, no incorrect intermediate action |
| Provider API timeout during change | Partial change, user cannot retry | Integration with fault injection | Idempotent retry produces one change |
| Worker crash mid-processing | Effect applied twice on retry | Integration with induced failure | Uniqueness constraints hold |
| Reconciliation mismatch injected | Drift undetected | Reconciliation job test | Discrepancy appears in queue with correct classification |
The layers each catch something specific, which is why none of them substitutes for another:
Unit tests own monetary arithmetic and state transition legality. Proration amounts, discount and credit ordering, rounding, tier calculations, and the transition table itself are pure logic and should be tested with exact expected values, not tolerances.
Integration tests against the provider's sandbox own the assumptions about how the provider actually behaves. Whether a proration is invoiced immediately, which events a given change emits, what a subscription's status is after a failed first payment. These are the assumptions that documentation summarizes and that differ from what the team assumed.
Contract tests on event payloads own the shape of what arrives. Handlers written against a payload shape break when a provider API version changes; a test that validates handler behavior against captured representative payloads catches that at build time.
API-level tests own the internal billing endpoints: the upgrade endpoint, the cancel endpoint, the admin operations. They are where idempotency and authorization on money-moving operations get verified.
End-to-end tests own the small number of flows where the interaction between the interface, the provider, and the product genuinely matters: checkout, payment method update during dunning, and the self-service upgrade. Keeping this set small is deliberate, since these tests are the slowest and most brittle and the same coverage is usually available lower down.
Production monitoring owns everything that cannot be simulated: real payment method behavior, real issuer declines, real delivery latency, real reconciliation drift. Billing has an unusually high proportion of correctness that is only observable in production, which is why the observability discussed below is part of the test strategy rather than adjacent to it.
Billing systems are strong candidates for risk-based prioritization, because the consequences of a defect are unusually broad: a single incorrect transition can affect revenue records, customer access, financial reporting, and support workload at the same time. [Internal link opportunity: SaaS QA strategy]
Test Data Is the Constraint
The reason billing regression suites decay is rarely the assertions. It is that reaching the state under test is expensive. Verifying behavior for a customer with an overdue invoice, an annual plan expiring in three days, two subscriptions, a $25 credit balance, and a grandfathered price requires constructing that customer, and doing it by hand takes long enough that it happens once.
Treating billing fixtures as code is what makes the suite maintainable. A small library of state builders that construct known scenarios through the provider's sandbox API and the application's own operations, each producing a named, reproducible state:
new_customer_no_payment_method
customer_in_trial_day_12_of_14
customer_with_failed_renewal_in_grace
customer_with_annual_plan_expiring_in_3_days
customer_with_two_subscriptions_one_canceled
customer_with_credit_balance_and_active_discount
customer_on_legacy_2022_price
enterprise_customer_net30_with_open_invoice
Combined with a controllable clock, these become the entry points for the transition tests above rather than a manual setup step, and the cost of adding a new scenario falls to writing one builder.
Money Is Not an Ordinary Number
Two properties of monetary arithmetic cause defects that ordinary numeric testing does not catch.
Floating-point representation cannot exactly represent most decimal fractions, so accumulated arithmetic drifts. The conventional response is to represent amounts as integers in the currency's smallest unit, which is what payment providers do, and to convert only at display. That removes representation error but does not remove rounding decisions.
Rounding decisions appear whenever a calculation produces a fraction of the smallest unit, which happens constantly in this domain: percentage discounts, prorations over day counts, tax on discounted amounts, allocating a discount across multiple lines. The choices are where to round (per line or on the total), which direction, and how to allocate a remainder when a rounded total does not equal the sum of rounded parts. A $100 discount allocated across three lines cannot be split into three equal integer amounts, and one line must absorb one extra minor unit. Which one is a decision.
Tests should assert exact expected totals for a set of deliberately awkward inputs: amounts that do not divide evenly, periods with odd day counts, discounts producing repeating decimals, and currencies with zero or three decimal places. Assertions using approximate comparison hide precisely the class of defect that matters, because a one-cent difference per invoice is invisible in a test and material in a reconciliation.
Observability for Billing
The useful signals are the ones that correspond to a specific operational question and have a defined response when they move. A short list covers most of what teams find themselves needing:
- Renewal attempts, successes, and failures per day, segmented by failure reason, because a shift in the reason mix indicates a provider or configuration change rather than customer behavior.
- Unprocessed billing events, by age. A queue that is normally empty and now has eleven items an hour old is a clearer signal than any throughput metric.
- Events that have exhausted retries and are in the dead-letter queue. This should alert, not appear in a dashboard, because every item is an account in an unknown state.
- Accounts where payment state and entitlement state are inconsistent. This is the reconciliation output surfaced as a metric, and it is the single most direct measure of whether billing and product agree.
- Invoices open beyond their expected duration, separated by collection method, since a self-service invoice open for four days means something different from an enterprise invoice open for twenty.
- Subscriptions in a transitional state longer than the state permits: past_due beyond the retry schedule, grace period beyond the policy window, scheduled changes past their effective date and not applied.
- Provider API error rate and latency on the paths that block user actions, particularly checkout and plan changes.
What makes these useful is not the count but the ability to move from the number to the specific accounts behind it. A dashboard that reports seventeen inconsistent accounts without letting anyone list them produces awareness and no action.
Auditability Makes Billing Incidents Solvable
Billing investigations are historical. The question is almost never what the state is now; it is how it came to be that way, and whether the same thing happened to other accounts.
To answer that, the system needs to be able to reconstruct: what the customer's plan and price were at a given date, what changed and when, who or what initiated each change, which provider events were received and in what order, which of them were processed and by which version of the code, which invoices were generated with which line items, which credits and refunds were issued and for what stated reason, and what the entitlement state was at each point.
Most of this falls out of decisions already described: an events table with receipt and processing timestamps, an append-only credit ledger, entitlement grants that record source and actor, invoices that snapshot their inputs, and an audit log covering administrative operations. Recording the application version or deployment identifier alongside processed events adds a small amount of data and answers a question that comes up in most billing incidents, which is whether the account was processed before or after a particular change shipped.
This does not require full event sourcing. The argument for event sourcing in billing is real, and so is the cost. A middle position is common and effective: keep current-state tables for the things the application reads on every request, and keep append-only histories for the things that carry money, permission, or contractual meaning.
Changing Billing Code Is a State Migration
Deployments in most parts of a product affect behavior going forward. Billing deployments affect obligations that already exist, which changes what safe release looks like.
Changing plan identifiers or the mapping between plans and entitlements affects every existing subscriber immediately, including ones whose subscriptions were created under assumptions the new mapping does not preserve. Introducing new pricing raises the question of who moves and who does not, and the default answer for existing subscriptions should be explicit rather than implied by whatever the code does. Changing a webhook handler changes how events are interpreted, including events already in the queue that were generated under the old interpretation, so handlers need to tolerate payloads from before the change. Moving billing logic between services changes which system writes which fields during the transition, and dual-write periods are where drift is introduced.
The practical protections are unglamorous. Add before removing, so new and old plan identifiers coexist. Backfill and verify before switching reads. Keep handlers backward-compatible for at least the provider's retry window, since events generated three days ago can still arrive. Verify migrations against a snapshot of production-shaped subscription data rather than fixtures, because the interesting cases in a billing table are the accounts nobody remembers creating. And run reconciliation before and after, since the fastest way to detect that a billing deployment did something unintended is that the discrepancy queue grows.
Legacy Pricing Persists
Pricing changes, and each change leaves a cohort behind. A product that has repriced three times has customers on four price structures, plus negotiated exceptions that match none of them.
The clean way to hold this is to separate three things that are easy to conflate. The commercial catalog is what can be sold today; it changes with marketing decisions. The subscription contract is what a specific customer agreed to and continues to pay; it is historical and does not change because the catalog did. The product capability set is what the software can do; it changes with engineering work and is not versioned by pricing.
Entitlements bridge them. A customer on a 2022 price has grants that were defined when that price was created, and those grants continue to be honored even though nothing on the current pricing page produces them. New capabilities added since then either apply to that cohort or do not, and that is a deliberate decision per capability rather than a consequence of how the conditional was written.
Deleting old prices from application logic is the specific hazard. The price still exists at the provider, subscriptions still reference it, and invoices still cite it. Removing its representation from the application means those subscriptions no longer map to anything, and the failure surfaces as customers losing access at renewal or as entitlement resolution falling through to a default.
Migrations Between Billing Structures
Larger migrations recur in most subscription businesses: consolidating separate products into a bundle, moving a cohort from legacy pricing to current pricing, moving between provider accounts after a corporate change, or replacing the provider entirely.
The invariants worth protecting are the same across all of them. Customers should not be charged unexpectedly or on an unexpected date. Renewal dates should be preserved unless the customer agreed otherwise. Entitlements should not lapse during the transition, including for accounts in dunning or in a scheduled-change state. Invoice history must remain accessible, since customers and auditors reference documents years later. Credit balances must transfer or be settled explicitly.
The migrations that go badly usually go badly for the accounts in unusual states at the moment of the cutover: mid-dunning, mid-trial, with a pending scheduled downgrade, with a credit balance, or on a price that exists in neither the source nor the target catalog. Enumerating those states before the migration, and running the migration against each one in a sandbox, is most of the work.
Where the Boundary Sits
The division of responsibility is not fixed, and drawing it as a rigid line is misleading. What can be said is which side each capability naturally starts on.
Payment and billing infrastructure is well suited to card and alternative payment method handling, network-level payment method updates, authentication flows and regulatory requirements such as strong customer authentication, subscription scheduling and renewal execution, invoice generation and delivery, retry scheduling and dunning communication, tax rate determination and collection, usage aggregation against defined meters, and the event stream describing all of it. Building any of these in-house means maintaining them against changing rules and network behavior indefinitely, which is a poor use of a software company's engineering capacity.
The application retains what is specific to its own product and business: what a customer is allowed to do and how that is enforced on the request path, what usage means and how it is measured, the internal account and permission model, customer-specific commercial terms that do not fit the catalog, the operational workflows support and finance run, the internal financial records that reporting depends on, and the reconciliation that verifies the two sides still agree.
The middle is where teams should make deliberate choices rather than defaults. Entitlement resolution can sit largely with the provider when entitlements are a clean function of purchased plans, and must sit with the application when they are not. Usage aggregation can be delegated when the meter definition is simple and disputes are rare, and needs an internal ledger when usage is expensive, contested, or contractually significant. Dunning communication can be handled by the provider's templates, or owned by the product when the messaging needs product context the provider does not have.
Getting this wrong in the direction of building too much produces a team maintaining a payments platform they did not want. Getting it wrong in the other direction produces a product whose commercial rules live in a vendor configuration screen, invisible to code review, untested, and unversioned.
When Billing Deserves Its Own Boundary
Billing logic living inside a well-organized monolith module is a perfectly good arrangement, and for most products it is the right one for a long time. Extracting a service is a response to specific pressures, not a maturity milestone.
The signals that the domain is outgrowing its current placement tend to be concrete: several product lines with different billing rules that keep entangling; usage metering at a volume that has different scaling characteristics from the rest of the application; both self-service and contract-driven commercial paths that must resolve to one entitlement model; a growing catalog of currencies, tax jurisdictions, and payment methods; billing event processing that needs isolation so that a backlog does not affect user-facing capacity; and enough teams touching billing code that coordination cost is visible in delivery time.
The counter-pressures are equally concrete. A billing service introduces a network boundary on paths that need to be synchronous and correct, such as checkout. It introduces distributed transactions where a local transaction previously sufficed. It requires the entitlement read path to be fast and available, which usually means caching or replication, which reintroduces consistency questions. Splitting a domain that is still being figured out tends to fix the wrong boundary in place.
A common intermediate step captures much of the benefit at lower cost: a clearly bounded billing module inside the existing application, with an explicit interface, its own tables, no direct reads of its tables from elsewhere, and its own event processing. That structure makes later extraction mechanical if it becomes necessary, and reveals whether the boundary is right before it is expensive to move.
Failure Patterns
The following are recurring patterns rather than reports of specific incidents. Each is described by its triggering condition, the resulting system behavior, the underlying cause, and the architectural capability that prevents or detects it.
1. Payment succeeds, provisioning never happens. A payment completes and the event handler crashes before the entitlement is written. The customer is charged and has no access. The cause is treating HTTP 200 as evidence that the business process completed. Prevention is durable receipt with asynchronous processing and retries; detection is a reconciliation check comparing recent successful payments against entitlement changes.
2. A duplicate event applies a credit twice. The same event is delivered twice and the handler adds a credit each time. The cause is a handler with no effect-level uniqueness. Prevention is a uniqueness constraint on the source reference in the credit ledger, which makes the second application a no-op regardless of how many times the event arrives.
3. A downgrade removes access the customer already paid for. The customer requests a downgrade on day eight of a prepaid month and loses features immediately. The cause is applying an entitlement change at request time rather than at the change's effective date. Prevention is modeling scheduled changes as first-class state with an effective timestamp, and resolving entitlements from grants with windows rather than from the current plan field.
4. A dashboard refund never reaches internal reporting. An agent refunds an invoice in the provider dashboard. The provider's records are correct; the internal revenue report still counts the original payment. The cause is treating the application as the only writer. Prevention is handling refund events as authoritative inputs; detection is reconciliation between provider money movements and internal ledger entries.
5. Billed seats and permitted seats diverge. An admin increases seats, the provider quantity updates, and the application's seat allowance does not, so the customer pays for twelve seats and can use eight. The cause is two representations of the same quantity with no verification. Prevention is deriving the allowance from the subscription item rather than storing it independently; detection is a reconciliation check on quantities.
6. Entitlement granted, billing change failed. A user upgrades, the application grants Enterprise features optimistically, and the provider call fails. The customer has capabilities they are not being charged for, indefinitely. The cause is applying the local effect before the authoritative operation confirmed. Prevention is ordering the operations so the money-side change is confirmed first, with the local change applied idempotently from the resulting event.
7. A failed renewal terminates an account. A card expires, retries fail, and the account is closed and its data deleted within days. The cause is a missing intermediate state between active and terminated. Prevention is an explicit recovery state machine with restricted and suspended states, defined durations, and data retention that outlasts the recovery window.
8. Usage aggregation double-counts. A worker reprocesses a batch after a failure, usage is counted twice, and the invoice is materially too high. The cause is usage events without stable identifiers derived from the underlying work. Prevention is deterministic event identifiers and deduplication at ingestion; detection is period-over-period anomaly checks on usage totals before invoice finalization.
9. A price change reaches existing customers. A price object is updated rather than superseded, and subscribers on that price are charged the new amount at their next renewal without having agreed to it. The cause is treating prices as mutable configuration. Prevention is immutable prices, archived rather than edited, with migration of existing subscribers as a deliberate operation.
10. A tax detail correction rewrites history. Finance corrects a customer's tax registration, and previously issued invoices render with the new treatment because they read current customer fields. The cause is documents that reference mutable data instead of snapshotting it. Prevention is capturing tax-relevant facts on the invoice at finalization.
Reasoning About a Billing Change Before Building It
Take a request that sounds like a small feature: allow customers to upgrade from Pro monthly to Business annual.
The implementation could be a single API call. The design questions it raises are the actual work:
When does Business access begin? Immediately on request, or when the annual payment settles? If the payment requires authentication and the customer abandons the challenge, what state are they in?
What happens to the unused portion of the Pro month? Credited against the annual charge, forfeited, or refunded? If credited, the annual invoice total will not match the advertised annual price, and the invoice needs to explain why.
When does the annual term start? Today, which means the renewal date moves and the customer's finance team sees a new anniversary, or at the existing renewal date, which means the customer waits for a plan they have paid for.
What happens to an existing discount? A 20% coupon applied to the monthly plan may or may not be intended to apply to an annual commitment, and its duration may have been expressed in months.
Which currency applies? The annual price must exist in the customer's billing currency, and if it does not, the change cannot proceed, which needs a defined behavior rather than an error.
What if the payment succeeds and the entitlement update fails? The customer has paid for a year and has Pro access. Recovery must be automatic and must not charge again.
What if the customer clicks twice? Both requests must resolve to one subscription change and one charge.
What will the invoice show? Line items for the annual charge, the proration credit, any discount, and tax, in an order and with descriptions the customer can reconcile against what they were shown at the point of purchase.
What does support see afterward, and how is the change reversed if it was made in error within the hour?
Which signals confirm it worked in production, and which would show it silently failing for a subset of customers?
Written as a review structure, these questions generalize to most billing changes:
| Review dimension | Question to answer before implementation |
|---|---|
| Effective time of access | When does the new entitlement begin, and what is the state between request and confirmation? |
| Effective time of money | When is the customer charged, and against which period? |
| Treatment of the prior period | Credit, refund, forfeit, or carry, and how is it shown on the invoice? |
| Term and renewal date | Does the anniversary move, and does the customer expect that? |
| Interaction with existing modifiers | Discounts, credits, negotiated prices, grandfathered entitlements |
| Currency and tax | Does the target price exist in this currency, and does the tax treatment change? |
| Failure of the money-side operation | What is the customer-visible state, and how do they retry? |
| Failure of the product-side operation | How is it detected, and how does it self-heal? |
| Duplicate submission | What guarantees one effect from repeated requests? |
| Reversal | Can support undo it, within what window, and with what record? |
| Invoice presentation | Do the line items explain the total without a support contact? |
| Observability | Which counters and which reconciliation checks confirm correctness in production? |
The value of the structure is that it turns an implicit set of assumptions into a list of decisions with owners. Most of these questions belong to product and finance rather than engineering; what engineering contributes is knowing that they exist and that leaving them unanswered means they will be answered accidentally by whatever the code happens to do.
What "Done" Means for a Billing Capability
"Stripe integration completed" describes a connection, not a capability. A more useful completion standard for any billing feature is that the following are true and demonstrable:
The state transitions it can cause are enumerated, including the illegal ones, and the illegal ones are rejected rather than merely unlikely.
The monetary behavior is explicit: which amounts are calculated, in what order, with what rounding, and what the invoice will show.
The entitlement consequences are defined, with effective dates, including what happens in the intermediate state between request and confirmation.
Every failure mode has a recovery path that does not require an engineer, or, where it does, an alert that reaches one.
Asynchronous processing is idempotent at the effect level, verified by replaying events rather than by inspection.
Actions that move money or change access are recorded with actor, reason, and before and after state.
Support can see the account's billing state and history without a database client, and can perform the routine corrections the feature will generate.
Finance can reconcile the resulting records against the provider without manual assembly.
The transitions that carry money or access are covered by tests at the layer where they are actually verifiable.
Discrepancies between the provider's state and the application's state are detectable in production without a customer reporting them.
That standard is more demanding than a passing checkout test and less demanding than perfection. It corresponds closely to the set of things that, when absent, produce the incidents described earlier.
Conclusion
A payment provider executes transactions and supplies a sophisticated set of billing primitives: subscription scheduling, invoicing, proration arithmetic, retry logic, tax calculation, metering, entitlement signals. Stripe supplies more of these than most teams use, and building any of them in-house would be a poor allocation of engineering effort.
What remains with the software company is the commercial meaning of those transactions inside its own product. Which capabilities a customer has and why. What a seat is. What a billable request is. How long an unpaid account keeps working. What a credit applies to. Which system is believed when two disagree. These are not gaps in the provider's functionality; they are decisions that belong to the business, and they become code whether or not anyone writes them down first.
As a subscription business grows, billing turns into a domain with the characteristics that make software hard: money, time, permissions, asynchronous state, exceptions negotiated one customer at a time, and the need to recover from failures without human intervention. The expensive problems in that domain rarely originate in the checkout integration. They accumulate in the states that exist afterward, in the transitions between them, and in the places where two systems each hold a partial and slightly different account of the same commercial relationship.
Billing systems are a strong candidate for risk-based quality engineering, because a single incorrect transition can affect revenue records, customer access, financial reporting, and support workload at once. QAtronic works with software teams on testing critical workflows across APIs, integrations, automation, and end-to-end product behavior, in systems where correctness has commercial consequences.
Frequently Asked Questions
Is Stripe a complete billing system? Stripe provides a large part of one. Stripe Billing handles subscription scheduling, invoicing, proration calculation, coupons, retry and dunning logic, tax through Stripe Tax, usage metering, and entitlement signals derived from purchased products. What it does not do is decide what those things mean in a specific product: how entitlements are enforced on the request path, what a billable unit is, how long an unpaid account keeps access, how negotiated contracts map to product capabilities, or how internal financial records are maintained. Those decisions and the systems that implement them belong to the application.
What is the difference between Stripe Payments and Stripe Billing? Payments covers accepting a payment: payment methods, authorization and capture, authentication, and payouts. Billing sits on top of it and covers recurring commercial relationships: products and prices, subscriptions, invoices, prorations, discounts, usage metering, revenue recovery, and entitlements. A product can use Payments without Billing, for example by managing subscription logic itself and charging on its own schedule, though most subscription businesses use both.
Should a SaaS application store subscription state locally, or read it from Stripe? Both, with different roles. The provider is authoritative for payment and subscription collection state, and the application should reconcile against it. The application needs its own durable record because authorization decisions happen on every request and should not depend on an external network call, because it must represent concepts the provider does not have (internal accounts, manual grants, contract terms), and because it needs a queryable history for support and reconciliation. The provider's identifiers should be stored as references rather than used as the application's primary keys.
How should a SaaS product handle failed subscription payments? As a state machine with defined durations rather than as a retry loop. The provider handles retry scheduling and dunning communication. The application decides what happens to access at each stage: full access during the retry window, a restricted or read-only state after it, suspension, and eventual cancellation with a data retention window. The right durations depend on how disruptive loss of access is for the customer; business-critical B2B platforms typically carry unpaid accounts far longer than consumer products.
What is subscription proration, and who decides how it works? Proration converts a mid-period change into money: a credit for the unused portion of the old plan and a charge for the remaining portion of the new one. The provider calculates it from configuration. The business decides the policy the configuration should express, including whether the change is invoiced immediately or at the next cycle, whether the renewal date moves, whether downgrades produce credits, and how existing discounts apply to prorated amounts.
How should Stripe webhooks be tested? At several layers. Replay the same event twice to verify idempotency at the effect level, not just at the event level. Replay related events in reverse order to verify that stale data cannot overwrite current state. Deliver an event hours late to verify the handler does not assume freshness. Induce a worker crash mid-processing to verify that retries do not duplicate effects. Verify signature failures are handled without state changes. Use test clocks in a sandbox to generate the lifecycle events that renewals, trial expiry, and mid-cycle changes actually produce, rather than hand-constructing payloads.
What is the difference between subscription state and entitlement state? Subscription state describes the commercial relationship: active, past due, canceling at period end. Entitlement state describes what the customer is allowed to do in the product right now. They correlate but are not the same. A canceled subscription can carry full entitlements until the prepaid period ends. A past-due subscription may retain full access by policy. An account may hold capabilities granted by contract or by support that no subscription implies. Treating them as one field is what eventually forces a rewrite.
How do SaaS companies reconcile billing data between systems? By running scheduled jobs that compare specific object classes across systems and produce a queue of individual discrepancies rather than an aggregate difference: customers, subscriptions and their statuses and quantities, invoices and amounts, payments and refunds, entitlement states, usage totals per period, and provider event ids against locally recorded events. Expected differences should be classifiable and acknowledgeable so that a non-empty queue remains meaningful.
When should billing logic be extracted into its own service? When specific pressures appear, not as a maturity milestone. Multiple product lines with divergent billing rules, high-volume usage metering with different scaling characteristics, parallel self-service and contract-driven commercial paths, a large catalog of currencies and jurisdictions, and enough teams touching billing code that coordination is slowing delivery. A well-bounded billing module inside an existing application, with its own tables and an explicit interface, captures much of the benefit and makes later extraction mechanical.
Why do billing bugs cost more than their size suggests? Because a single incorrect transition propagates into several systems at once. A wrongly applied refund affects the payment record, the internal ledger, revenue reporting, and possibly commissions. A wrongly revoked entitlement affects the customer's ability to work, support workload, and the renewal conversation. A duplicated usage record affects an invoice the customer will dispute, the credit issued to resolve it, and the reconciliation of both. The defect is small; the surface it touches is not.