Cancellation flows and failed-payment recovery: when someone tries to cancel it offers a pause or a discount, and it retries declined cards on a schedule that works.
Build me cancellation and dunning flows that replace Churnkey: a cancel path that offers something first, and card retries that actually recover money. STACK - Node 20+ with Fastify - SQLite through better-sqlite3, WAL mode - Your payment processor's API and webhooks — this is a layer on top of it, not a replacement for it - A worker process for the retry schedule - Caddy in front THE DATA MODEL - customers: id, external_id, email, plan, mrr_cents, currency, started_at, status - subscriptions: id, customer_id, external_id, plan, interval, amount_cents, status, current_period_end, cancel_at, cancelled_at, paused_until - cancellations: id, subscription_id, started_at, completed_at, outcome, reason_code, reason_text, offer_shown_id, offer_accepted_id — outcome is saved, deflected, cancelled, or abandoned - offers: id, name, kind, config_json, eligibility_json, max_per_customer, cooldown_days, is_active — pause, discount, plan change, extension, or a message - offer_events: id, cancellation_id, offer_id, shown_at, accepted_at, declined_at - invoices: id, subscription_id, external_id, amount_cents, currency, status, attempt_count, due_at, paid_at, failed_at, decline_code - retries: id, invoice_id, scheduled_for, attempted_at, result, decline_code, gateway_message - recoveries: id, invoice_id, kind, amount_cents, recovered_at — one row per recovered payment, so the number reported can be defended - Everything append-only. Every claim this tool makes about money it saved must be traceable to rows THE CANCEL FLOW - The customer clicks cancel and is asked why, from a short list plus a free-text box. The reason drives everything that follows - One offer, matched to the reason: too expensive gets a discount or a cheaper plan, not using it enough gets a pause, missing a feature gets a message and a note to the team, a temporary problem gets an extension - One offer, not three. A gauntlet is what makes people angry enough to write about it publicly - Accepting applies immediately through the processor and confirms in writing - Declining cancels. The cancel button is always visible, always works, and is never disguised. In several jurisdictions a cancellation that is harder than the signup is now illegal — and quite apart from that, a dark pattern buys one month and costs the reputation - Eligibility rules: never offer the same discount twice, never offer to somebody who just took one, cap the lifetime discounts per customer - Everything recorded, including the abandonment — somebody who opened the flow and closed the tab is a signal DUNNING, WHICH IS WHERE THE MONEY ACTUALLY IS - A failed payment starts a retry schedule. The schedule is the product: retry at intervals that spread across days of the week and times of day, because a card that failed on a Sunday night may work on a Tuesday morning - Read the decline code and act on it. Insufficient funds is worth retrying and worth timing near a payday; a stolen card is not worth retrying at all; an expired card needs the customer, not another attempt. Retrying a hard decline is how a processor decides you are a risk - Stop after a stated number of attempts, and stop immediately on any code that means never - Emails alongside the retries, escalating in tone but never in volume: at most one a day, at most a handful in total, each with a one-click link to update the card - The card update page needs no login: a long random token, expiring, that opens a hosted field from the processor. Every extra step here loses a percentage - A grace period during which access continues, set by you, and a clear final message before it ends TELLING THE TWO CHURNS APART - Voluntary churn is somebody choosing to leave. Involuntary churn is a card that failed. They are different problems with different fixes, and a dashboard that adds them together is useless - Report them separately, always, with the recovery rate on the involuntary side and the deflection rate on the voluntary side - Recovery rate defined and written on the page: recovered invoices over failed invoices, in a stated window, counted once each MEASURING HONESTLY - Deflection rate per offer per reason, with the sample size beside it - Revenue retained, counted as the amount actually collected afterwards, not as the annualised value of a save that churned six weeks later - A cohort view: of the people who accepted an offer, how many are still paying three and six months on. This is the number that says whether the tool is working, and it is the one nobody shows - Everything computed from rows at query time THE PROCESSOR - Webhooks consumed with signature verification, idempotency by event id, and out-of-order delivery handled - Every mutation through the processor is idempotent with a key - Reconcile nightly against the processor's own records, and alert on any disagreement — the local state is a mirror and must be treated as one OPERATIONS - .env: DATABASE_PATH, BASE_URL, PROCESSOR_KEY, WEBHOOK_SECRET, SMTP_URL, SESSION_SECRET - Migrations on boot, each once - Nightly backup off the machine, restore script - Health endpoint reporting webhook lag and the oldest unattempted retry WHAT MATTERS MOST The decline-code logic and the honest reporting. Build the retry schedule with per-code handling first — that is where the recovered money is — and make the cohort view before the offer flow looks good. A tool that claims saves it did not make is worse than no tool, because it hides the churn instead of fixing it. Give me the repository, migrations, .env.example, a seed of failed invoices with a range of decline codes, and a README with deploy steps behind Caddy and the legal note about cancellation flows.
What you lose
- Retry schedules for declined cards tuned against millions of attempts, which recovers real revenue
- Cancellation offers tested for what actually retains
- Reporting that separates involuntary churn from voluntary
If you would rather not build
- Stripe Smart Retries, which is built in
- Your own flow page plus webhooks
- Nothing, if your volume is small
What it costs
read from their page 15 Aug 2026
| Plan | Billed monthly | Billed yearly | Last read |
|---|---|---|---|
| — | $250/mo | — | 15 Aug 2026 |
Their pricing page is where these came from. Seeing a different price? Tell us.
The escape hatch
open source · no votes, no paid placement
Killbill
$0Open billing with configurable dunning and retry schedules.
killbill/killbillfree · open source
Why this verdict
our own opinion · changed only by a person
74/100
Verdict yes at 74. Dunning and a cancellation flow are ordinary; separating involuntary from voluntary churn is the reporting decision that matters.
History
tracked since 10 Aug 2026 · nothing is ever overwritten
Questions about Churnkey
answered from the record above
Is Churnkey free?
No — the plan we track is $250 a month. From around $250/month billed monthly, priced on subscriber volume.
Can you replace Churnkey by building your own?
YES. Replaceable in one session with an AI coding agent. Replacement score 74 out of 100, build time one session. Read what you lose before you decide.
How much does Churnkey cost?
$250 a month on Growth — $3,000 a year. Recorded 10 Aug 2026.
What do you lose by replacing Churnkey?
Retry schedules for declined cards tuned against millions of attempts, which recovers real revenue; Cancellation offers tested for what actually retains; Reporting that separates involuntary churn from voluntary. If any of those carry weight for you, keep paying.
Is there an open-source alternative to Churnkey?
Yes: Killbill, Listmonk. The prompt on this page is for when you want it your way instead.
Related entries
same category first, most replaced first
Every week, something stops being worth paying for.
New verdicts, prices that moved, entries added. One email a week. Unsubscribe in one click. Nothing is being sent yet — your address is kept here, and the first issue is the first thing it is used for.
free forever · no tracking pixel · stored here, never passed to anyone

