Alertes d'échec de paiement Stripe : litiges, refus et webhooks morts

Stripe vous prévient par e-mail des litiges et paiements échoués. Comment transformer les événements webhook Stripe en push, alerte urgente ou appel.

Mis à jour

Table des matières

Stripe sait déjà quand un litige est ouvert, quand un paiement d'abonnement échoue ou quand un virement est rejeté. Il vous le dit par e-mail. Pour obtenir quelque chose de plus sonore, enregistrez dans Stripe un second endpoint webhook pointant vers l'URL d'un canal Echobell, abonnez-le à une poignée de types d'événements et choisissez le type de notification en fonction de l'échéance attachée à chacun. Les litiges et les alertes précoces de fraude ont un chrono qui tourne ; un renouvellement échoué, en général, non.

Ce guide couvre les événements Stripe qui méritent de vous interrompre, la façon de brancher Stripe sur Echobell en cinq minutes environ, les champs de modèle qui existent réellement sur chaque objet, et le compromis que vous acceptez en sautant la vérification de signature.

Les événements qui méritent vraiment de vous interrompre

L'alerting de paiement échoue toujours de la même manière : quelqu'un s'abonne à payment_intent.succeeded parce que c'est agréable, le téléphone vibre quarante fois par jour, et six semaines plus tard une notification de litige défile sans être lue. Partez plutôt de l'échéance. Si rater l'événement pendant huit heures ne coûte rien, il n'a pas besoin de vous atteindre en huit secondes.

ÉvénementPourquoi c'est importantType conseillé
charge.dispute.createdVous avez une fenêtre limitée pour répondre — généralement 7 à 21 jours selon le réseau de cartes. Manquée, elle vaut défaite automatique.Appel
radar.early_fraud_warning.createdL'émetteur de la carte a signalé à Stripe qu'un paiement pourrait être frauduleux. Rembourser avant que cela ne devienne un litige formel est l'action qui vous reste, et la fenêtre est courte.Appel
payout.failedL'argent encaissé par Stripe n'arrive pas sur votre compte. Tout ce qui en dépend — salaires, calcul de runway — est désormais faux.Appel
invoice.payment_failedChurn involontaire. Cela se résout souvent tout seul, donc un appel est excessif, mais les plus gros comptes méritent un coup d'œil le jour même.Urgent
customer.subscription.deletedChurn volontaire. Bon à savoir aujourd'hui, pas de quoi vous réveiller.Normal
payment_intent.succeededRien n'est cassé. C'est précisément l'événement qui vous entraîne à ignorer les cinq autres.Rien

Ces niveaux correspondent aux trois types de notification d'Echobell : Normal est un push ordinaire, Urgent traverse la plupart des modes de concentration, et Appel se présente comme un appel entrant et sonne donc malgré Ne pas déranger. Chaque abonné choisit son niveau canal par canal : votre cofondatrice peut prendre les litiges en appel pendant qu'un collègue du support les reçoit en push.

Ce qu'il vous faut

  • Un compte Stripe avec accès à l'onglet Webhooks de Workbench
  • Echobell installé (App Store / Google Play)
  • Cinq minutes. Pas de serveur, pas de déploiement, pas de code — sauf si vous voulez la vérification de signature, qui fait l'objet de la dernière section.

Étape 1 — Un canal par type d'événement

La tentation est de créer un unique canal « Stripe » et d'y envoyer tout. Ne le faites pas. Le modèle de corps et le lien vers le tableau de bord diffèrent entre un litige, une facture et un virement, parce que chacun transporte un objet différent — et tout l'intérêt du montage est que la notification vous dise ce qui s'est passé sans rien ouvrir.

Créez un canal nommé d'après l'événement : Stripe Disputes. Écrivez les modèles de titre et de corps pour qu'ils se lisent d'un trait sur l'écran verrouillé :

Title: 🔴 Dispute opened — {{data.object.reason}}
Body: Amount: {{data.object.amount}} {{data.object.currency}}
Charge: {{data.object.charge}}
Status: {{data.object.status}}

Réglez le modèle de lien dans les paramètres avancés pour que l'enregistrement de notification ouvre la bonne page :

https://dashboard.stripe.com/disputes/{{data.object.id}}

Abonnez-vous ensuite avec le type Appel et copiez l'URL du webhook depuis la vue détaillée du canal. Elle ressemble à https://hook.echobell.one/t/<channel-token>.

