Stripe webhook · Cancellation feedback

The Stripe webhook tool for cancellation feedback.

Stripe sends a webhook when a subscription is cancelled. ChurnNote listens for that event, sends an exit email from your domain, captures the reply, and groups the reason into a churn taxonomy — all without writing or maintaining a webhook handler yourself.

Quick answer

Subscribe to customer.subscription.deleted in Stripe, verify the webhook signature, look up the customer email, send a plain-text exit email from your domain, capture the reply, and group the reason into a churn taxonomy. ChurnNote does this whole pipeline natively for $12/mo — no webhook handler code to write or maintain.

What the webhook handler looks like (build-it-yourself)

If you'd rather build the pipeline yourself, here's the shape of it. ChurnNote does this for you, but the code is helpful for understanding what's happening under the hood.

// /api/webhooks/stripe
import Stripe from 'stripe'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)

export async function POST(req: Request) {
  const body = await req.text()
  const sig = req.headers.get('stripe-signature')!

  // 1. Verify the webhook
  const event = stripe.webhooks.constructEvent(
    body, sig, process.env.STRIPE_WEBHOOK_SECRET!,
  )

  if (event.type === 'customer.subscription.deleted') {
    const sub = event.data.object as Stripe.Subscription

    // 2. Look up the customer
    const customer = await stripe.customers.retrieve(sub.customer as string)

    // 3. Send the exit email (Resend / SES / Postmark)
    await sendExitEmail({
      to: (customer as Stripe.Customer).email!,
      from: 'you@yourdomain.com',
      subject: 'quick question',
      body: writePersonalEmailBody(customer, sub),
    })

    // 4. Set up inbound parser to capture the reply
    //    (out of scope for this snippet — see Resend Inbound)

    // 5. When reply lands, group the reason
    //    (this is the part most teams skip and regret)
  }

  return new Response('ok', { status: 200 })
}

What this snippet doesn't show: signature rotation, retry handling, inbound email parsing, threading replies to the original email, an AI step to group the reason, deliverability tuning, suppression handling. Those are the parts that make this a multi-week project rather than an afternoon.

What ChurnNote handles for you

  1. 1

    Subscribe to customer.subscription.deleted

    In your Stripe dashboard, register a webhook endpoint and select the customer.subscription.deleted event. Stripe will POST to your endpoint every time a subscription is cancelled.

  2. 2

    Verify the webhook signature

    Stripe signs every webhook with your webhook secret. Use stripe.webhooks.constructEvent(body, sig, secret) to verify the payload before processing — never trust unsigned requests.

  3. 3

    Look up the customer email

    The webhook gives you a subscription object with a customer ID. Call stripe.customers.retrieve(id) to get the email and any metadata you stored on the customer.

  4. 4

    Send the cancellation email

    Use a transactional sender (Resend, SES, Postmark, SendGrid) to send a plain-text email from your domain. Keep it under five lines, ask one open question, and use your real reply-to address.

  5. 5

    Capture the reply

    Set up an inbound email parser (Resend Inbound, SendGrid Inbound Parse, or a forwarding rule into a webhook) so replies get ingested back into your system, not just sitting in your inbox.

  6. 6

    Group the reason and store it

    Tag each reply by reason — pricing, missing feature, too complex, switched tool, bad experience, no longer needed — and store it on the customer record alongside their LTV and tenure. The tag drives win-back targeting later.

Steps 1–6 are what ChurnNote does the moment you connect Stripe. You don't write any handler code; you just paste an API key.

FAQ

Which Stripe webhook fires when a customer cancels?
customer.subscription.deleted fires when a Stripe subscription is cancelled — whether the customer cancelled via the Customer Portal, your own cancel UI, or your dashboard. customer.subscription.updated also fires before that (with cancel_at_period_end = true) if the cancellation is scheduled for end-of-period rather than immediate.
What's the difference between customer.subscription.deleted and customer.subscription.updated?
customer.subscription.updated fires whenever any subscription field changes — including when a customer schedules a cancellation for end-of-period (cancel_at_period_end = true). customer.subscription.deleted fires only when the subscription actually ends. For cancellation feedback, listen to deleted (the customer is gone, ask why now) — not updated (they may still come back during the billing period).
Do I need to write a webhook handler myself to collect cancellation feedback?
No, if you use ChurnNote — it subscribes to the webhook on your account, handles the entire pipeline (event verification, email lookup, sender, inbound parsing, reason grouping), and gives you a clean dashboard. Yes, if you want to build it yourself with Stripe's webhook + a transactional sender like Resend or Postmark.
Can I use Zapier or Make instead of code?
Technically yes — you can wire Stripe webhook → Zapier → Gmail/Resend → email send. The catch is reply capture and reason grouping; Zapier can fire the email but won't ingest replies, parse them, or group the reasons. You'll see who left but not why. ChurnNote does the whole loop end-to-end.
How fast does the email get sent after cancellation?
With a properly configured webhook, the email is sent within seconds. Stripe fires the event immediately upon cancellation; ChurnNote queues and sends the email through Resend in real time. The customer is still in the moment, which is why reply rates are high.
What about customer.subscription.deleted from failed payments?
Good question — Stripe fires this event for both voluntary cancellations and final payment failures. ChurnNote distinguishes the two: failed-payment-driven deletions are routed into the dunning recovery flow (3-email retry sequence), while voluntary cancellations get the exit email + reason capture.
What other Stripe webhooks does ChurnNote subscribe to?
Three: customer.subscription.deleted (cancellations → exit email), invoice.payment_failed (failed payment → dunning sequence), and customer.subscription.updated (catches scheduled cancellations early so you can pre-emptively email when needed). Setup is a single API key — ChurnNote registers the webhook for you.

Skip the webhook handler. Get the feedback.

ChurnNote subscribes to the Stripe webhook on your account, runs the whole pipeline, and gives you a clean dashboard. $12/mo flat.

Get started