---
id: saas/inadimplencia-dunning
industry: SaaS and technology
usage: specific
effort: medium
grain: Payment attempt (event), subscription (queue) and customer x month (snapshot)
---

# Subscription delinquency and dunning

How much MRR is stuck in collections, how much was recovered within the cycle and which subscriptions the team needs to chase today, without counting an attempt as debt.

## Executive summary

**For:** SaaS companies with recurring billing that lose customers to payment failures without noticing.

**What it solves**

- Involuntary churn separated from voluntary, with each month's recovery rate.
- A daily queue of subscriptions with money stuck, with reason and next action.
- Delinquency on the reference date that matches finance.

**Why it matters:** Payment failure is the quietest kind of churn: the card is declined, billing retries, gives up and cancels, and nobody talks to the customer. Billing only shows the invoice's current status, not how long it was stuck or whether it was recovered. Measuring recovery by cohort and working a daily queue brings back revenue that was already sold.

**Business questions it answers**

- How much MRR is stuck in collections today?
- What is the recovery rate for August's payment failures?
- Which subscriptions need a new card and which only need a reminder?
- Who is delinquent today and for how many days?
- Which decline reasons most often end in cancellation?
- How much of this month's churn was involuntary?

**What you need**

- Billing connected to Nekt with invoices, payment attempts and subscriptions
- The collections policy: how many attempts, when access is blocked and when it cancels
- The gateway's decline codes mapped to categories with an action

**What you get**

- A table of failure and recovery events, one row per attempt
- Monthly recovery rate by cohort, with each month's maturity
- A daily collections queue by subscription, prioritized
- An AI agent that answers who owes, how much and since when

## Detailed

### The problem

Every declined card attempt becomes a row, and the same invoice can have several before it is paid. Whoever counts rows multiplies delinquency; whoever looks only at current status doesn't see the invoice that failed and was paid days later, nor the one voided at cancellation. The result is involuntary churn mixed with voluntary churn and a CS team that discovers the failure after the customer has already left.

### Business rules

