Aller au contenu principal

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.

Dessin de l'application Unlimited Messaging montrant une liste de messages avec leur statut (lu, distribué, envoyé, en file), avec le titre « Relances Stripe par WhatsApp, SMS et email ».

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.

  1. 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_xxxx
  2. Ajoutez 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();
    }
  3. 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 });
    }
  4. 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;
    }
  5. 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.

Prompt à copier
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.

Les emails intégrés de Stripe ou vos propres messages par webhook ?
Emails clients StripeIntégrés à BillingWebhook + API emailPar exemple ResendWebhook + TwilioAPI SMSWebhook + Unlimited MessagingEmail, WhatsApp et SMS
CanauxEmailEmailSMS, WhatsApp avec modèles MetaEmail, WhatsApp et SMS en une requête
Paiement échouéUn email après chaque paiement par carte échouéVotre email, votre calendrierVotre SMS, votre calendrierEscalade de l'email vers WhatsApp et SMS
Fin d'essaiEmail 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 messagesCompris dans Stripe BillingGratuit jusqu'à 3 000 emails par mois, 100 par jourFacturé au segment SMSPlan Free, sans frais par message
CodeAucun, réglages du DashboardUn handler de webhookUn handler de webhookUn handler de webhook
ExpéditeurStripe, à vos couleursVotre domaine vérifiéUn numéro TwilioVotre 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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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

Ilian, ingénieur logiciel senior

Écrit par

· 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.

Voir le profil LinkedInMis à jour le

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.