Activez POST Only dans les paramètres avancés du canal. Stripe envoie toujours en POST, et cet interrupteur fait que coller l'URL dans une fenêtre de chat ou un onglet de navigateur ne peut plus déclencher une fausse alerte de litige.

Étape 2 — Pointer Stripe vers le canal

Dans le tableau de bord Stripe, ouvrez l'onglet Webhooks et créez une destination d'événements :

Cliquez sur Create an event destination, sélectionnez Your account et laissez la version de l'API sur la valeur par défaut de votre compte.

Sélectionnez exactement un type d'événement — charge.dispute.created pour ce canal. Le conseil de Stripe lui-même est de ne s'abonner qu'aux événements dont votre intégration a besoin ; ici, cela garde aussi le modèle honnête, puisque chaque payload reçu a la même forme.

Choisissez Webhook endpoint comme type de destination et collez l'URL du canal Echobell.

Enregistrez, puis utilisez Send test event — ou stripe trigger charge.dispute.created depuis la CLI — et vérifiez que le téléphone sonne.

Répétez pour chaque canal créé. Stripe autorise jusqu'à 16 endpoints webhook par compte, largement de quoi en avoir un par niveau d'alerte.

Étape 3 — Ce qui arrive réellement

Stripe poste l'objet Event en JSON. Echobell lit le corps tel quel : chaque champ est donc adressable dans les modèles et les conditions avec la notation par points :

{
  "id": "evt_1P...",
  "type": "charge.dispute.created",
  "livemode": true,
  "created": 1757548800,
  "data": {
    "object": {
      "id": "dp_1P...",
      "amount": 4900,
      "currency": "usd",
      "reason": "fraudulent",
      "status": "needs_response",
      "charge": "ch_3P...",
      "evidence_details": { "due_by": 1759449600 }
    }
  }
}

Trois choses surprennent dans ce payload :

Les montants sont des entiers dans la plus petite unité monétaire. Un amount de 4900 vaut 49,00 $. Les modèles Echobell interpolent et comparent les valeurs mais ne font pas d'arithmétique : {{data.object.amount}} affiche donc 4900. Soit vous l'étiquetez honnêtement (Amount: 4900 (cents)), soit vous utilisez le relais de la dernière section pour diviser par 100 avant l'envoi.

Les horodatages sont en secondes Unix. {{data.object.evidence_details.due_by}} s'affiche 1759449600, pas sous forme de date. Si l'existence de l'échéance compte plus que l'heure exacte, retirez-la du modèle — la page du litige l'affiche — et laissez le modèle de lien faire le travail.

Les noms de champs diffèrent selon l'objet. Un litige a amount ; une facture a amount_due, customer_email, attempt_count et hosted_invoice_url ; un virement a failure_message et arrival_date ; une alerte précoce de fraude a fraud_type, actionable et charge sous forme de simple ID texte. Une variable absente s'affiche comme chaîne vide plutôt que comme erreur : un modèle copié du mauvais canal échoue donc en silence. C'est la raison pratique du principe « un canal par type d'événement ».

Étape 4 — Filtrer par conditions, pas par volonté

Les conditions de canal utilisent la même syntaxe d'expressions que les modèles, sans les accolades, et s'exécutent avant toute livraison.

Celle à mettre sur chaque canal Stripe :

livemode == true

Le trafic en mode test — vos propres stripe trigger, un collègue qui bricole dans un bac à sable — n'atteint plus votre téléphone. Ajoutez-la après avoir vérifié que le câblage fonctionne, pas avant.

Pour le canal des factures échouées, un seuil tient les petits comptes à l'écart de vos soirées :

livemode == true && data.object.amount_due > 20000

À lire « plus de 200 $ », en centimes. Et si vous préférez voir les relances réellement bloquées plutôt que chaque premier échec :

livemode == true && data.object.attempt_count > 1

Si vous avez malgré tout dirigé un endpoint portant plusieurs types d'événements vers un seul canal, les conditions les redécoupent :

type == "charge.dispute.created" || type == "payout.failed"

Étape 5 — Garder le chemin d'alerte hors de ce qui casse

Voici la partie qui vaut plus que les modèles.

Votre endpoint webhook de production est l'endroit où se fait la livraison du service : il ouvre les accès, écrit en base, envoie le reçu. C'est donc aussi l'endpoint qui tombe quand votre application tombe. Quand cela arrive, Stripe réessaie jusqu'à trois jours avec un backoff exponentiel et vous envoie un e-mail — et un e-mail sur des webhooks non livrés ressemble à tous les autres e-mails Stripe, ce qui explique qu'on le retrouve le lundi.

