+1
Revenue

Failed payment and involuntary churn audit

Separate customers who chose to leave from cards that simply declined: failed payments, declined renewals and involuntary churn, and where in the dunning and retry sequence the recovery is being lost. Covers involuntary churn share, first-attempt failure rate, payment recovery rate, decline codes and which accounts are sitting in dunning right now.

  • +2
    How much of last month's churn was failed payments, not cancellations?
  • +2
    Which customers are in dunning right now and how much MRR is at risk?
  • +2
    What's our failed payment recovery rate?

The playbook

Churn reported as a single number silently merges two unrelated businesses: customers who decided to leave, and customers who still want the product but whose card failed. This audit never recomputes the MRR bridge. It sits underneath it and asks where in the retry sequence the money stopped moving, which decline codes caused it, and which named accounts are still recoverable today.

Steps

  1. Discover the billing surface before measuring anything. Establish which provider is authoritative, then learn this account's own vocabulary. Call stripe.list_prices with type: "recurring" and stripe.list_products to build a price to plan-name map, because nickname is frequently null and plan identity often lives in product metadata. Call polar.get_metrics with a one month window to see which of the 55 metric slugs this workspace actually populates. Do not assume status values are in use: pull the status distribution first, since the terminal state after retries is a merchant setting.

  2. Pull the live dunning pool and price it. stripe.list_subscriptions with status: "past_due", limit: 100, paging on starting_after until exhausted, then repeat for status: "unpaid" and status: "paused". On Polar use polar.list_subscriptions with status: ["past_due", "unpaid"]. On Dodo use dodo_payments.list_subscriptions with status: "on_hold", the documented failed-renewal state, plus status: "failed". Sum monthly-normalised amount per currency: divide annual by 12 and quarterly by the interval_count months.

  3. Compute the first-attempt failure rate over renewals only. Stripe's definition is the percentage of subscription payment volume that failed on the first attempt, and its scope note is explicit: recurring subscription payments only, excluding the first invoice payment following a trial (docs.stripe.com/billing/revenue-recovery/recovery-analytics). Pull stripe.list_invoices with status: "open" and created_gte / created_lte covering the period plus a 60 day lookback, then again with status: "uncollectible" for the written-off ones. Keep attempt_count, next_payment_attempt, subscription, total, currency and billing_reason, and drop first invoices. Report failure rate by volume and by count, because they diverge when failures concentrate in large accounts.

  4. Compute the recovery rate with an in-recovery carve-out. Recovery rate is volume recovered by any means after a failure, divided by volume that failed. Payments still inside the retry window are neither recovered nor lost, so exclude them from the denominator and show them as a separate "in recovery" line. Stripe's recommended default retry policy is 8 tries within 2 weeks, with custom schedules capped at 3 retries, so compare the observed attempt_count distribution and retry span against that. Price the gap as (0.55 minus your observed rate) multiplied by failed MRR, using Stripe's published 55% average recovery figure.

  5. Date the realised involuntary churn correctly. stripe.list_subscriptions has no canceled_at filter. Its own tool description suggests pairing status: "canceled" with a canceled_at range, but the input schema accepts only created_gte and created_lte, so that instruction cannot be followed and attempting it will not filter anything. Use stripe.search with resource: "subscriptions" and a query such as status:"canceled" AND created>..., paging the synthetic _next_page row, then filter on canceled_at and ended_at client side. A termination with no successful final payment and no explicit cancel action is involuntary; a termination with cancellation_details populated is voluntary. On Polar read the per-reason cancellation metrics directly through polar.get_metrics.

  6. Segment decline reasons, then check whether the customer is still there. Use stripe.list_payment_intents over the window and read last_payment_error.decline_code, rather than stripe.list_charges, which has no status filter and would mean paging the whole charge volume. dodo_payments.list_payments with status: "failed" returns error_code directly and is the best decline surface of the three; polar.list_payments covers the Polar case. Split retryable soft declines (insufficient funds, generic decline, do not honor, processing errors) from Stripe's published hard decline list: incorrect_number, lost_card, pickup_card, stolen_card, revocation_of_authorization, revocation_of_all_authorizations, authentication_required, highest_risk_level, transaction_not_allowed. Then join the dunning pool to recent product activity with posthog.query or a mirrored billing table via postgres.query after calling postgres.get_schema. An account in dunning that still logs in daily is recoverable; one dark for 30 days is voluntary churn wearing a card-decline costume.

  7. Report. One row per month with columns: month, renewal volume attempted, first-attempt failed volume, failure rate by volume, failure rate by count, recovered volume, recovery rate, volume still in recovery, written-off volume, involuntary churn rate, voluntary churn rate, involuntary share of total churn. Add a second table of the top accounts in dunning now: customer, plan, monthly amount, currency, status, attempt count, last decline code, days since last product activity. Then deliver one judgement: whether the loss is a card-capture problem (hard declines dominate, fix account updater and pre-dunning) or a retry-timing problem (soft declines dominate, fix the retry schedule), with the priced gap against the 55% benchmark.

