Guides · Logique et bugs · Sécurité

Stripe : votre application sait-elle vraiment qu'un paiement a réussi ?

Beaucoup d'applications considèrent qu'un paiement a réussi parce que la page de retour de Stripe s'est affichée. Une adresse tapée à la main suffit alors à obtenir ce qui n'a pas été payé. Voici comment vérifier la vôtre.

15 minutesApplications qui encaissent avec Stripe Checkout ou Payment LinksPublié le 4 octobre 2026

Le piège : la page de retour n'est pas une preuve

Avec Stripe Checkout, le client paie sur une page de Stripe, puis il est renvoyé vers votre application, sur une adresse de retour (la success_url). Il est tentant d'écrire, sur cette page : « le client est revenu, donc il a payé, je confirme sa commande ».

C'est faux. N'importe qui peut ouvrir cette adresse sans avoir payé, en la tapant à la main ou en annulant le paiement puis en revenant en arrière. Si votre application confirme la commande à ce moment-là, elle offre ce qu'elle vend.

La preuve fiable vient de Stripe lui-même : un événement signé envoyé par Stripe à votre serveur (le webhook, dont votre serveur vérifie la signature avec un secret que seuls Stripe et vous connaissez), ou une vérification faite par votre serveur auprès de Stripe (il relit la session de paiement et contrôle qu'elle est bien payée). Jamais le simple fait que la page de retour s'affiche.

C'est le constat B de notre diagnostic de Studio Kerelune : la page de retour enregistrait elle-même un paiement « réussi » et confirmait la réservation.

Vérification 1 : qui décide qu'une commande est payée ?

Dans le code, trouvez l'endroit où une commande, une réservation ou un abonnement passe à l'état « payé ». Cherchez des mots comme paid, succeeded, confirmed, status, ou le nom de votre table de paiements.

  • Si ce changement se fait dans le code du navigateur (un composant de page, un fichier du dossier src de votre interface), c'est le piège décrit plus haut.
  • S'il se fait dans une fonction serveur appelée par Stripe (un fichier nommé webhook, stripe-webhook, ou une route d'API), c'est la bonne architecture : passez à la vérification 2.

Sur Supabase, regardez aussi les règles d'accès de la table des paiements : si une règle autorise un client à créer ou modifier ses propres lignes de paiement, il peut s'écrire un paiement réussi lui-même, même sans passer par votre interface.

Vérification 2 : le test, en mode test Stripe

Sur votre propre application, en mode test Stripe (avec les cartes de test, sans argent réel) :

  1. lancez un paiement, puis annulez-le sur la page Stripe ;
  2. ouvrez à la main l'adresse de retour de succès, telle qu'elle apparaît dans votre code, avec un identifiant inventé ;
  3. regardez l'état de la commande.

Si la commande est confirmée, votre application se fie à la page de retour. Si elle reste « en attente » jusqu'à ce que Stripe confirme, elle est correcte.

Vérification 3 : le webhook vérifie-t-il la signature ?

Ouvrez la fonction qui reçoit les événements de Stripe. Elle doit vérifier la signature avant de lire quoi que ce soit, avec une ligne de ce type :

const event = stripe.webhooks.constructEvent(corps, signature, secretWebhook);
// sur Deno ou un environnement asynchrone : await stripe.webhooks.constructEventAsync(...)

corps doit être le texte brut de la requête (await req.text()), pas le JSON déjà décodé : c'est la cause habituelle des erreurs de signature, et de la tentation de commenter la vérification.

Signaux d'alarme :

  • l'événement est lu directement (await req.json()) sans vérification ;
  • la vérification est commentée, souvent avec une note du genre « faisait planter en local » ;
  • la variable du secret (STRIPE_WEBHOOK_SECRET, qui commence par whsec_) n'est définie nulle part.

Sans vérification, n'importe qui peut envoyer à votre webhook un faux événement « paiement réussi ».

Vérification 4 : doublons, échecs et remboursements

Stripe peut envoyer plusieurs fois le même événement. Votre code doit le reconnaître (en gardant l'identifiant de l'événement) pour ne pas créditer deux fois. Vérifiez aussi que vous traitez :

  • l'échec d'un paiement (payment_intent.payment_failed) ;
  • le remboursement (charge.refunded), qui doit repasser la commande à l'état remboursé ;
  • avec Checkout, le statut payment_status dans checkout.session.completed, et checkout.session.async_payment_failed pour les paiements différés comme le prélèvement SEPA ;
  • et, pour les abonnements, l'échec de prélèvement (invoice.payment_failed) et la fin d'abonnement (customer.subscription.deleted).

Côté remboursement, regardez qui peut le déclencher. Une fonction qui rembourse la commande qu'on lui indique, sans vérifier que l'appelant en est le propriétaire ou un administrateur, permet à un client de rembourser les commandes des autres : c'est le constat G de Studio Kerelune.

Vérification 5 : le rapprochement

Une fois par mois, comparez la liste des paiements du tableau de bord Stripe et celle de votre base, sur la même période. Elles doivent correspondre exactement. Un paiement présent en base et absent de Stripe, c'est une commande obtenue sans payer. L'inverse, c'est un client qui a payé et que votre application ignore.

Si vous avez trouvé un problème

La correction suit toujours le même principe : Stripe décide, votre serveur enregistre, la page attend. Ce que Stripe confirme, par le webhook signé ou par une vérification de votre serveur, devient la seule source de vérité ; les règles d'accès interdisent au navigateur d'écrire dans la table des paiements ; la page de retour se contente d'afficher « paiement en cours de confirmation » jusqu'à ce que la commande change d'état.

Ce que cette vérification ne couvre pas

Les montants eux-mêmes (le prix envoyé à Stripe doit être relu côté serveur, jamais pris dans la page), la TVA et les factures, les codes promotionnels, et l'ensemble des autres axes : données, sécurité, montée en charge. C'est ce que couvre notre diagnostic.

Dans notre grille : La chaîne de paiement de bout en bout : signature des événements reçus du prestataire, doublons, droits ouverts côté serveur, rapprochement.
Voir les soixante questions
Vingt minutes

Vous préférez qu'on regarde avec vous ?

Le diagnostic couvre ce guide et toute la grille, soixante questions sur sept axes, pour 2 900 € HT : premiers constats sous 48 heures, rapport sous cinq jours ouvrés.