Subscription Engineering6 min read

Build a Stripe Dunning Sequence That Recovers Revenue, Not Just Reminders

Stripe retries the card. You still have to reach the customer. A 3-step email + Telegram sequence recovers more than a single generic invoice email.

1. Stripe retries the card. That is not dunning.

Smart Retries will hit the card again on a better day. They will not tell the customer their Visa expired. If the only email they get is Stripe's generic invoice, a lot of them ignore it — and you lose a month of MRR that was already earned.

A real dunning sequence is a conversation: instant notice, a second nudge when Stripe retries, a last chance before access drops. The trigger is already in your stack — invoice.payment_failed fires on every attempt. You just have to map attempt 1 / 2 / 3 to different copy and channels.

2. The 3-step sequence that actually converts

  • Step 1 — instant. Email the customer with the hosted invoice link. Ping yourself on Telegram so you see the miss in real time.
  • Step 2 — next Stripe retry (~24h). Email again, shorter and more direct. Same pay link. Do not invent a cron.
  • Step 3 — last retry (~3 days). Final notice. If they pay, invoice.payment_succeeded marks the row recovered and the sequence stops.

That mapping is one function. Stripe's attempt_count is the step index:

function pickStep(flow, attemptCount) {
  const enabled = flow.steps.filter((s) => s.enabled);
  const idx = Math.max(0, Math.min(attemptCount - 1, enabled.length - 1));
  return enabled[idx];
}

// attempt 1 → instant email + Telegram
// attempt 2 → 24h email
// attempt 3 → final notice

3. What to send (and what not to)

Put the amount in the subject. Put a single button in the body. Send them to Stripe's hosted invoice — never collect a card on your own page. Founder alerts belong on Telegram, not in the same inbox as the customer.

Pause must actually pause. If a founder hits Pause on a row, the next Stripe retry for that invoice should skip. Otherwise the sequence is theater.