Blog ·
Stripe pour l’hébergement : quand un paiement est à vous
Réussi ne veut pas dire encaissé, un litige n’est pas un remboursement, et la relance est un réglage de tableau de bord. Facturer un hébergement sur Stripe.
Par Provibr
Facturer de l’hébergement n’est pas du commerce en ligne. Une boutique encaisse une fois et expédie un carton ; vous encaissez chaque mois et, entre-temps, vous maintenez une machine en marche. Cette différence se manifeste en un seul endroit, et c’est celui que toute intégration rate en premier : décider à partir de quand un paiement est assez réel pour continuer à livrer.
Voici ce que nous avons rencontré en construisant la facturation récurrente sur Stripe. Rien n’y est exotique, et l’essentiel reste invisible jusqu’au mois où un paiement s’inverse.
« Réussi » ne veut pas dire « encaissé »
Un paiement validé n’est pas de l’argent. La carte et iDEAL sont assez proches de l’instantané pour que l’on puisse prétendre le contraire. Le prélèvement SEPA ne l’est pas, et le virement bancaire non plus.
Après validation, un paiement SEPA reste en processing pendant des jours, sans que rien n’ait été reçu. Il finit généralement par arriver. Parfois non. Si votre intégration traite la validation comme le moment de vérité, vous avez provisionné un serveur sur la foi d’une intention.
Le modèle d’état n’est donc pas un booléen. Nous avons abouti à six phases, et une seule d’entre elles signifie « livrer » :
| Phase | Ce que cela signifie | Livrer ? |
|---|---|---|
| pending | Démarré, rien reçu pour l’instant | Non |
| settled | Argent reçu et acquis | Oui |
| failed | Cela n’arrivera pas | Non |
| disputed | Le client le conteste | Non, mais cela peut revenir |
| reversed | Rétrofacturé ou remboursé | Non, définitivement |
| unknown | Nous n’avons pas pu le déterminer | Non |
Faire en sorte que « livrable » signifie précisément et uniquement settled ne coûte rien à écrire et vous offre cette propriété : aucune autre partie du code n’a le droit d’avoir un avis là-dessus. Partout ailleurs, on pose une seule question.
L’ordre des événements n’est pas garanti : rendez illégales les transitions illégales
Les webhooks arrivent dans le désordre. Pas souvent, mais cela arrive, et la défaillance est silencieuse : un événement périmé écrase un état plus récent, et le service d’un client soit s’arrête sans raison, soit continue de tourner après une rétrofacturation.
La défense bon marché consiste à écrire quelles transitions sont impossibles et à les refuser. Un paiement encaissé ne peut pas redevenir en attente, un paiement encaissé ne peut pas devenir échoué, un paiement en attente ne peut pas sauter directement à contesté, et rien ne quitte jamais les états inversé ou échoué. Un événement tardif qui provoquerait l’une de ces transitions est écarté plutôt qu’appliqué.
La seconde défense est architecturale, et c’est la décision que nous garderions dans n’importe quelle intégration de paiement.
Le webhook est un accélérateur, pas une source de vérité. Le nôtre vérifie la signature, déduplique, et pousse un seul identifiant d’organisation dans une file. C’est tout ce qu’il fait. Un scrutateur récupère ensuite l’état courant auprès de Stripe avant que quoi que ce soit ne change. La conséquence vaut bien la tuyauterie : un webhook manqué n’est jamais un paiement manqué, c’est un paiement plus lent. La livraison ne dépend pas d’un mécanisme de livraison que vous ne contrôlez pas.
La vérification de signature comporte trois détails qu’il est facile de rater subtilement :
- Comparez en temps constant, mais vérifiez d’abord les longueurs, car la comparaison à temps constant lève une exception sur des longueurs différentes au lieu de renvoyer faux.
- Acceptez plusieurs signatures dans l’en-tête. C’est ainsi que fonctionne la rotation du secret, et une implémentation qui ne lit que la première casse le jour où vous effectuez cette rotation.
- Rejetez une signature non hexadécimale avant de l’analyser comme telle, car l’analyse hexadécimale tronque en silence au premier caractère invalide, et une signature de deux caractères se comparera allègrement à rien du tout.
Nous dédupliquons aussi en réservant l’identifiant d’événement avant de publier le signal. Cela présente un inconvénient connu : réserver, puis échouer à publier, et l’indice est perdu. C’est acceptable précisément parce que la consultation est la source de vérité, ce qui illustre bien comment une décision d’architecture paie un compromis ailleurs.
La relance est un réglage de tableau de bord, pas un paramètre d’API
Si un paiement mensuel échoue, quelque chose doit le réessayer et quelque chose doit décider quand le service s’arrête. Nous avons d’abord écrit cette logique, puis nous en avons supprimé la plus grande partie.
Les Smart Retries de Stripe choisissent le calendrier des relances à partir de leurs propres données, et ce que devient un abonnement une fois toutes les relances échouées se configure dans le tableau de bord. C’est un réglage valable pour tout le compte plutôt qu’une valeur passée par abonnement, et le mode test et le mode réel se configurent séparément, ce qui est un bon moyen d’être surpris en production après une série de tests au vert.
Il ne reste alors qu’un seul travail de votre côté : traduire le statut de l’abonnement en ce que le client a le droit d’utiliser.
| Statut de l’abonnement | Ce que nous accordons | Pourquoi |
|---|---|---|
| active, trialing | Accès complet | |
| past_due | Grâce : une bannière, rien d’arrêté | Stripe relance encore |
| unpaid | Suspendu | Les relances sont épuisées |
| canceled | Suspendu | |
| incomplete | Accès complet, aucune restriction | Le premier paiement n’a jamais abouti |
| paused | Accès complet |
La ligne past_due est pilotée par le statut plutôt que par l’horloge, et c’est la simplification qui vaut la peine d’être prise. Dès lors que le prestataire possède le calendrier des relances, « depuis combien de jours cela échoue-t-il » cesse d’être votre question. La poser quand même revient à faire tourner un second minuteur contre un calendrier que vous ne voyez pas.
La ligne incomplete est celle qui mord. Elle désigne un abonnement dont le tout premier paiement n’est jamais passé, ce qui n’est pas la même chose qu’un client payant qui a cessé de payer. Les traiter de la même façon suspend rétroactivement un client en activité, sur la foi d’un événement portant sur un abonnement qui n’a jamais commencé.
Et une suspension est une pause, jamais une suppression. Le retour est une action accomplie par un être humain. Rien dans une machine à états de facturation ne devrait pouvoir détruire les données d’un client, si sûre d’elle soit-elle.
Le bogue qui rendait l’argent et laissait le service tourner
Voici la défaillance qui nous a le plus appris, et elle n’avait rien à voir avec le prestataire de paiement.
Une rétrofacturation avait correctement fait passer l’état de licence du client à bloqué. En moins de cinq minutes, il était revenu à actif. Le client avait son argent et ses serveurs.
La cause : une tâche de fond qui recalculait tout autre chose, le dépassement de ressources, émettait un UPDATE sur la même colonne toutes les cinq minutes sans clause WHERE. Trois morceaux de code distincts composaient le même champ, chacun correct pris isolément, et celui qui tournait le plus souvent l’emportait.
Le correctif fut structurel plutôt qu’un rafistolage : une seule fonction compose cette colonne, en prenant trois planchers en entrée, un venu de la facturation, un du dépassement, un des inversions, et rien d’autre ne l’écrit. La règle générale mérite d’être énoncée dans l’abstrait, car elle s’applique bien au-delà de la facturation. Si plus d’un rédacteur calcule la même colonne, cette colonne n’est pas une décision. C’est une course, et elle se tranchera en faveur de la tâche dont l’intervalle est le plus court.
Un litige n’est pas un remboursement, et un remboursement partiel n’est pas une demi-livraison
Voici deux distinctions dont la facturation d’hébergement a besoin et qu’un guide générique des paiements ne fera pas à votre place.
Un litige ouvert n’est pas une perte. Il peut être gagné. Le paiement cesse de compter comme livré, mais c’est une affirmation sur le paiement et non sur la licence : un litige ouvert correspond à la grâce, donc le client garde son service et vous recevez une notification. Suspendre quelqu’un qui va gagner son litige est une conversation de support dont vous ne voulez pas. Une rétrofacturation définitive est une perte, et celle-là correspond à la suspension.
Le remboursement partiel est le cas embarrassant. Il est tentant de le traiter comme une livraison partielle, et cela n’existe pas. Un mois d’abonnement est livré ou il ne l’est pas. Un remboursement partiel fait donc entièrement sortir le paiement de l’état encaissé, et la livraison s’arrête. C’est la lecture honnête, même si elle paraît dure, car l’alternative consiste à inventer une fraction de service que votre produit ne sait pas réellement fournir.
Si vous poussez des avoirs vers un système comptable, un avertissement de plus. Dans le nôtre, un avoir n’est pas une ressource distincte : c’est une facture à montants négatifs, et l’appel de duplication recopie le champ de référence de l’original. Cette référence est exactement la clé sur laquelle un contrôle d’idempotence effectue sa recherche : une implémentation naïve retrouve donc l’original au passage suivant, conclut que rien n’a été crédité, et émet un second avoir. De l’argent rendu deux fois. Chaque document généré a besoin de sa propre ancre, dérivée de l’événement qui l’a provoqué.
Ce qu’est ici une proforma, et pourquoi le compteur a deux préfixes
Une proforma est le document que produit la plateforme elle-même, avec son propre numéro, et c’est ce que le client voit dans le panneau. La facture légale est créée dans le système comptable, avec sa propre série. Sans ce système comptable connecté, la proforma est le seul document qui existe, et c’est pourquoi elle doit être numérotée correctement plutôt que traitée comme un reçu.
Deux détails sortis de la pratique :
Le numéro est complété à quatre chiffres au minimum, et non sur une largeur fixe. Le dix millième document en reçoit cinq. Tronquer pour garder un format bien rangé donnerait le même numéro à deux documents, et un index unique vous le fera découvrir au pire moment possible.
Un seul compteur, deux préfixes. Quand le préfixe a changé, la séquence a continué au lieu de repartir de zéro. Deux compteurs auraient produit un nouveau document dont le numéro ne diffère d’un ancien que par un préfixe de deux lettres, ce qu’une base de données voit comme unique et qu’un humain lisant un relevé bancaire ne voit pas.
Et un rechargement de crédit prépayé ne porte pas de TVA sur sa proforma, car un rechargement est un paiement d’avance et non un service. La TVA appartient à la facture de ce pour quoi le crédit finit par être dépensé. Inverser les deux revient à taxer deux fois le même argent.
Ce que nous avons pu mesurer, et ce que nous n’avons pas pu
La chose la plus utile dans tout cela est l’horloge de test. Un cycle de facturation mensuel peut être parcouru de bout en bout en quelques minutes : créer un paiement SEPA, confirmer que processing n’active absolument rien, avancer l’horloge au-delà de la fenêtre d’encaissement, le voir s’encaisser, et vérifier que l’activation a lieu exactement une fois et reste idempotente lors d’un second passage. Sans elle, le cycle mensuel est la seule chose que l’on ne peut pas répéter, et sa première exécution réelle a lieu en production.
Deux limites honnêtes à tout ce qui précède. Cela a été mesuré contre le bac à sable de Stripe et un webhook réel livré par le réseau, pas contre une large base de clients en production. Et une leçon qui dépasse les paiements : un drapeau de fonctionnalité posé sur un travailleur et sur un bouton ne verrouille pas un schéma de base de données. Un déploiement a livré un jour du code qui lisait des colonnes dont la migration n’avait pas été jouée, et une page d’administration sans rapport a renvoyé une erreur serveur pour tout le monde, car le drapeau était baissé mais la requête ne l’était pas.
Tout cela est sorti de la construction du volet facturation de Provibr, où les hébergeurs gèrent leur propre infrastructure depuis un seul panneau et où l’agent ne se connecte que vers l’extérieur.