DEV Community

Ale
Ale

Posted on

I replaced Stripe's dunning emails with SMS — here's the architecture and why it recovers 2x more revenue

The problem nobody talks about

If you're running a subscription SaaS on Stripe, you probably have a silent revenue leak: involuntary churn.

That's when customers churn not because they want to, but because their card expired, got declined, or hit a spending limit. Stripe retries the charge a few times, sends some dunning emails, and if nothing works — subscription canceled.

The numbers are brutal:

  • 20-40% of all subscription churn is involuntary
  • Stripe's dunning emails have a ~20% open rate
  • Most of those emails land in Promotions or Spam

I kept seeing this across every project I worked on and decided to fix it.

Why SMS beats email for payment recovery

The insight is simple: people don't check email in real-time, but they check texts immediately.

Channel Open Rate Avg Response Time
Email 20-25% Hours to days
SMS 45% Under 5 minutes

When a payment fails and you text the customer "Hey Alex, your $49 payment didn't go through — tap here to update your card", they do it right then. No logging in, no finding the right settings page. One tap → Stripe's Customer Portal → card updated → next retry succeeds.

The technical approach

Here's the architecture I ended up with:

Stripe webhook (invoice.payment_failed)
→ Validate event + extract customer data
→ Check retry attempt number (1st, 2nd, 3rd)
→ Select SMS template based on attempt
→ Generate Stripe Customer Portal session URL
→ Send SMS via provider (Twilio/MessageBird)
→ Log event for analytics

Key decisions:

1. Stripe Restricted API Keys

Don't use your full secret key. Create a restricted key with only the permissions you need:

  • Customers: Read
  • Invoices: Read
  • Customer portal sessions: Write

This is the minimum to read customer info, check the invoice, and generate a portal link.

2. Customer Portal Session URL

This is the magic piece. Stripe's Customer Portal lets your customer update their payment method without any authentication on your side:

const session = await stripe.billingPortal.sessions.create({
  customer: customerId,
  return_url: 'https://yourapp.com/account',
});
// session.url → one-tap link for the SMS
Enter fullscreen mode Exit fullscreen mode

The URL is short-lived and scoped to that customer. Perfect for SMS.

3. Escalating SMS templates

Don't send the same message three times. Escalate:

  • Attempt 1 (friendly): "Hi {name}! Your ${amount} payment didn't go through. Update your card here: {link}"
  • Attempt 2 (reminder, +24h): "Reminder: your ${amount} payment is still pending. Tap to update: {link}"
  • Attempt 3 (urgent, +48h): "Last chance — update your card now or your subscription will be canceled: {link}"

Each attempt catches a different segment. Some people just need a nudge, others need urgency.

4. Webhook handling

The critical webhook event is invoice.payment_failed. Key fields:

// Inside your webhook handler
const invoice = event.data.object;
const customerId = invoice.customer;
const amountDue = (invoice.amount_due / 100).toFixed(2);
const attemptCount = invoice.attempt_count;
const subscription = invoice.subscription;

// Only act on subscription invoices, not one-time
if (!subscription) return;

// Get customer phone
const customer = await stripe.customers.retrieve(customerId);
const phone = customer.phone; // or from your own DB
Enter fullscreen mode Exit fullscreen mode

5. Don't spam

Important guardrails:

  • Only send for subscription payments, not one-time charges
  • Max 3 SMS per failed invoice
  • Don't send if the payment was already recovered between retries
  • Respect timezone — nobody wants a payment text at 3 AM
  • Check invoice.status before sending — if it's paid, skip

Results

After implementing this on my own SaaS:

  • Recovery rate went from ~18% (email only) → ~42% (email + SMS)
  • Average time to card update: under 8 minutes after SMS
  • Customer complaints about the texts: zero (they're grateful, actually)

I turned it into a product

After building this for the third time across different projects, I packaged it into RecoverPing (recoverping.com).

It's basically all of the above without writing code:

  • Paste your Stripe restricted API key
  • We register the webhook automatically
  • Customize your SMS templates
  • Done in 5 minutes

Plans start at $19/mo. Coming soon: WhatsApp and Telegram channels.

But if you prefer to build it yourself, everything above should get you there. The core idea is simple: reach your customers where they actually look.

TL;DR

  • Involuntary churn is 20-40% of all subscription churn
  • Stripe dunning emails: 20% open rate. SMS: 45%.
  • Use invoice.payment_failed webhook + Stripe Customer Portal sessions + SMS
  • Escalate urgency across retries
  • Went from 18% → 42% recovery rate

What's your approach to handling failed payments? Anyone tried other channels like WhatsApp or push notifications? Would love to hear what's working for you.

Top comments (0)