Guide Stripe
Une carte est refusée sur Stripe,
votre client reçoit un WhatsApp.
Un seul webhook écoute invoice.paid, invoice.payment_failed et customer.subscription.trial_will_end, et envoie le reçu, la relance d'impayé ou l'avis de fin d'essai par email, WhatsApp ou SMS depuis vos propres comptes. Gratuit, avec le code pour Next.js.
Pressé ? Résumez cet article avec une IA
La réponse courte
Comment envoyer un email, un WhatsApp ou un SMS depuis un webhook Stripe ?
Créez un endpoint de webhook, vérifiez chaque événement avec stripe.webhooks.constructEvent sur le corps brut, répondez 2xx tout de suite, puis envoyez le message en un appel POST /message. Stripe envoie customer.subscription.trial_will_end 3 jours avant la fin d'un essai et invoice.payment_failed après chaque tentative échouée : chaque événement donne un message.
Stripe fait déjà une partie du travail. Smart Retries retente un paiement d'abonnement échoué, jusqu'à 8 fois en 2 semaines par défaut sur les nouveaux comptes, et les emails clients de Stripe peuvent prévenir d'un paiement échoué, d'un essai qui se termine dans 7 jours ou d'une carte qui expire. Uniquement par email.
Un email sur une carte refusée passe facilement inaperçu. Un WhatsApp ou un SMS venant d'un numéro que le client connaît est lu, et le lien vers la page de facture hébergée lui permet de payer en une minute. Votre webhook choisit le canal et le moment : email et WhatsApp au premier échec, SMS aux tentatives suivantes.
Stripe peut livrer le même événement deux fois et dans n'importe quel ordre, et relance un endpoint en production pendant 3 jours maximum s'il ne reçoit pas de 2xx. Votre handler doit être rapide et idempotent : traitez chaque event.id une seule fois.
À lire aussi : les notifications transactionnelles par WhatsApp, et comment éviter un bannissement WhatsApp.

