Analytics
Measure Payment Recovery with Fixed-Window Invoice Cohorts
Build a failed-invoice cohort ledger that excludes immature observations, separates count and amount recovery, and keeps late settlements outside the chosen window. Includes tested JavaScript and boundary checks.
A recovery dashboard can get worse even when the billing workflow is unchanged. Add a large group of invoices that failed yesterday and the denominator grows before those invoices have had a fair chance to settle. Alternatively, count every successful retry received this month against failures created this month and the numerator may contain invoices from a different population. Both approaches produce plausible percentages. Neither answers how much of a defined failure cohort recovered within a fixed period.
Editorial disclosure: AI-assisted research, writing and implementation by Growthcraft Editorial. This is an original measurement convention, not a client result or a provider-certified report. Every worked amount is synthetic; Akshay's personal review is not claimed. The runnable JavaScript is tested with Node.js 24 and needs no credentials, customer data or paid API.
Four takeaways
- Use one failed invoice as the unit, not one retry, webhook or subscription.
- Compare only invoices that have had the full observation window, including when some younger invoices have already paid.
- Show invoice-count and amount-weighted recovery together. Different invoice sizes make the two rates answer different questions.
- Keep late settlement, unpaid invoices and immature cohorts visible. Observed recovery is not proof of incremental impact or retained recurring revenue.
Why recovery deserves its own measurement contract
Payment recovery sits between growth operations, billing and customer experience. A team may reasonably want to know whether to investigate a broken payment-update journey, change a messaging sequence or evaluate a recovery provider. Those decisions require a trustworthy description before a causal argument. Today's public founder discussions about failed renewals and missed invoices are qualitative evidence that this is an active operational problem, not evidence of keyword volume or the size of its market.
Provider workflows also differ. Stripe's retry documentation distinguishes scheduled attempts from executed charges and describes cases requiring a new payment method. Paddle's recovery documentation describes a default 30-day dunning period. Both were checked on 24 September 2026; publication dates are not stated on these living pages. These are reasons to record actual configuration, not instructions to assume identical eligibility or a universal retry schedule.
This guide therefore makes no claim to reproduce either dashboard. It proposes a small, explicit, fixed-window report that a team can reconcile. Unlike an account-level GRR/NRR bridge, it follows failed invoices and settlement times. Unlike a delivery-reconciliation report, its outcome is not API acceptance. Keeping these questions separate prevents a technically correct report from being used to answer the wrong commercial question.
Define the observation before selecting the rows
Choose a first-failure cohort: for example, eligible renewal invoices whose first qualifying failure occurred during August. Freeze that entry rule before examining which invoices eventually settle. An invoice stays in its original cohort if it fails again, its customer changes a payment method, or it is retried by another system. New invoices from the same subscription are separate units. If the decision is about customers rather than invoices, build a separate account-grain report rather than calling invoice recovery customer retention.
Record the invoice type, payment provider, currency, extraction cutoff and observation window W. Renewal failures and first paid invoices after trials may represent different customer situations; keep them separate unless the intended decision explicitly combines them. Record whether the balance includes tax and what was outstanding at the first failure. The calculator freezes that balance as the weighting amount. It is not an accounting revenue-recognition engine.
Use a precise timestamp convention. Let f be the first-failure instant, c the extraction cutoff and p the first qualifying full-settlement instant. Then age is (c − f)/86,400,000 in elapsed 24-hour days. Recovery time is (p − f)/86,400,000. A missing p means confirmed no qualifying settlement by cutoff, not merely that a join failed. Normalize timestamps to explicit UTC offsets and reject p before f or after c. The window endpoint is inclusive: a payment exactly at f + W days qualifies.
Do not convert elapsed time to local calendar dates or round 30.01 days to 30. The former can cross daylight-saving boundaries; the latter turns a late payment into an on-time one. If source precision is only daily, document that limitation and use a separate day-grain convention consistently. More decimal places do not create more precise source evidence.
A minimal ledger that survives retries
Maintain one frozen observation row per invoice and preserve the underlying event history separately. The minimum working fields are an anonymised invoice key, currency, amount outstanding at first failure, first-failure timestamp, full-settlement timestamp or an explicit unpaid state, and extraction cutoff. Store a definition version alongside the export. In the compact calculator interface, timestamps have already been transformed into elapsed days.
A useful ingestion boundary has three layers. The raw event layer retains provider event IDs and arrival timestamps inside approved storage. A normalization layer maps provider objects into the team's invoice and settlement definitions. A cohort layer selects one observation per invoice as of a cutoff. Event arrival order should not silently become business-event order. A late-arriving record can require a restated report, with both the old cutoff and new extraction version visible.
Use invoice identity to deduplicate observations and event identity to deduplicate deliveries; they solve different problems. Dropping every repeated invoice event would lose the eventual settlement. Keeping every repeated event as an invoice would inflate the denominator. Reconciliation should therefore count unique eligible invoices and compare their frozen balances to an independently generated aggregate export.
The supplied model deliberately excludes partial settlement, credit-only closure, write-offs and net refund accounting. If those are common in your business, do not disguise them as full cash payment to make the tool accept the data. Extend the ledger into payment allocations with transaction amounts and reversal events, then define the alternative numerator. Exclusions must be defined consistently, with excluded counts and reasons reported outside this calculator—not selected after seeing a disappointing result.
The fixed-window calculation
An invoice is mature when age ≥ W. All other invoices are immature and excluded from both parts of each rate. Among mature invoices, classify three mutually exclusive states: settled within W, settled later than W but by cutoff, and not settled by cutoff. This separation preserves the difference between a late payment and an unpaid invoice without rewriting the earlier window.
Let M be the number of mature invoices and R the number settled within W. Invoice-count recovery is R/M. Let B be the sum of frozen balances for mature invoices and C the sum for those settled within W. Amount-weighted recovery is C/B. If M is zero, both rates are undefined. With positive balances, B is zero exactly when there is no mature invoice in this implementation.
Two conservation checks must hold. Mature count equals within-window count plus late count plus unpaid count. Mature balance equals the corresponding three balance buckets. Keep amounts as integer minor units to avoid accumulating decimal-currency drift. The supported convention is 100 minor units per whole unit; currencies with a different exponent need a deliberate adaptation rather than silent rounding.
Never include an immature success while excluding an immature failure. That uses knowledge of the outcome to decide who is eligible and mechanically favours winners. A mature-only report trades freshness for equal follow-up. Show the excluded count and amount so readers understand how much recent activity is not yet represented. If you need a time-to-recovery analysis with censoring, use a separately validated survival method; this calculator does not implement one.
Reproduce the synthetic example in JavaScript
Use W = 30 days. Four invoices are 35 days old: A has amount 100 and settles on day 3; B has amount 200 and settles on day 20; C has amount 100 and remains unpaid; D has amount 600 and settles on day 32. E has amount 100, settles on day 2, but is only 10 days old at extraction. E must remain outside the comparison even though its outcome is already favourable.
Mature count is four and mature balance is 1,000. Two invoices, totalling 300, settled within the window. The rates are therefore 50% by invoice and 30% by amount. Late balance is 600 and unpaid balance is 100. Immature balance is 100. A report claiming 90% amount recovery within 30 days has incorrectly included D; a report using E only because it paid has selected on outcome.
The following dependency-free reference function consumes already validated observations in integer minor units. Null explicitly means no qualifying settlement at cutoff. Use the companion calculator for CSV validation, row limits and aggregate export. The test suite executes this exact article example and checks its result against the production function.
function summarizeRecovery(rows, windowDays) {
const mature = rows.filter(row => row.ageDays >= windowDays);
const within = mature.filter(row =>
row.recoveredDay !== null && row.recoveredDay <= windowDays);
const sum = items => items.reduce((total, row) => total + row.amountMinor, 0);
const eligibleMinor = sum(mature);
const recoveredMinor = sum(within);
return {
matureCount: mature.length,
recoveredCount: within.length,
eligibleMinor,
recoveredMinor,
countRate: mature.length ? within.length / mature.length : null,
amountRate: eligibleMinor ? recoveredMinor / eligibleMinor : null
};
}
const rows = [
{ amountMinor: 10000, ageDays: 35, recoveredDay: 3 },
{ amountMinor: 20000, ageDays: 35, recoveredDay: 20 },
{ amountMinor: 10000, ageDays: 35, recoveredDay: null },
{ amountMinor: 60000, ageDays: 35, recoveredDay: 32 },
{ amountMinor: 10000, ageDays: 10, recoveredDay: 2 }
];
console.log(summarizeRecovery(rows, 30));
// matureCount 4; recoveredCount 2; eligibleMinor 100000;
// recoveredMinor 30000; countRate 0.5; amountRate 0.3
Transform timestamps without moving the boundary
The reference helper below requires ISO UTC timestamps ending in Z. Validate provider payloads and calendar dates at ingestion; Date.parse is not a schema validator. Subtracting instants rather than local dates avoids daylight-saving day-length differences. Reject a settlement beyond the extraction cutoff even if that later payment is known when you rerun the report. Otherwise you are leaking future evidence into a historical snapshot.
function elapsedObservation(firstFailure, cutoff, paidAt = null) {
const toMs = value => {
if (typeof value !== 'string' || !value.endsWith('Z'))
throw new Error('Expected an explicit UTC ISO timestamp');
const n = Date.parse(value);
if (!Number.isFinite(n)) throw new Error('Invalid timestamp');
return n;
};
const first = toMs(firstFailure), end = toMs(cutoff);
const paid = paidAt === null ? null : toMs(paidAt);
if (end < first || (paid !== null && (paid < first || paid > end)))
throw new Error('Inconsistent event order');
return {
ageDays: (end - first) / 86400000,
recoveredDay: paid === null ? null : (paid - first) / 86400000
};
}
console.log(elapsedObservation(
'2026-08-01T00:00:00Z', '2026-09-05T00:00:00Z',
'2026-08-31T00:00:00Z'
));
// { ageDays: 35, recoveredDay: 30 }
Validation and operational acceptance
Begin with known answers before connecting an export. Test the five-invoice fixture, no mature invoices, all unpaid, all recovered, recovery at day zero, exactly at W and just after W. Empty inputs are errors; a documented unpaid marker is data. Negative balances, duplicate invoice keys, non-finite numbers and recovery after the cutoff must fail rather than quietly disappear. Test the maximum accepted rows and amounts so integer totals remain within JavaScript's safe range.
Add invariants, not just examples. Both rates must remain between zero and one. Reordering rows must not change results. Multiplying every balance by the same positive factor must not change either rate. Adding an immature invoice must not change the mature rates. Increasing W on a dataset where all invoices are mature can only add within-window settlements; on a mixed-age dataset, cohort membership changes, so that monotonic comparison is not valid without freezing the population.
Operational acceptance needs more than a green unit test. Confirm that the source export includes all eligible failures, that missing joined timestamps are distinguished from confirmed unpaid states, and that excluded categories are logged. Assign an owner for late-arriving corrections and version every restatement. Compare source totals before discussing a policy change; otherwise a cleaner dashboard may only be hiding omitted invoices.
Turn the report into a bounded decision
Suppose count recovery rises while amount recovery falls. Start with invoice-size composition and large unresolved balances, not an automatic conclusion about email quality. Suppose both rates fall after adding a new market: separate currency, invoice type and payment-method eligibility before attributing the difference to a provider. The report describes outcomes under a contract; it does not identify the cause.
A before-and-after rise can reflect customer mix, seasonality, changing balances, a different cutoff or manual recovery work. To estimate incremental effect, design an appropriate comparison and define the intervention unit, contamination risks and customer guardrails. Recovering one invoice also does not establish continuing subscription retention. Track later renewal separately, and evaluate contribution after relevant costs in a different economic model.
Use the recovery review framework to freeze definitions and assign ownership, the fixed-window calculator to reproduce the bridge, and the evidence-review prompt to challenge the proposed interpretation. The useful outcome is a smaller, defensible next step—not a bigger recovery percentage produced by changing who gets counted.