Gotchas

  • Minor currency units, exactly once. Stripe, Polar and Dodo all return integer amounts in the currency's smallest unit. Divide by 100 once, never twice, and never for zero-decimal currencies such as JPY and KRW, where dividing understates by 100x. Never sum across currencies: only dodo_payments.list_ledger_entries exposes a usd_equivalent_amount, so for Stripe and Polar report per currency or state the FX source and date.
  • The canceled_at filter does not exist. stripe.list_subscriptions documents a status plus canceled_at range pattern that its schema does not support. Passing it does nothing and the result silently covers subscriptions by creation date instead, which looks plausible and is wrong. All churn-timing slices must go through stripe.search or be filtered client side on canceled_at and ended_at.
  • past_due, unpaid, canceled and incomplete_expired are config artefacts. Stripe's terminal behaviour after exhausted retries is a merchant setting: cancel, mark unpaid, leave past_due, or pause. The same reality therefore appears as three different statuses across two accounts. incomplete_expired is separate again: Stripe moves a subscription there if the first invoice is unpaid within 23 hours, and it is terminal. Read the distribution before interpreting any single status, and note that Dodo spells it cancelled with two Ls where Stripe spells it canceled.
  • attempt_count rises without a new charge, and one invoice spawns many charges. On a hard decline Stripe keeps scheduling retries and keeps incrementing attempt_count, but the payment only executes once a new payment method exists, so no Charge object is created. Counting charges undercounts attempts and counting attempt_count overcounts real attempts. Use failed invoices for revenue at risk and failed attempts for operational volume, and never add the two.
  • The retry window straddles the month cut, and the cut itself is UTC. The in-recovery pool means current-month and previous-month recovery rates stay provisional for up to two months, so label them. Stripe filters are Unix epoch seconds in UTC, Polar accepts an IANA timezone on polar.get_metrics, and Dodo takes ISO timestamps. A UTC month boundary reshuffles which failures land in which month for a US-based team.
  • Trial first charges are a different population. Stripe's recovery analytics deliberately exclude the first invoice after a trial, so including it here turns a trial or card-capture problem into a fake dunning problem. Keep that cohort out and send it to the trial-conversion-audit playbook.
  • Test data and invisible pauses. Filter Stripe objects on livemode and drop anything with a non-null test_clock, because dunning is the most test-clocked part of a billing integration. Separately, pause_collection leaves the subscription status unchanged, so a paused-collection account looks active while generating nothing. India-issued cards are never auto-retried by Stripe, so an India-heavy base carries a structurally higher unrecovered rate that no retry config fixes.

Sequel CLI

Install Sequel skills into your agent

One command connects your agent to Sequel and installs the Sequel skill, so it knows this playbook exists and reads it when a question matches. The CLI signs you in, provisions a scoped API key and writes the config for you.

Already have an MCP client?

https://api.sequel.sh/mcp

Point it at this URL and sign in when prompted, or send an API key from Settings as a Bearer token. Skills come with it; nothing else to install. Manual setup per client

  1. 1

    Install the Sequel CLI

    One line installs the latest CLI with whatever package manager you have.

    curl -fsSL https://sequel.sh/install | sh
  2. 2

    Sign in

    Authenticate in your browser and pick an organization.

    sequel login
  3. 3

    Install into your agent

    Writes the MCP config and installs the Sequel skill file for agents that support skills. Pick an agent from the list, or target one directly by its slug.

    sequel install
    • Claude Code
      sequel install claude-code
    • Claude
      sequel install claude
    • Cursor
      sequel install cursor
    • VS Code
      sequel install vscode
    • Windsurf
      sequel install windsurf
    • Zed
      sequel install zed
    • Codex
      sequel install codex
    • OpenClaw
      sequel install openclaw
    • Hermes
      sequel install hermes

FAQ

Frequently asked questions

What is involuntary churn and how is it different from voluntary churn?
Voluntary churn is a customer taking an action to cancel. Involuntary churn is a subscription lapsing because a renewal payment failed and was never recovered, usually an expired, replaced or over-limit card. Recurly's July 2026 network data puts SaaS total churn at 3.22% split 2.16% voluntary and 1.06% involuntary, so roughly a third of SaaS churn is a collections problem rather than a product problem.
What percentage of churn is normally involuntary?
Per Recurly's published churn benchmark table (July 2026), involuntary churn is 1.06 points of 3.22% total SaaS churn, which is 33% of the total, and 1.25 of 3.60% across all industries, which is 35%. Recurly labels these median annual rates and does not disclose sample size. Treat the widely quoted 20 to 40% range as folklore and cite the table instead.
How do I calculate failed payment recovery rate?
Stripe defines recovery rate as the percentage of subscription payment volume successfully recovered by any means after a failure. Exclude payments still inside the retry window from the denominator, because they are neither recovered nor lost yet. Stripe states that businesses using Stripe recover 55% of failed payments on average (stripe.com/billing), so the gap between your rate and 55% multiplied by failed MRR prices the opportunity.
Why do failed payments not show up as churn in my dashboard?
Because the subscription usually sits in past_due or unpaid for weeks before it terminates, and whether it ends as canceled, unpaid or lingering past_due is a merchant setting rather than a fact about the customer. Stripe's own revenue recovery analytics also exclude the first invoice after a trial, so trial conversion failures never appear in the dunning view at all.
Can an AI agent audit involuntary churn and failed payment recovery?
Yes. With Stripe, Polar or Dodo Payments connected the agent builds the renewal population, computes first-attempt failure rate and recovery rate on the provider's own definitions, segments decline codes into retryable and hard declines, and ranks the accounts currently in dunning by MRR at risk. Sequel provides the Stripe, Polar, Dodo Payments and PostHog connections over MCP and the involuntary-churn-recovery playbook.

Put this playbook to work

Connect a source, ask the question, and the agent follows these steps. Free to start.