Pas à pas
Comment le construire, étape par étape ?
Cinq étapes dans un projet Next.js App Router, environ 20 minutes. Tout backend fonctionne de la même façon : vérifier, répondre 2xx, envoyer.
Rassemblez les trois secrets
Votre clé secrète Stripe, le secret de signature (whsec_) de votre endpoint de webhook, et une clé API Unlimited Messaging une fois votre boîte mail, votre numéro WhatsApp ou votre téléphone Android connecté. Les trois restent côté serveur, jamais avec un préfixe NEXT_PUBLIC_.
.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_xxxxAjoutez le helper notify
Une fonction réservée au serveur autour de POST /message. Elle ignore les canaux sans destinataire et marque le message comme transactionnel, pour qu'il passe devant un envoi en masse déjà dans votre file.
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(); }Créez la route du webhook
Lisez le corps brut avec req.text() : Stripe signe les octets exacts, et parser le JSON avant casse la vérification. constructEvent rejette aussi les événements plus vieux que sa tolérance par défaut de 5 minutes. Répondez 200 tout de suite et envoyez dans 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 }); }Associez chaque événement à un message
invoice.paid envoie un reçu par email. invoice.payment_failed envoie un email et un WhatsApp avec le lien de la facture hébergée, puis ajoute le SMS quand attempt_count dépasse 1. customer.subscription.trial_will_end arrive 3 jours avant la fin de l'essai : récupérez le client pour l'email et le télé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; }Testez avec la CLI Stripe, puis passez en production
stripe listen transfère les événements de test vers localhost et affiche le secret de signature à utiliser. Déclenchez un paiement échoué, vérifiez les messages, puis ajoutez votre URL de production comme endpoint dans le Dashboard avec les trois événements et son propre secret whsec_.
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
Vous codez avec Cursor, Lovable ou Claude Code ? Collez ce prompt
Le prompt donne à votre éditeur IA le bon schéma : vérification de signature sur le corps brut, 2xx rapide, handler idempotent, messages par canal. Il vous demande vos contacts de test avant d'envoyer.
Ajoute des notifications client sur les événements Stripe à ce projet Next.js (App Router) avec Unlimited Messaging : email, WhatsApp et SMS envoyés depuis mes propres comptes.
1. Utilise STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET et UM_API_KEY depuis .env.local, et liste-les dans .env.example. N'utilise jamais de préfixe NEXT_PUBLIC_.
2. Crée lib/notify.ts (import 'server-only') qui exporte notify({ email, phone, channels, subject, text }) et envoie un POST https://api.unlimitedmessaging.app/message avec l'en-tête Authorization: Bearer UM_API_KEY et le JSON { to: { email, phone }, channels: [{ channel: 'EMAIL' | 'WHATSAPP' | 'SMS' }], subject, text, priority: 'transactional' }. Ignore les canaux sans destinataire.
3. Crée app/api/stripe/webhook/route.ts : lis le corps brut avec req.text(), vérifie-le avec stripe.webhooks.constructEvent et l'en-tête stripe-signature, renvoie 400 en cas d'échec, renvoie 200 tout de suite et traite l'événement dans after() de next/server.
4. Gère invoice.paid (reçu par email), invoice.payment_failed (email + WhatsApp avec hosted_invoice_url, ajoute le SMS quand attempt_count > 1) et customer.subscription.trial_will_end (récupère le client, email + WhatsApp). Enregistre event.id dans une table avec une contrainte d'unicité et ignore les événements déjà traités.
5. Référence de l'API : https://docs.unlimitedmessaging.app. Webhooks Stripe : https://docs.stripe.com/webhooks.
Montre-moi le diff et les commandes stripe listen / stripe trigger pour tester, et demande-moi mon email et mon numéro de test avant d'envoyer quoi que ce soit.Les options
Les emails intégrés de Stripe ou vos propres messages par webhook ?
Les emails clients de Stripe ne demandent aucun code. Un webhook ajoute les canaux et le calendrier de votre choix. Les deux se combinent.
| Emails clients StripeIntégrés à Billing | Webhook + API emailPar exemple Resend | Webhook + TwilioAPI SMS | Webhook + Unlimited MessagingEmail, WhatsApp et SMS | |
|---|---|---|---|---|
| Canaux | SMS, WhatsApp avec modèles Meta | Email, WhatsApp et SMS en une requête | ||
| Paiement échoué | Un email après chaque paiement par carte échoué | Votre email, votre calendrier | Votre SMS, votre calendrier | Escalade de l'email vers WhatsApp et SMS |
| Fin d'essai | Email de rappel 7 jours avant | Événement 3 jours avant, votre email | Événement 3 jours avant, votre SMS | Événement 3 jours avant, tout canal |
| Coût des messages | Compris dans Stripe Billing | Gratuit jusqu'à 3 000 emails par mois, 100 par jour | Facturé au segment SMS | Plan Free, sans frais par message |
| Code | Aucun, réglages du Dashboard | Un handler de webhook | Un handler de webhook | Un handler de webhook |
| Expéditeur | Stripe, à vos couleurs | Votre domaine vérifié | Un numéro Twilio | Votre boîte mail, votre numéro WhatsApp et votre téléphone |
Vérifié le 9 novembre 2026 dans la documentation de Stripe, sur resend.com/pricing et twilio.com (liens dans les sources). Vous pouvez garder les emails de Stripe et ajouter WhatsApp ou SMS par-dessus.
Les contreparties
Quelles sont les limites ?
Gratuit supprime la facture au message. Les webhooks demandent toujours du soin, et WhatsApp et le SMS ont toujours leurs règles.
Les numéros dans Stripe sont du texte libre
customer_phone et customer.phone contiennent ce qui a été saisi. WhatsApp et le SMS exigent le format E.164 (+33612345678) : normalisez les numéros à la collecte, par exemple avec la collecte du téléphone dans Checkout, et ignorez les canaux téléphone sans numéro.
Répondre 200 d'abord, c'est gérer vous-même les relances
after() envoie les messages une fois le 2xx reçu par Stripe : Stripe ne relivrera pas si votre envoi échoue. Pour les messages à ne pas perdre, enregistrez l'événement et traitez-le depuis une file qui réessaie.
Ni l'unicité ni l'ordre ne sont garantis
Stripe peut livrer un événement plusieurs fois et ne garantit pas l'ordre. Dédoublonnez sur event.id, pas sur created, et récupérez l'objet à jour via l'API quand l'état courant compte.
WhatsApp est non officiel : relance oui, campagne non
Votre numéro est lié comme un appareil, hors de la plateforme Business de Meta. Un rappel de paiement à votre propre client est attendu ; une campagne vers tous vos anciens clients est le genre de trafic qui fait bannir un numéro.
Le SMS demande un téléphone Android allumé
Les SMS partent de la SIM de votre téléphone, sur votre forfait. Android demande une confirmation au-delà de 30 SMS en 30 minutes, l'application espace donc les envois à environ 30 toutes les 30 minutes : largement assez pour des relances, pas pour une campagne de masse.
Les messages du plan Free portent une signature
Sur le plan Free, les messages se terminent par une courte ligne "Sent with UnlimitedMessaging.app", et les médias et les webhooks de statut sont payants. Medium et Max retirent la ligne.
FAQ
Questions sur les notifications par webhook Stripe
Sources
Sources & références
Pour vérifier par vous-même les informations de cet article :
- Utilisation des webhooks avec les abonnements (trial_will_end 3 jours avant, invoice.payment_failed)Stripe Docs
- Recevez les événements Stripe dans votre endpoint de webhook (signatures, 2xx, doublons, tolérance de 5 minutes)Stripe Docs
- Automatiser les relances de paiement (Smart Retries : jusqu'à 8 relances en 2 semaines par défaut)Stripe Docs
- Automatiser les e-mails aux clients (paiements échoués, rappel 7 jours avant la fin de l'essai)Stripe Docs
- after (stable depuis la v15.1.0)Next.js Docs
- route.js (Route Handlers)Next.js Docs
- Pricing (Free : 3 000 emails par mois, 100 par jour)Resend
- Tarifs SMS, au segment et par paysTwilio
- SmsUsageMonitor (seuil de 30 SMS en 30 minutes)Android Open Source Project
- Référence de l'API : POST /messageUnlimited Messaging Docs

Écrit par
Ilian · Senior Software Engineer
Ingénieur logiciel senior, il fonde et dirige plusieurs sociétés tech. Il conçoit des produits autour des API de messagerie, de l’automatisation et de l’intégration de l’IA, et partage ici ce qu’il apprend en construisant.
Transformez vos impayés en rappels WhatsApp dès aujourd'hui.
Créez un compte gratuit, connectez une boîte mail, un numéro WhatsApp ou un téléphone Android, collez le handler. Sans carte bancaire, sans frais par message.