Docs

The reference manual. Two installs to activate, then everything else explained.

Getting started

ChurnNote is the reason-first retention OS for Stripe and Lemon Squeezy SaaS. Find the leak, save who you can, learn why they left, win them back when something changes.

When you're logged in, your live setup status appears below. Most setup is automatic once you connect Stripe or Lemon Squeezy — the only two things that need your hands are installing Cancel Flow and verifying your sender.

What needs your hands

  1. Install Cancel Flow — one backend call into your cancel button. Save 20–40% of cancel attempts. Setup ↓
  2. Verify your sender — start on our default sender or upgrade to your own domain or SMTP. Setup ↓

What runs automatically

  • Failed payment recovery — auto-fires the moment a webhook arrives. How it works ↓
  • AI reply intelligence — every cancellation reply and cancel-flow feedback runs through the structured extractor. How it works ↓
  • Inbound reply capture — replies to founder-style emails land back in the dashboard via Resend inbound. Works on default sender, custom domain, and custom SMTP.
  • Webhook ingestion — cancellations, scheduled cancels, failed payments, recoveries. Events ↓

Tools you reach for as needed

  • Churn Leak Report — run it any time to see 30 days of leaking MRR. Lives at /tools/churn-leak-score.
  • Win-backs — when someone churns, ChurnNote drafts a personal email. You review, then send. More ↓
  • Product-change win-backs — when you ship a feature a churned customer asked for, email them. More ↓

Connect billing

Stripe

Create a restricted API key in your Stripe dashboard with read access to customers, subscriptions, invoices, charges, and payment intents. Paste it into ChurnNote. We never store your secret in plain text — the key is encrypted with AES-256-GCM at rest, and we read it back only to call the Stripe API on your behalf.

You can revoke the key at any time from Stripe and the connection will go inactive instantly. Both test-mode and live-mode keys work. Test mode is the safest place to play with Cancel Flow before opening to real customers.

Lemon Squeezy

Click Connect Lemon Squeezyon the onboarding screen. You'll be prompted for your API key and store ID. We register webhooks automatically so cancellations and failed payments flow into ChurnNote within seconds of the event.

What works for Lemon Squeezy today: reason collection, AI reply intelligence, billing-help link, failed-payment recovery scan, product-change win-backs, native cancel-at-period-end. What soft-captures: pause, discount, downgrade (the LS APIs for these are coming later). See Lemon Squeezy specifics for full detail.

Switching businesses

ChurnNote allows one active connection per account. You can disconnect from Settings at any time and connect a different store; previous data stays archived and visible.

Cancel Flow

When a customer clicks Cancel subscription in your app, you get one last chance. Cancel Flow shows them a reason picker, a tailored save offer (pause, discount, or downgrade), then a confirm step. Most teams running it recover 20–40% of cancel attempts in the first month.

Install (5 minutes)

From Settings → Cancel Flow, click Generate API key. The key is shown once — copy it, store it as CHURNNOTE_CANCEL_FLOW_KEY in your server environment. We only store the hash; if you lose it, regenerate (which invalidates the old key).

Wire one backend call into your cancel button. When the customer clicks Cancel:

const res = await fetch("https://www.churnnote.com/api/cancel-sessions", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.CHURNNOTE_CANCEL_FLOW_KEY}`,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    provider: "stripe",
    provider_subscription_id: "sub_123",
    customer_email: "customer@example.com"
  })
});
const { cancel_url } = await res.json();
return redirect(cancel_url);

We return a cancel_url — a short-lived link to a hosted page that walks the customer through reasons, offers, and confirmation. Redirect them there.

Don't have a developer in the chair? In Settings → Cancel Flow, click Send to developer to copy a self-contained handoff message including the snippet, ready to paste into Slack or email.

Configure offers

Each cancel reason maps to a chain of save offers. Defaults work for most SaaS:

  • Too expensive → discount, fallback downgrade
  • Missing feature → feature request (notify-when-shipped), fallback pause
  • Not using enough → pause, fallback downgrade
  • Too complex → setup help, fallback founder follow-up
  • Switching to a competitor → founder follow-up, fallback discount
  • Payment issue → billing help (payment update link)
  • Something else → straight to confirm

Live mode safety

Live Stripe actions (real coupon creation, real subscription updates, real cancel-at-period-end) require explicit opt-in. Flip Enable live Stripe actions on the setup page after confirming your offers are configured the way you want. Without opt-in, save offers soft-capturethe customer's intent without mutating anything — the founder gets emailed and follows up.

Always set a fallback cancellation URL (your Stripe billing portal, Lemon Squeezy store URL, or support page) so a customer who declines all offers and clicks I still want to cancel has a clean exit. We never trap customers inside ChurnNote.

Failed payment recovery

When a payment fails (card expired, insufficient funds, network decline), Stripe or Lemon Squeezy fires a webhook. ChurnNote receives it, queues a 3-email recovery sequence over 10 days, and sends each email with a fresh, single-use payment-update link.

The links don't require login. The customer clicks, updates their card directly with Stripe or Lemon Squeezy, and their subscription reactivates. Recovery is auto-on once your connection is active.

From /recovery you can see every failed payment, the recovery status (sent, recovered, expired), and manually resend or refresh a link if a customer asks.

Stripe Smart Retries

ChurnNote is Smart-Retries-aware. We wait for Stripe's built-in retry schedule to complete before sending our emails so customers don't get duplicate messages.

Email sending

Three sending modes, listed from fastest to most professional. You can always start with the default and upgrade later — past sends keep working either way.

  1. Default sender (instant) — sends from your-name@mail.churnnote.com with your founder name in the From line. Zero setup. Good enough to start.
  2. Custom domain (10 min) — verify a domain with DNS records, send as founder@yourdomain.com through our shared Resend infrastructure. Best deliverability + your brand without leaving ChurnNote.
  3. Custom SMTP (5 min) — bring your own SMTP host. Emails leave from your own mail account, full control over deliverability, and every send shows up in your own sent folder if your provider supports it.

Every template is plain text and short. No HTML, no logos, no marketing chrome. The point is to look like a founder writing a real email, because that's what gets 10–30% reply rates instead of the 1–2% surveys get.

Configure from /settings/email.

Set up custom SMTP — send from your own mail

Custom SMTP routes every ChurnNote email through your own provider, using credentials you control. From your customer's inbox, the email looks exactly like one you wrote and sent by hand from your founder address.

Reply capture still works

This is the question everyone asks. Yes — even on custom SMTP, customer replies still land in ChurnNote. We set the Reply-To header on every outgoing email to your-name@mail.churnnote.com regardless of where the message originated. Mail clients honor Reply-To when the customer hits reply, so the reply routes to our inbound parser. You read it in the dashboard, the AI extracts the reason, and the loop closes.

Your From address looks like you. Your Reply-Tois ChurnNote. Customers don't notice; you don't lose replies.

Step 1 — Pick a provider and get SMTP credentials

Any SMTP server works. Common picks for SaaS founders, with the exact values you'll need to paste into ChurnNote:

ProviderHostPortSecurityUsernamePassword
Resendsmtp.resend.com465SSLresendYour Resend API key
Postmarksmtp.postmarkapp.com587STARTTLSServer API tokenSame as username
SendGridsmtp.sendgrid.net587STARTTLSapikeyYour SendGrid API key
Amazon SESemail-smtp.<region>.amazonaws.com587STARTTLSSES SMTP usernameSES SMTP password
Mailgunsmtp.mailgun.org587STARTTLSpostmaster@your-domainYour SMTP password
Gmail (Google Workspace)smtp.gmail.com465SSLYour Gmail addressApp password (not your login)

For Gmail you must use a 16-character app password, not your real password. Generate one at myaccount.google.com/apppasswords — your Workspace admin needs to allow it.

Step 2 — Verify your sender domain with the provider

Before you can send from founder@yourdomain.com, the provider needs to know you own that domain. Each provider walks you through adding DNS records (SPF, DKIM, sometimes DMARC). Do this in their dashboard, not ChurnNote's. Skipping verification almost always lands your emails in spam.

Quick sanity check: send a test email from the provider's own dashboard to yourself and check that it lands in the inbox, not promotions or spam. If it doesn't, fix DNS before adding credentials to ChurnNote.

Step 3 — Add credentials in ChurnNote

Go to /settings/email and switch the sending mode to Custom SMTP. Fill in:

  • SMTP host — from the table above, e.g. smtp.resend.com
  • Port — usually 587 (STARTTLS) or 465 (SSL)
  • Security — pick SSL, STARTTLS, or None to match the port
  • Username — the SMTP username for your provider (see table)
  • Password — the SMTP password or API key. We encrypt this at rest with AES-256-GCM; we never log it.
  • From name — how you appear in the customer's inbox (e.g. Alex from MakerStory)
  • From email — the address customers see, e.g. founder@yourdomain.com. Must match a verified sender at your provider.

Step 4 — Send a test email

ChurnNote requires a successful test before live sending. Click Send test email, enter your own address, and check the inbox.

  • The test will arrive from your From email with Reply-To set to your-name@mail.churnnote.com — that's correct. Replying to the test from your own inbox doesn't do anything; it's a self-test.
  • If the test passes, smtp_verified_at is recorded and real sends start using your SMTP immediately.
  • If the test fails, ChurnNote shows the SMTP error verbatim. Most common causes are below.

Troubleshooting custom SMTP

  • Authentication failed / 535 — wrong username or password. For Gmail, you must use an app password, not the regular one. For SendGrid the username is literally apikey.
  • Connection timeout — wrong port for the chosen security. Try 587 with STARTTLS or 465 with SSL. Some hosts also block outbound port 25; that's expected.
  • Sender not authorized / 550 — your provider hasn't verified the From email domain yet. Go back to Step 2.
  • Emails arrive in spam — DNS records (SPF, DKIM, DMARC) aren't fully set up at the provider. Fix at the provider; ChurnNote doesn't touch DNS.
  • Replies aren't in the dashboard — make sure the test email's Reply-To is the ChurnNote mail.churnnote.com address. If it's pointing at your own inbox, something is overriding our header — open the email's raw source and check the Reply-To: line. Email us if it's wrong.

Switching back to default sender

Toggle the sending mode back to Default sender in /settings/email. Your SMTP credentials stay stored (encrypted) but ChurnNote stops using them — useful if you want to try out the default sender for a campaign without losing your saved config.

Win-backs

When a customer cancels, ChurnNote drafts a personal win-back email using their reason, plan, and recoverability signal. The draft sits in the win-back queue. You review, edit, and send. We never auto-send win-backs — founders win back customers, software doesn't.

From /winbacksyou'll see drafts ready to send, drafts scheduled for the future (e.g. "not now" reasons), and historical sends with reply tracking.

Product-change win-backs

The killer feature: when you ship a feature a churned customer specifically asked for, ChurnNote can email everyone who'd opted in for a notification at cancel time. Write the message once, queue per-customer drafts, review, send.

Set this up from the Cancel Flow page after you have feature-request opt-ins collected.

AI reply intelligence

Every cancellation reply and every cancel-flow feedback text runs through a structured extractor that returns:

  • Primary reason — pricing, missing feature, competitor, etc. (matches dashboard taxonomy)
  • Sub-reason — a plain-English 8-word phrase, e.g. "Onboarding felt confusing"
  • Feature mentioned — specific product feature the customer wanted (or null)
  • Competitor mentioned — tool they're switching to (or null)
  • Sentiment — positive, neutral, frustrated
  • Recoverability — low, medium, high
  • Suggested action — one imperative sentence
  • Drafted reply — 3–5 sentence plain-text founder-style email, ready to copy

You see the extracted fields on every cancellation detail. Re-run the extractor any time from the same screen.

Lemon Squeezy specifics

ChurnNote is multi-provider by design. Here's what works for Lemon Squeezy today vs. coming later:

Works today

  • Cancel Flow hosted page (reasons, feedback, soft-capture save offers, native cancel-at-period-end)
  • AI reply intelligence on cancel-flow feedback and email replies
  • Billing help (returns the LS payment update URL)
  • Failed-payment recovery scan and email sequence
  • Product-change win-backs
  • Founder email notifications
  • Inbound reply capture
  • Native cancel-at-period-end via DELETE /v1/subscriptions/:id

Coming later

  • Real LS pause (no native pause API — would be implemented via "switch to free variant")
  • Real LS discount application (LS coupons API not yet wired)
  • Real LS downgrade (variant swap not yet wired)

Until those land, pause/discount/downgrade soft-capture: the customer expresses interest, the founder gets notified, the customer is routed to the fallback cancellation URL if they still want to leave. No false promises.

Webhooks & events

ChurnNote registers webhooks automatically when you connect a provider. You don't need to configure them manually.

Stripe events ingested

  • customer.subscription.deleted — cancellation
  • customer.subscription.updated — scheduled cancel, plan changes
  • invoice.payment_failed — triggers failed-payment recovery
  • invoice.payment_succeeded — confirms recovery
  • charge.dispute.created — dispute signal

Lemon Squeezy events ingested

  • subscription_cancelled
  • subscription_updated
  • subscription_payment_failed
  • subscription_payment_recovered

Reply capture is handled separately via Resend inbound webhooks. Customer replies to founder-style emails land back in the dashboard within seconds.

Slack notifications

Real-time pings when Radar-class signals fire. Off by default. Email and the weekly digest stay on regardless of whether you enable Slack.

What fires a Slack ping

  • Scheduled cancellation — a customer just clicked Cancel and the subscription is set to end at period close. Fires once per cancellation.
  • Payment failed — a Stripe invoice failed and ChurnNote opened a recovery sequence. Fires on the first failure for that invoice.

Abandoned cancel-flow and high-recoverability churn show on the Radar page but don't ping Slack in v1 (they're derived signals, not webhook events).

Setup

  1. Go to Slack incoming webhooks and create one pointed at the channel you want pings in.
  2. Copy the URL (starts with https://hooks.slack.com/services/...).
  3. Paste it into Radar → Setup → Slack notifications and click Save.
  4. Click Send test ping to confirm it arrives in the right channel.
  5. Flip Notify on Radar signals on. From now on, scheduled cancels and failed payments fire a Slack message in real time.

ChurnNote validates the URL must be on hooks.slack.comover https. Anything else is rejected at save time so a typo can't leak data to the wrong endpoint.

Churn Radar

Radar surfaces customers at risk of churning so you can act before the cancel button gets clicked. Each signal is computed from existing data — most need no setup beyond connecting your billing provider. The Inactivity-class signals additionally need the activity pixel.

Radar is strictly forward-looking: it shows still-subscribed customers showing risk so you can act before they cancel. Customers who've already cancelled live on /dashboard, where high-recoverability ones get a green RECOVERABLE badge so they stand out in the cancellation list.

Today Radar runs on five signals. Three more are planned and require infrastructure that isn't shipped yet — they're honestly listed so you know what's coming.

Active signals (still-subscribed customers at risk)

  • Scheduled cancellation (next 14 days) — customers whose subscription is set to cancel at period end. Reach out before the sub actually ends.
  • Abandoned cancel flow — customers who started cancel flow, picked a reason, then walked away mid-flow. Real intent signal, often saveable.
  • Failed payment, not recovered — recovery sequence is firing on real failed payments. Watch for lapses.
  • Drop in usage — previously engaged customers (3+ sessions, 7+ days tenure) who've gone quiet 5-13 days. Early warning before they hit Inactive.
  • Inactive customers (14+ days) — customers your app hasn't seen in two weeks. Lapsing usage usually precedes cancellation.

Already cancelled? On the Dashboard.

When a customer cancels and the AI tags them as high recoverability, they show up on /dashboard with a bold green RECOVERABLE pill on their cancellation row. The pill drops when you click Acknowledge in Requests, mark them unrecoverable, or they reply on their own — same handling rules as before. Slack pings (if enabled) still fire in real time the moment the AI tags a fresh cancellation.

Planned signals

These are real product roadmap items, not aspirational fluff. They're held until the infrastructure they need exists.

  • Trial signals — customers stuck in onboarding or running out of trial without engagement. Needs: ChurnNote ingesting Stripe subscriptions with status=trialing + trial_end, which is a separate sync we don't do today (we currently only ingest cancellations + failed payments). Half-day of build when prioritized.
  • Per-feature inactivity — customers who stopped using your key feature even though they're still logging in. Needs: the activity pixel extended to send event (not just email), plus UI to define which features count. About a day of build.
  • Plan-fit mismatch — customers on an Enterprise plan whose actual usage looks like a Hobby plan, or vice versa. Needs: per-feature usage tracking + plan metadata sync. Bigger build.

Notifications

Slack pings (real-time on three signals, daily digest on the rest) are configured under Settings → Slack. Off by default; opt in by pasting a Slack incoming-webhook URL.

The weekly digest email already covers your Radar signals separately — Slack is the live channel.

Activity tracking

A lightweight timestamp ping. When the pixel runs in a customer's browser, it tells ChurnNote “this email was here at this time.” That's it — no URLs, no behaviour tracking, no session recordings. We use those timestamps to flag customers who've gone silent before they actually cancel.

Where to install it

One post-login page. Pick something every active user hits — your main dashboard, home, project list. Just one place is enough to power the Inactive customers and Drop in usage signals.

Don't put it on:

  • The login page itself — they're not authenticated yet, so the email field will be empty and the ping will be skipped.
  • Marketing or logged-out pages — no email to pass in.
  • Every single internal page — overkill. Inflates ping counts without improving signal accuracy.

If you want more accurate “is this customer actively using my product” data, fire it on a few key feature pages too. But for the inactivity / drop signals to work, one post-login page is the floor.

What we track

  • Customer email — so we can match them to their cancellation later if they churn.
  • Timestamp of each ping — drives the “last active” signal.
  • Count of total pings per customer — used to filter out one-and-done visitors from the Drop in usage signal (need ≥3 pings to qualify as “previously engaged”).
  • User agent — captured for debugging.

What we don't track

  • No URLs, page paths, or referrers.
  • No click behaviour, scroll depth, or session recordings.
  • No additional PII beyond the email you choose to pass in.

If you want full analytics, install Mixpanel or PostHog. ChurnNote is the minimum tracking needed to flag inactivity — nothing more.

Install

Grab the snippet pre-filled with your connection id from Settings → Activity tracking. Two versions available: vanilla JavaScript for any framework, and a React/Next.js component for direct drop-in.

Auth model

Public endpoint. No secret needed — the site key is your unguessable connection id (a UUID). Safe to ship to the browser. Spam doesn't expose data; it only bumps existing customers to more active, which silences a churn signal rather than creating false alarms.

Tuning

Default inactivity threshold is 14 days. Drop in usage fires at 5-13 days quiet for customers with ≥3 pings and ≥7 days of tenure. We skip brand-new pings (<3 days old) so the signal isn't noisy on fresh installs. Thresholds live in src/lib/radar.ts if you need to adjust them.

Troubleshooting

Cancel Flow shows "Needs fallback URL"

Your connection is in live Stripe mode without the live-actions opt-in, and no fallback URL is set. Add a fallback URL (your Stripe billing portal or support page) or enable live actions in Settings → Cancel Flow. Lemon Squeezy connections don't need a fallback — native cancel works.

I generated an API key and lost it

We only store the hash. Regenerate from Settings → Cancel Flow (this invalidates the old key), update CHURNNOTE_CANCEL_FLOW_KEYon your server, and you're back in business.

Custom SMTP emails aren't arriving

ChurnNote requires a successful test email before live SMTP sending. From /settings/email, send a test to yourself. Check spam, check your SMTP logs. If the test passes, real sends will too.

Webhook says "invalid signature"

Re-register the webhook from Settings → Integrations. Most often this means the webhook secret rotated on the provider side and ours is stale.

Something else

Email makerstory.dev@gmail.com with your account email and a short description. Replies within 24 hours.

Ready to set up? Open the dashboard →