1. **An attempt is an event, the debt is the invoice.** Keep attempts in an events table, one row per attempt, with date, reason and gateway. Delinquency, recovery and the queue count at the invoice or subscription grain. Automatic retries, duplicate slips and reissues are the same debt. (Avoid: Counting rows in the declines table as debts.)
2. **Recovered means paid within one cycle of the first failure.** The month's cohort is the subscriptions with at least one failure in the month, anchored on the month's first failure. Recovered is one with a successful payment within one billing cycle from it: about 30 days monthly, 90 quarterly, one year annual. Every billing interval goes in, each with its own window. In a real base, recovery in closed months sat near half. (Avoid: Calling recovered only the invoice paid the same week, or looking at today's status.)
3. **A recent month is still maturing.** In recent months, the one-cycle window is still open. Flag the month as maturing until the window closes (monthly: end of month plus 31 days) and never read the low rate of an open month as a decline. A weekly reading with a short window is a different indicator and doesn't compare with the monthly one. (Avoid: Comparing the current month with closed months.)
4. **The decline reason decides the action.** Normalize each gateway's decline codes into a single table, with category and whether a retry is worth it. Invalid number, CVC or expiry (incorrect_number, incorrect_cvc, invalid_expiry_month, invalid_expiry_year in Stripe) need a new card; a generic issuer decline or insufficient funds need a retry or a reminder. Codes that arrive with and without a leading zero are the same. A code missing from the table shows as unmapped, never as other, and someone reviews it every week. (Avoid: Treating every decline the same, or grouping the unknown into other.)
5. **The queue is for those nobody is chasing.** The queue takes live subscriptions with an overdue open invoice and no retry scheduled by billing (next_payment_attempt empty in Stripe), plus those canceled by billing in the last 30 days whose last attempt failed and nothing was paid afterwards. While billing is still retrying, it sends its own notice: don't duplicate the contact. The queue is a snapshot, so check the invoice live before chasing. (Avoid: Sending the whole list of open invoices to the team.)
6. **Priority by recency and reason.** A simple ladder works: up to 14 days from the failure, recover now; invalid card, ask for a new card; up to 60 days, resend the charge with the invoice link; beyond that, likely loss. Count days from the due date of the oldest open invoice, not from the last payment, which includes the cycle already paid. Sort by MRR within each priority. (Avoid: Sorting the queue by invoice creation date.)
7. **Official delinquency is the snapshot on the reference date.** Delinquent is whoever has a non-canceled charge that is overdue and unpaid on the reference date: month end for closed months, today for the current month. Due today doesn't count yet. There is no grace period: one day late already counts, and severity comes from the days-late bucket. Having been late at some point in the month is a different, historical-only metric. Delinquent is not inactive: the customer stays in the base until cancellation. (Avoid: Flagging as delinquent someone who was late on some day of the month and already paid.)
8. **Past_due stays in MRR, flagged.** A past_due subscription stays in MRR flagged as in collections until it becomes canceled or unpaid. Removing it and adding it back when paid creates churn and reactivation that never happened. If the company decides to remove it after a grace period (for example, 7 days), the rule is written and the same in every month. (Avoid: Removing past_due from MRR and adding it back the next month.)
9. **Involuntary churn only after the window.** A cancellation by billing, or by the team under a delinquency policy, after failures with no payment is involuntary churn. Declare churn only after the recovery window, at the MRR from before the failure. A common policy is to cut off whoever has two or more overdue bills, the oldest more than 60 days late. (Avoid: Counting churn on the day the card is declined.)
10. **Access blocks need history.** Billing and product only store whether the customer is blocked now. Record block and unblock events, via webhook or a daily snapshot, to know how long each customer stayed blocked and whether they paid afterwards. Blocking after a deadline is an operational decision, not automatic: don't infer a block from days late. (Avoid: Assuming every customer more than 30 days late is blocked.)

### Collections queue: priority and action

Days counted from the due date of the subscription's oldest open invoice.

- **failure up to 14 days ago** → P1, recover now (`reminder with the invoice link`)
- **invalid number, CVC or expiry** → P2, ask for a new card (`incorrect_number, incorrect_cvc, invalid_expiry_*`)
- **failure between 15 and 60 days ago** → P2, resend the charge
- **more than 60 days without payment** → P3, likely loss
- **canceled by billing up to 30 days ago, no payment since** → Recovery window
- **billing still retrying** → Out of the queue (`next_payment_attempt filled in`)

### Definitions

| Metric | Definition |
|---|---|
| Monthly failure cohort | subscriptions with at least one declined payment attempt in the month, anchored on the month's first failure |
| Recovery rate | cohort subscriptions with a successful payment within one cycle of the first failure / cohort subscriptions |
| MRR in collections | MRR of active subscriptions with an overdue open invoice, on the reference date |
| Delinquent on the date | customer with any non-canceled charge overdue and unpaid on the reference date, with no grace period |
| Days late | reference date minus the due date of the oldest open invoice |
| Involuntary churn | prior-month MRR of subscriptions canceled after a payment failure with no later payment, declared after the recovery window |
| First-attempt approval | auto-charged invoices paid on the first attempt / auto-charged invoices in the month |

### Pitfalls

- **Declines that multiplied the debt.** A dashboard counted rows in the declines table, and the same sale appeared once per retry. Delinquency looked several times larger than it was. How to avoid: Count invoices and subscriptions; attempts only as events.
- **An open month read as a drop.** The current month's recovery showed well below average and became an alarm, but that month's one-cycle window was still open. How to avoid: Store maturity on the row and compare only settled months.
- **The account vanished along with the debt.** The customer view only showed accounts with MRR. A subscription canceled by billing for non-payment left the list exactly during the window when it could still be recovered. How to avoid: Keep subscriptions canceled by billing in the last 30 days in the queue.
- **Voided invoice, recoverable customer.** At cancellation, billing voided the open invoice. The open amount became zero, a reminder made no sense, and the case was dropped as having no debt. How to avoid: A zero open amount is valid here: approach with reactivation, not a reminder.
- **Historical flag used as current status.** A late-at-some-point-in-the-month column was used as current status and flagged as delinquent customers who had already paid. How to avoid: Use the snapshot on the reference date and label the other as historical.
- **Rate recomputed on an incomplete load.** The monthly rate ran at a fixed time and sometimes read yesterday's version of the events table, or recomputed on top of a load that had failed. How to avoid: Trigger after the load, by event, and store on the row the latest failure the source had.

Validated on real data: In Nekt's own Stripe account, invoices that had any decline carry about three declined attempts each, so counting attempts triples delinquency. And more than 8 in 10 of those invoices were paid later, which is recovery, not loss.

### Acceptance test

- [ ] Each subscription appears at most once in each month's failure cohort.
- [ ] Recovered + not recovered = cohort, and no month with an open window shows as settled.
- [ ] The queue's open amount matches the sum of open invoices in billing for the same subscriptions (difference under 1%).
- [ ] No subscription with a retry scheduled by billing is in the queue.
- [ ] Delinquency for a closed month doesn't change when the pipeline runs again.

### Context document

```text
Delinquency and dunning
- A payment attempt is an event. Delinquency, recovery and the queue count by invoice or subscription, never by attempt.
- Monthly failure cohort = subscriptions with at least one decline in the month, by the month's first failure.
- Recovered = successful payment within one cycle of the first failure: 30 days monthly, 90 quarterly, 1 year annual.
- A month with an open window is maturing. Don't compare it with settled months.
- Delinquent = non-canceled charge overdue and unpaid on the reference date (closed month end or today). Due today doesn't count. No grace period.
- Delinquent is not inactive. Past_due stays in MRR flagged as in collections.
- Involuntary churn only after the recovery window, at the MRR from before the failure.
- Collections queue: only subscriptions with no retry scheduled by billing, or canceled by billing up to 30 days ago with no payment since.
- Priority: P1 up to 14 days; P2 invalid card or 15 to 60 days; P3 more than 60 days.
- A decline code missing from the table shows as unmapped.
Per-client exceptions:
- <client>: grace period of <days> days before removing past_due from MRR.
- <client>: cut-off policy at <n> overdue bills with the oldest <days> days late.
```

### Variations

- **Boleto and Pix:** With no automatic retry, the ladder is reminders before and after the due date, and a preventive queue lists who is due in the next 7 days.
- **Collections in the ERP:** When the bill lives in the ERP, use days-late buckets and the credit-bureau flag, reducing to one row per invoice and CNPJ (company tax ID) before joining.
- **Involuntary churn forecast:** The delinquency policy becomes the notice queue, and whoever isn't under any rule gets a calibrated churn probability.