La mort silencieuse d'un webhook a des causes banales. Stripe traite une redirection 3xx comme un échec : un endpoint qui se met à rediriger http vers https ou à ajouter une barre oblique finale cesse de recevoir des événements. Il exige TLS 1.2 ou supérieur : un certificat expiré ou mal configuré suffit. Un 403 renvoyé par une règle WAF ajoutée la semaine dernière aussi.

Un second endpoint pointé droit sur Echobell ne partage rien de tout cela. C'est une autre URL sur un autre hôte avec un autre certificat, et elle continue de sonner pendant que votre application est à terre. La règle se généralise : le chemin qui vous dit que quelque chose est cassé ne devrait pas passer par la chose cassée.

Vous voulez tout de même voir l'échec de votre propre endpoint. Consultez l'onglet Event deliveries dans Workbench quand quelque chose cloche — il affiche Delivered, Pending et Failed par événement, avec le statut HTTP de chaque tentative. Stripe permet de renvoyer un événement jusqu'à 15 jours depuis le tableau de bord, ou 30 jours avec stripe events resend en CLI : un trou repéré en quinze jours est donc rattrapable.

L'URL de canal Echobell est un identifiant porteur : quiconque la détient peut déclencher le canal. Pointer Stripe directement dessus signifie que personne ne vérifie l'en-tête Stripe-Signature : une URL divulguée est une machine à fausses alertes, pas une fuite de données. Gardez-la hors des dépôts et des captures d'écran, utilisez Reset Token si elle s'échappe, et lisez la section suivante si ce compromis vous gêne.

Optionnel — Vérifier d'abord la signature

Si vous voulez que la signature Stripe soit réellement vérifiée et les montants formatés comme de l'argent, placez un petit relais devant. Ce Cloudflare Worker vérifie l'événement, renvoie 200 immédiatement comme Stripe le demande, et envoie à Echobell un payload à plat :

import Stripe from "stripe";

export default {
  async fetch(request, env, ctx) {
    const stripe = new Stripe(env.STRIPE_SECRET_KEY);
    const body = await request.text();

    let event;
    try {
      event = await stripe.webhooks.constructEventAsync(
        body,
        request.headers.get("stripe-signature"),
        env.STRIPE_WEBHOOK_SECRET,
      );
    } catch {
      return new Response("invalid signature", { status: 400 });
    }

    const invoice = event.data.object;
    ctx.waitUntil(
      fetch(env.ECHOBELL_HOOK_URL, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          customer: invoice.customer_email || invoice.customer,
          amount: (invoice.amount_due / 100).toFixed(2),
          currency: invoice.currency.toUpperCase(),
          attempt: invoice.attempt_count,
          externalLink: invoice.hosted_invoice_url,
        }),
      }),
    );

    return new Response("ok", { status: 200 });
  },
};

Le modèle de l'autre côté devient bien plus agréable, puisque la mise en forme s'est faite dans le code :

Title: 💳 Payment failed — {{currency}} {{amount}}
Body: Customer: {{customer}}
Attempt #{{attempt}}

externalLink est une variable spéciale : sans modèle de lien défini, Echobell l'utilise pour le lien de l'enregistrement de notification, et la facture hébergée est alors à une tape de distance.

Notez la forme du compromis : vous venez d'ajouter une infrastructure qui peut elle-même tomber, ce contre quoi l'étape 5 met précisément en garde. Un compromis raisonnable consiste à vérifier les signatures sur le canal à fort volume, là où de fausses alertes seraient pénibles, et à laisser le canal des litiges branché en direct, là où une sonnerie parasite coûte un regard perplexe et une sonnerie manquée coûte le montant contesté.

Ce que ce montage ne fournit pas

  • Ni tour d'astreinte ni escalade. Toutes les personnes abonnées à un canal d'appel sonnent en même temps. C'est une qualité à quatre et un problème à quarante ; à quarante, il vous faut une plateforme d'incidents.
  • Pas de déduplication. Stripe ne garantit pas l'ordre des événements et peut livrer le même plusieurs fois. Deux sonneries pour un litige, c'est possible.
  • Pas d'accusé de réception. Rien n'enregistre qu'un humain l'a vu, et rien n'escalade vers une deuxième personne si personne ne réagit.
  • Pas d'alerte « les paiements se sont arrêtés ». Stripe émet des événements quand les choses arrivent, jamais quand elles cessent. Si votre tunnel de paiement casse, aucun événement ne part. Celle-là exige une tâche planifiée de votre côté qui ping un canal quand le nombre de paiements de la dernière heure est nul — un homme mort à base de cron.

