AI-generated. Written by an automated system, checked by independent automated gates, and published without human editorial review. How we label and verify.
A customer wrote in to ask why they still had access. Their card had failed, the provider had moved the subscription to unpaid, and the access system was never told. Looking for others we found the opposite case in the same batch: a credit note issued at the provider that never reached the books. Both had sat inside a subscription reconciliation we ran at every cycle close, and both had passed it.
The run had not missed them. The run had never looked.
What we thought the close was checking
We treated the close as a comparison. Pull the three registers — the payment provider, whatever grants the paid capability, and the ledger — put them beside each other, mark the ones that agreed. The artifact came out saying matched and consistent, with no integer behind any comparison it claimed.
That is the failure in one line: nobody had subtracted anything. A reconciliation in which no difference was ever calculated is not a reconciliation, however clean the output. The assumption underneath was worse — that the provider held the truth and the other two were copies. Three registers record the consequences of a billing period independently, and each is right about something the others cannot see.
Repair order is not a matter of preference
Once we started sizing differences instead of eyeballing them, the tempting first move was money. It is what the customer notices, and the only irreversible action in the set. Repair it before the other registers are straight and you have not removed a difference — you have moved it. A refund creates a provider event with no ledger counterpart behind it; a re-charge does the same in reverse. One drift becomes two, and you made the second.
The order that survives contact is access, then ledger, then money. Access first because it is reversible, and because it is the only class still causing new loss while it stands open. Ledger second because a correction there is additive, auditable, and tells you whether the money move is needed at all. Money last, and prepared rather than executed.
Why a subscription reconciliation has to difference twice
The part that took us longest to see is that a repair is itself an event. A ledger correction posts a line, a refund creates a provider record, an entitlement change moves a grant window. Every fix writes new state into a register you were comparing.
A run that differences once and then repairs closes on a position that stopped existing the moment it acted: right when calculated, stale when published. Worse, because money goes last, the final repair of a pass produces exactly the drift the difference step exists to find, after that step has stopped running. Anything written to has to be differenced again, and non-zero results re-enter the full order, not a shortened one. And a loop that re-differences after every repair runs forever without a budget and an exit that is not success.
Zero, and nearly zero
None of this survives floating-point arithmetic. Amounts have to be integer counts of the currency’s minor unit — one euro is one hundred of them — because a float turns a difference of zero into a difference of almost zero: small enough to read as rounding noise, large enough to keep the row on the report every cycle. Rounding it away hides which class the drift belongs to.
The same discipline decides proration. A mid-period plan change leaves an amount nobody has re-derived, and re-deriving it in integers leaves a remainder: a 4 999 minor-unit monthly plan changed with 11 of 31 days remaining gives 1 773, remainder 26. That 26 goes to the customer, a rounding account, or the next cycle — by a stated rule, to the same place every time.
Two registers that cannot be made to agree
There is a state that is neither failure nor retry. Some pairs cannot converge: the provider account is read-only, an amount one register cannot represent, a field each side derives from a different immutable source. No number of passes closes that gap, and a subscription reconciliation that keeps writing at it is not converging, it is just running.
Naming it is the answer: the subscription is frozen for the run and carried on the residual with its remaining difference and an owner, open until something outside the procedure changes. A residual of zero reached by adjusting a figure until both sides agreed is worse than a named non-zero one.
What follows for anyone building this
A register is not reconciled by being read. It is reconciled by being subtracted — and then, once you have touched it, subtracted again. Evidence of a repair is the value read back out of the register afterwards — not the status code your write returned, not your own log line; both record intent, not result. And this cycle’s residual only means something beside the previous one: stable is one bug, growing every cycle is an unassigned remainder, vanished with no repair recorded means somebody plugged it.
The runnable procedure — the repair classes, the pass budget, and the checkpoint a third party re-derives the verdict from — is in the member library.
How we know
What the billing documentation says
Stripe’s subscription overview treats the state after a failed payment as a setting, not a fixed outcome. When retries are exhausted, you can configure the subscription to move to canceled, to unpaid, or leave it past due. The same failure can therefore land in different states in different accounts, and anything downstream has to read which one it got.
Access is supposed to follow events. Stripe’s guide to webhooks for subscriptions says you can provision access to your product when an invoice is paid and the subscription is active. Granting is tied to an event. Revoking needs an event too, or the grant simply stays where it was.
Events themselves are not a clean record. Stripe’s webhook documentation warns that an endpoint may receive the same event more than once, and that Stripe doesn’t guarantee the delivery of events in the order they were generated. A system that only replays what it was told can end up in a state no one sent it.
Credit notes are their own documents. The API reference describes creating one as a way to adjust the amount of a finalized invoice. The invoice stays, the adjustment sits beside it, and a ledger that only imports invoices never sees it. The Dashboard guide adds a limit: The sum of all credit notes issued for an invoice can’t exceed the invoice total.
Amounts are integers. Stripe’s currency documentation says API amounts are given in the currency’s minor unit, so you set the amount to 1000 to charge 10 USD. Proration happens on the provider side as well. Stripe’s proration guide notes that Credit prorations are issued when customers downgrade or cancel items before the end of the period, so a mid-period change produces money movements that neither the access system nor the ledger created.
How this lines up with our cycle-close runs
Both cases in this article match the documented behavior. The customer who kept access had been moved to unpaid at the provider, and the access system was never told. Given that the target state is configurable and access hangs on events, an access register that misses one transition has nothing that will ever correct it. Only a subtraction against the provider’s current status finds it.
The credit note that never reached the books is the second shape. It adjusted an invoice that had already been finalized. A ledger fed by invoices treats that invoice as settled and has no line to compare. That is why a register is reconciled by being differenced against the others, not by being read.
The webhook warnings explain the rule about evidence. If events can repeat and arrive out of order, neither the write response nor our own log line says what a register holds after a repair. Re-querying the register and comparing the changed field does.
The integer rule is the provider’s own convention, so working in minor units removes a conversion rather than adding one. The proration example in this article, 4 999 with 11 of 31 days remaining giving 1 773 remainder 26, is the part the provider does not decide for you: where the remainder goes is a rule you state yourself.
A subscription reconciliation checklist
- Record which failed-payment state your account is configured to use, and check the access register against that state, not against an assumed one.
- Pull credit notes as their own source. Do not expect them to arrive through invoice imports.
- Store every amount as an integer in the currency’s minor unit, in all three registers.
- Calculate a difference for every pair of registers. A row with no number behind it is not matched.
- Repair in order: access, then ledger, then money. Prepare the money step rather than executing it.
- After each repair, re-query the register you wrote to and difference again.
- Route proration remainders by one stated rule.
- Carry pairs that cannot converge on a named residual with an owner and the remaining amount.
More questions
Can webhooks alone keep access in sync with billing?
Not reliably. Stripe’s documentation says an endpoint may receive the same event more than once, and order is not guaranteed either. Use events to trigger work, then confirm state by reading the subscription itself during the close.
Why are credit notes easy to miss in the ledger?
Because the original invoice does not change. A credit note is issued to adjust the amount of a finalized invoice, not to replace it, so a ledger that only follows invoices keeps the original figure and reports nothing wrong.
Grounded in: our own cycle-close runs across provider, access and ledger, and the revision made on 2026-09-04 after a run repaired and closed in a single pass. Verified: proration re-derived in integer minor units — 4 999 with 11 of 31 days remaining gives 1 773 remainder 26 — and every repair confirmed by re-querying the register written to and comparing the changed field, never the write response. The runnable procedure lives in the member library.