# Stripe webhook send email, SMS or WhatsApp: free dunning?

> Stripe retries a failed card up to 8 times in 2 weeks but only emails the customer. Add WhatsApp and SMS to your dunning from one webhook, for free.

Source: https://unlimitedmessaging.app/articles/stripe-payment-notifications-whatsapp-email

Stripe guide

# A card fails on Stripe,  
_your customer gets a WhatsApp._

One webhook handler listens to invoice.paid, invoice.payment\_failed and customer.subscription.trial\_will\_end, and sends the receipt, the dunning reminder or the trial notice by email, WhatsApp or SMS from your own accounts. Free, with the code for Next.js.

[Get a free API key](https://app.unlimitedmessaging.app)[See the API](https://unlimitedmessaging.app/api)

The short answer

## How do you send an email, WhatsApp or SMS from a Stripe webhook?

Create a webhook endpoint, verify each event with stripe.webhooks.constructEvent on the raw body, answer 2xx at once, then send the message with one POST /message call. Stripe sends [customer.subscription.trial\_will\_end](https://docs.stripe.com/billing/subscriptions/webhooks) 3 days before a trial ends and invoice.payment\_failed after each failed attempt, so each event maps to one message.

Stripe already does part of the job. Smart Retries retry a failed subscription payment, up to 8 times over 2 weeks by default on new accounts, and Stripe's customer emails can warn about a failed payment, a trial ending in 7 days or an expiring card. They are emails only.

An email about a failed card is easy to miss. A WhatsApp or an SMS from a number the customer recognises gets read, and the link to the hosted invoice page lets them pay in a minute. Your webhook decides which channel to use and when: email and WhatsApp on the first failure, SMS on the next attempts.

Stripe can deliver the same event twice and in any order, and retries a live endpoint for up to 3 days when it does not get a 2xx. Your handler has to be fast and idempotent: process each event.id once.

Related: [transactional notifications by WhatsApp](https://unlimitedmessaging.app/articles/notifications), and [how to avoid a WhatsApp ban](https://unlimitedmessaging.app/articles/avoid-whatsapp-ban).

![Drawing of the Unlimited Messaging app showing a list of messages with their status (read, delivered, sent, queued), with the headline “Stripe dunning by WhatsApp, SMS and email”.](https://unlimitedmessaging.app/_next/image?url=%2Fassets%2Farticles%2Fcovers%2Fstripe-payment-notifications-whatsapp-email-en.png&w=3840&q=75)

Definition
Dunning

Recovering a failed recurring payment: retrying the charge and asking the customer to update their payment method before the subscription is canceled.

Also called failed payment recovery or revenue recovery.

[Source: Stripe Docs, Automate payment retries↗](https://docs.stripe.com/billing/revenue-recovery/smart-retries)

Step by step

## How do you build it, step by step?

Five steps in a Next.js App Router project, about 20 minutes. Any backend works the same way: verify, answer 2xx, send.

1. 01  
### Collect the three secrets  
Your Stripe secret key, the signing secret (whsec\_) of your webhook endpoint, and an Unlimited Messaging API key once your mailbox, WhatsApp number or Android phone is connected. All three stay server-side, never with a NEXT\_PUBLIC\_ prefix.  
.env.local.env.local  
```  
# .env.local (server only, never NEXT_PUBLIC_)  
STRIPE_SECRET_KEY=sk_test_xxxx  
# Printed by "stripe listen" in dev, or shown on the endpoint in the Dashboard  
STRIPE_WEBHOOK_SECRET=whsec_xxxx  
UM_API_KEY=um_live_xxxx  
```
2. 02  
### Add the notify helper  
One server-only function around POST /message. It skips the channels that have no destination and marks the message transactional, so it goes ahead of any bulk send waiting in your queue.  
lib/notify.tsTypeScript  
```  
// lib/notify.ts  
import 'server-only';  
export type Channel = 'EMAIL' | 'WHATSAPP' | 'SMS';  
export async function notify(input: {  
  email?: string;  
  phone?: string; // E.164, e.g. +33612345678  
  channels: Channel[];  
  subject: string;  
  text: string;  
}) {  
  const channels = input.channels  
    .filter((c) => (c === 'EMAIL' ? input.email : input.phone))  
    .map((channel) => ({ channel }));  
  if (channels.length === 0) return null;  
  const res = await fetch('https://api.unlimitedmessaging.app/message', {  
    method: 'POST',  
    headers: {  
      Authorization: `Bearer ${process.env.UM_API_KEY}`,  
      'Content-Type': 'application/json',  
    },  
    body: JSON.stringify({  
      to: { email: input.email, phone: input.phone },  
      channels,  
      subject: input.subject,  
      text: input.text,  
      priority: 'transactional',  
    }),  
  });  
  if (!res.ok) throw new Error(`Unlimited Messaging ${res.status}: ${await res.text()}`);  
  return res.json();  
}  
```
3. 03  
### Create the webhook route  
Read the raw body with req.text(): Stripe signs the exact bytes, and parsing JSON first breaks the check. constructEvent also rejects events older than its 5-minute default tolerance. Return 200 at once and send inside after().  
app/api/stripe/webhook/route.tsRoute Handler  
```  
// app/api/stripe/webhook/route.ts  
import type Stripe from 'stripe';  
import { after } from 'next/server';  
import { stripe, handleStripeEvent } from '@/lib/stripe-notify';  
export async function POST(req: Request) {  
  // The raw body: parsing it as JSON first breaks the signature check  
  const body = await req.text();  
  const signature = req.headers.get('stripe-signature') ?? '';  
  let event: Stripe.Event;  
  try {  
    event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!);  
  } catch {  
    return new Response('Invalid signature', { status: 400 });  
  }  
  // Answer Stripe with a 2xx right away, send the messages after the response  
  after(() => handleStripeEvent(event).catch(console.error));  
  return new Response('ok', { status: 200 });  
}  
```
4. 04  
### Map each event to a message  
invoice.paid sends a receipt by email. invoice.payment\_failed sends email and WhatsApp with the hosted invoice link, then adds SMS once attempt\_count is above 1\. customer.subscription.trial\_will\_end arrives 3 days before the trial ends: fetch the customer for the email and phone.  
lib/stripe-notify.tsTypeScript  
```  
// lib/stripe-notify.ts  
import 'server-only';  
import Stripe from 'stripe';  
import { notify, type Channel } from '@/lib/notify';  
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);  
const money = (amount: number, currency: string) =>  
  new Intl.NumberFormat('en', { style: 'currency', currency }).format(amount / 100);  
export async function handleStripeEvent(event: Stripe.Event) {  
  // Stripe can send the same event twice: skip the ones already handled  
  if (!(await markProcessed(event.id))) return;  
  switch (event.type) {  
    case 'invoice.paid': {  
      const inv = event.data.object;  
      await notify({  
        email: inv.customer_email ?? undefined,  
        channels: ['EMAIL'],  
        subject: 'Payment received',  
        text: `Thanks, we received ${money(inv.amount_paid, inv.currency)}.`,  
      });  
      break;  
    }  
    case 'invoice.payment_failed': {  
      const inv = event.data.object;  
      // First failure: email + WhatsApp. Next retries: SMS as well.  
      const channels: Channel[] =  
        inv.attempt_count > 1 ? ['EMAIL', 'WHATSAPP', 'SMS'] : ['EMAIL', 'WHATSAPP'];  
      await notify({  
        email: inv.customer_email ?? undefined,  
        phone: inv.customer_phone ?? undefined,  
        channels,  
        subject: 'Your payment did not go through',  
        text: `Your payment of ${money(inv.amount_due, inv.currency)} failed. Update your card here: ${inv.hosted_invoice_url}`,  
      });  
      break;  
    }  
    case 'customer.subscription.trial_will_end': {  
      const sub = event.data.object; // sent 3 days before the trial ends  
      const customer = await stripe.customers.retrieve(sub.customer as string);  
      if (customer.deleted) break;  
      await notify({  
        email: customer.email ?? undefined,  
        phone: customer.phone ?? undefined,  
        channels: ['EMAIL', 'WHATSAPP'],  
        subject: 'Your trial ends in 3 days',  
        text: 'Your trial ends in 3 days. Nothing to do if your card is up to date.',  
      });  
      break;  
    }  
  }  
}  
async function markProcessed(eventId: string): Promise<boolean> {  
  // e.g. INSERT INTO stripe_events (id) VALUES ($1) ON CONFLICT DO NOTHING,  
  // then return true only when a row was inserted  
  return true;  
}  
```
5. 05  
### Test with the Stripe CLI, then go live  
stripe listen forwards test events to localhost and prints the signing secret to use. Trigger a failed payment, check the messages, then add your production URL as an endpoint in the Dashboard with the three events selected and its own whsec\_ secret.  
TerminalStripe CLI  
```  
# Forward test events to your local route (prints a whsec_ secret)  
stripe listen --forward-to localhost:3000/api/stripe/webhook  
# In another terminal: fire a failed payment  
stripe trigger invoice.payment_failed  
```

Vibe coding

## Using Cursor, Lovable or Claude Code? Paste this prompt

The prompt gives your AI editor the safe pattern: signature check on the raw body, fast 2xx, idempotent handler, messages by channel. It asks for your test contacts before sending.

Prompt to copy

Add customer notifications on Stripe events to this Next.js (App Router) project with Unlimited Messaging: email, WhatsApp and SMS sent from my own accounts.

1. Use STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET and UM_API_KEY from .env.local, and list them in .env.example. Never use a NEXT_PUBLIC_ prefix.
2. Create lib/notify.ts (import 'server-only') exporting notify({ email, phone, channels, subject, text }) that sends one POST https://api.unlimitedmessaging.app/message with the header Authorization: Bearer UM_API_KEY and the JSON { to: { email, phone }, channels: [{ channel: 'EMAIL' | 'WHATSAPP' | 'SMS' }], subject, text, priority: 'transactional' }. Skip channels without a destination.
3. Create app/api/stripe/webhook/route.ts: read the raw body with req.text(), verify it with stripe.webhooks.constructEvent and the stripe-signature header, return 400 on failure, return 200 at once and handle the event inside after() from next/server.
4. Handle invoice.paid (receipt by email), invoice.payment_failed (email + WhatsApp with hosted_invoice_url, add SMS when attempt_count > 1) and customer.subscription.trial_will_end (retrieve the customer, email + WhatsApp). Store event.id in a table with a unique constraint and skip events already processed.
5. API reference: https://docs.unlimitedmessaging.app. Stripe webhooks: https://docs.stripe.com/webhooks.

Show me the diff and the stripe listen / stripe trigger commands to test, and ask for my test email and phone number before sending anything.

The options

## Stripe's built-in emails or your own webhook messages?

Stripe's customer emails need no code. A webhook handler adds the channels and the timing you choose. They work together.

__Stripe's built-in emails or your own webhook messages?__
| [Stripe customer emails](https://docs.stripe.com/billing/revenue-recovery/customer-emails)Built into Billing | [Webhook + email API](https://resend.com/pricing)For example Resend | [Webhook + Twilio](https://www.twilio.com/en-us/sms/pricing)SMS API | Webhook + Unlimited MessagingEmail, WhatsApp and SMS |                                             |
| ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------- | ---------------------------------------------------- | ------------------------------------------- |
| Channels                                                                                                     | Email                                                               | Email                                                               | SMS, WhatsApp with Meta templates                    | Email, WhatsApp and SMS in one request      |
| Failed payment                                                                                               | An email after each failed card payment                             | Your email, your timing                                             | Your SMS, your timing                                | Escalate from email to WhatsApp and SMS     |
| Trial ending                                                                                                 | Reminder email 7 days before                                        | Event 3 days before, your email                                     | Event 3 days before, your SMS                        | Event 3 days before, any channel            |
| Cost of the messages                                                                                         | Part of Stripe Billing                                              | Free up to 3,000 emails a month, 100 a day                          | Billed per SMS segment                               | Free plan, no per-message fee               |
| Code                                                                                                         | None, Dashboard settings                                            | One webhook handler                                                 | One webhook handler                                  | One webhook handler                         |
| Sender                                                                                                       | Stripe, with your branding                                          | Your verified domain                                                | A Twilio number                                      | Your own mailbox, WhatsApp number and phone |

Checked on 9 November 2026 on Stripe's documentation, resend.com/pricing and twilio.com (links in the sources). You can keep Stripe's emails on and add WhatsApp or SMS on top.

The catch

## What are the limits?

Free removes the per-message bill. Webhooks still need care, and WhatsApp and SMS still have rules.

1. 01  
### Stripe phone numbers are free text  
customer\_phone and customer.phone hold whatever was typed. WhatsApp and SMS need E.164 (+33612345678): normalise numbers when you collect them, for example with phone number collection in Checkout, and skip the phone channels when a number is missing.
2. 02  
### Answering 200 first means you own the retries  
after() sends the messages once Stripe has its 2xx, so Stripe will not redeliver if your send fails. For messages you cannot lose, store the event and process it from a queue that retries.
3. 03  
### Duplicates and order are not guaranteed  
Stripe may deliver an event more than once and does not guarantee order. Deduplicate on event.id, not on created, and fetch the latest object from the API when the current state matters.
4. 04  
### WhatsApp is unofficial: dunning yes, blasts no  
Your number is linked as a device, outside Meta's Business Platform. A payment reminder to your own customer is expected; a campaign to every past customer is the kind of traffic that gets numbers banned.
5. 05  
### SMS needs an Android phone that stays on  
SMS leave from your phone's SIM, on your plan. Android asks for confirmation beyond 30 SMS in 30 minutes, so the app paces sends to about 30 every 30 minutes: plenty for dunning, not for a mass campaign.
6. 06  
### Free plan messages carry a footer  
On the Free plan, messages end with a short "Sent with UnlimitedMessaging.app" line, and media and delivery webhooks are paid features. Medium and Max remove the line.

When to choose something else

### Prefer zero code, or very high volume?

If email is enough, turn on Stripe's failed payment and trial reminder emails in the Dashboard and stop there: no endpoint to host. If you send thousands of SMS a day or need Meta's official WhatsApp templates, a provider like Twilio fits better. Your own webhook with Unlimited Messaging fits a SaaS that wants its dunning in the channels customers read, from its own number, free while it grows.

FAQ

## Questions about Stripe webhook notifications

Verify the event with stripe.webhooks.constructEvent on the raw body, answer 2xx, then call an email API. With Unlimited Messaging, POST /message with the channel EMAIL sends from your own mailbox, with no per-message fee on the Free plan.

invoice.paid for receipts, invoice.payment\_failed for dunning (attempt\_count tells you which attempt failed) and customer.subscription.trial\_will\_end for trial reminders, sent 3 days before the end. invoice.payment\_action\_required covers payments that need customer authentication.

Yes. In the invoice.payment\_failed handler, send POST /message with the channel SMS to invoice.customer\_phone. With Unlimited Messaging it leaves from your Android phone's SIM, with no per-SMS fee from us.

No. Stripe's automatic customer emails are emails only. WhatsApp or SMS dunning goes through your own webhook handler.

Not necessarily. You can keep Stripe's email, which links to its hosted page, and add a WhatsApp or an SMS. Just avoid sending two emails for the same failure.

Usually because the body was parsed before verification. In an App Router Route Handler, read it with await req.text() and pass that string to constructEvent with the stripe-signature header and the whsec\_ secret of the right endpoint: the one printed by stripe listen differs from the Dashboard's.

The Unlimited Messaging Free plan has no per-message fee on any channel and needs no credit card; messages carry a short footer. Stripe charges for payments and Billing, not for sending webhook events.

Sources

## Sources & references

To check the information in this article for yourself:

1. [01Using webhooks with subscriptions (trial\_will\_end 3 days before, invoice.payment\_failed)↗Stripe Docs](https://docs.stripe.com/billing/subscriptions/webhooks)
2. [02Receive Stripe events in your webhook endpoint (signatures, 2xx, duplicates, 5-minute tolerance)↗Stripe Docs](https://docs.stripe.com/webhooks)
3. [03Automate payment retries (Smart Retries: up to 8 retries in 2 weeks by default)↗Stripe Docs](https://docs.stripe.com/billing/revenue-recovery/smart-retries)
4. [04Automate customer emails (failed payments, trial reminders 7 days before)↗Stripe Docs](https://docs.stripe.com/billing/revenue-recovery/customer-emails)
5. [05after (stable since v15.1.0)↗Next.js Docs](https://nextjs.org/docs/app/api-reference/functions/after)
6. [06route.js (Route Handlers)↗Next.js Docs](https://nextjs.org/docs/app/api-reference/file-conventions/route)
7. [07Pricing (Free: 3,000 emails a month, 100 a day)↗Resend](https://resend.com/pricing)
8. [08SMS pricing, per segment and per country↗Twilio](https://www.twilio.com/en-us/sms/pricing)
9. [09SmsUsageMonitor (30 SMS in 30 minutes threshold)↗Android Open Source Project](https://android.googlesource.com/platform/frameworks/opt/telephony/+/refs/heads/main/src/java/com/android/internal/telephony/SmsUsageMonitor.java)
10. [10API reference: POST /message↗Unlimited Messaging Docs](https://docs.unlimitedmessaging.app)

![Ilian, senior software engineer](https://ilianazz.com/images/iliazz.png)

Written by

[Ilian](https://unlimitedmessaging.app/authors/ilian) · Senior Software Engineer

A senior software engineer, he founds and runs several tech companies. He builds products around messaging APIs, automation and AI integration, and shares here what he learns while building.

[View LinkedIn profile↗](https://www.linkedin.com/in/ilianAzz/)Updated November 9, 2026

![](https://unlimitedmessaging.app/assets/logos/whatsapp.svg)![](https://unlimitedmessaging.app/assets/logos/email.svg)![](https://unlimitedmessaging.app/assets/logos/sms.svg)

## Turn failed payments into WhatsApp reminders today.

Create a free account, connect a mailbox, a WhatsApp number or an Android phone, paste the handler. No credit card, no per-message fee.

[Start for free→](https://app.unlimitedmessaging.app)

Similar articles
* [![](https://unlimitedmessaging.app/_next/image?url=%2Fassets%2Farticles%2Fcovers%2Fnotifications-en.png&w=3840&q=75)Order confirmations on WhatsApp: the 2026 setup guideSend order confirmations, alerts, and receipts via WhatsApp.](https://unlimitedmessaging.app/articles/notifications)
* [![](https://unlimitedmessaging.app/_next/image?url=%2Fassets%2Farticles%2Fcovers%2Fecommerce-order-notifications-whatsapp-sms-en.png&w=3840&q=75)E-commerce: 5 WhatsApp and SMS order updates, freeE-commerceOrder confirmed, shipped, out for delivery: notify customers by WhatsApp, SMS and email from your own accounts, no per-message fee.](https://unlimitedmessaging.app/articles/ecommerce-order-notifications-whatsapp-sms)
* [![](https://unlimitedmessaging.app/_next/image?url=%2Fassets%2Farticles%2Fcovers%2Femail-whatsapp-single-api-request-en.png&w=3840&q=75)One API request, two channels: email and WhatsApp at onceSend the same message by WhatsApp and email in one call, from your own number and mailbox, free on every plan.](https://unlimitedmessaging.app/articles/email-whatsapp-single-api-request)