Dépannage

L'événement de test affiche 200 dans Stripe mais aucune notification n'arrive. Echobell répond 200 avec un corps JSON même sans livrer — regardez le corps de la réponse dans l'onglet Event deliveries. success: false avec un jeton de longueur valide signifie que le jeton du canal est faux. Si success vaut true, la cause probable est une condition : livemode == true bloque tous les événements de test, par conception.

Stripe signale 405 Method Not Allowed. Le canal a POST Only activé et quelque chose a envoyé un GET. Stripe poste toujours : c'est donc un aperçu de lien ou un onglet de navigateur, pas Stripe.

La notification arrive avec des champs vides. Le modèle vise le mauvais objet — {{data.object.amount}} sur un canal de factures, où le champ s'appelle amount_due. Envoyez un vrai événement, ouvrez-le dans le tableau de bord et lisez le JSON.

Les livraisons échouent après des semaines de fonctionnement. Vérifiez le certificat et toute redirection devant l'URL. Avec une URL de canal utilisée en direct, c'est rare ; avec un relais que vous avez déployé, c'est le suspect habituel.

Questions fréquentes

Stripe peut-il m'appeler quand un litige s'ouvre ?

Pas tout seul. Stripe vous prévient par e-mail, dans le tableau de bord, via l'événement charge.dispute.created, et par push si vous utilisez l'application Stripe Dashboard. Pour une vraie sonnerie, routez cet événement vers un canal dont le type d'abonnement est Appel.

Faut-il écrire du code pour relier Stripe à Echobell ?

Non. Stripe poste du JSON vers n'importe quelle URL HTTPS publique, et une URL de canal Echobell en est une. Le code n'est nécessaire que si vous voulez vérifier l'en-tête Stripe-Signature ou reformater les montants.

Est-il sûr de donner à Stripe une URL de webhook tierce ?

C'est un arbitrage assumé. Le payload envoyé par Stripe contient des métadonnées client et paiement, et Echobell ne conserve pas durablement les payloads bruts — la notification rendue vit sur votre appareil. Ce que vous abandonnez, c'est la vérification de signature : qui connaît l'URL peut vous envoyer un faux convaincant. Traitez-la comme une clé d'API et utilisez le relais pour tout ce que vous préférez vérifier.

Pourquoi mon alerte affiche-t-elle 4900 au lieu de 49,00 $ ?

Stripe envoie les montants en entiers dans la plus petite unité monétaire, et les modèles Echobell ne font pas d'arithmétique. Indiquez l'unité dans le modèle, ou divisez par 100 dans un relais avant l'envoi.

Comment empêcher les événements de test de me réveiller ?

Ajoutez la condition livemode == true au canal. Stripe marque tous les événements de bac à sable et de stripe trigger en livemode: false.

Mon cofondateur peut-il recevoir les mêmes alertes sans payer un siège ?

Oui. Partagez le lien du canal ; chaque abonné choisit son type de notification. L'un peut prendre les litiges en appel pendant que l'autre les reçoit en push normal, et il n'y a pas de tarification par siège pour les abonnés.

Faut-il alerter sur les paiements réussis ?

Brièvement seulement, et uniquement tant que l'activité est assez petite pour que chacun reste un événement. Dès que la notification de paiement réussi devient une routine, elle commence à éroder votre réaction à celles qui comptent — le mécanisme central de la fatigue d'alerte.

Conclusion

Tout le montage tient en une destination d'événements Stripe par canal Echobell, une condition livemode == true, et la discipline de réserver le niveau appel aux événements munis d'un chrono. Les litiges et les alertes précoces de fraude en ont un. Un renouvellement échoué sur une offre à 9 $, non — et faire semblant du contraire est exactement la façon dont on finit par dormir pendant celui qui en avait un.

Téléchargez Echobell pour iPhone ou récupérez-le sur Google Play, créez d'abord le canal des litiges, et lancez un stripe trigger charge.dispute.created avant de confier quoi que ce soit de sérieux à ce chemin.


À lire aussi