Construire les paiements par abonnement : Stripe du design à la production
Les paiements sont la partie la plus impitoyable de tout produit SaaS — un bug ici signifie des revenus perdus. Nous avons intégré le flux d'abonnement complet de Stripe dans une architecture microservices, y compris la logique de retry des webhooks, l'agrégation de facturation, et les cas limites dont personne ne vous prévient.
Les paiements sont l'endroit où vous ne pouvez pas vous permettre un échec silencieux. Un webhook manqué, une double facturation, un abonnement activé mais non provisionné — tout cela coûte de l'argent réel et de vrais utilisateurs. Chez Fygurs, nous avons intégré le flux d'abonnement Stripe complet dans notre architecture microservices. L'API Gateway gère la vérification des webhooks ; le service Payment possède chaque appel SDK Stripe. Voici l'architecture et les cas limites qui nous ont forcés à la concevoir ainsi.
Qu'est-ce que Stripe ?
Stripe est une plateforme d'infrastructure de paiement qui gère la complexité des paiements en ligne. Au lieu de construire vous-même le traitement des paiements, la détection des fraudes et les systèmes de conformité, Stripe fournit des APIs qui abstraient ces préoccupations en simples appels de fonctions.
Pourquoi Stripe pour un SaaS ?
- Facturation par abonnement : Support natif des paiements récurrents, des périodes d'essai et des changements de plan
- Paiements mondiaux : Acceptez plus de 135 devises avec conversion automatique
- Conformité PCI : Stripe gère les données de carte sensibles, réduisant votre charge de conformité
- Expérience développeur : Documentation excellente, SDKs pour chaque langage et outils de test
- Pages hébergées : Checkout et Customer Portal prêts à l'emploi réduisent le travail frontend
Concepts Fondamentaux de Stripe
Comprendre le modèle de données de Stripe est essentiel avant l'intégration. Ces objets forment la base de tout système de paiement.
RELATIONS ENTRE OBJETS STRIPE
┌──────────────┐
│ Customer │ ────────────────────────────────────┐
│ cus_xxx │ │
└──────┬───────┘ │
│ possède plusieurs │ appartient à
▼ ▼
┌───────────────┐ ┌───────────────┐ ┌─────────────┐
│ PaymentMethod │ │ Subscription │────────▶│ Invoice │
│ pm_xxx │ │ sub_xxx │génère │ in_xxx │
└───────────────┘ └───────┬───────┘ └─────────────┘
│
│ contient
▼
┌─────────────┐
│ Price │
│ price_xxx │
└──────┬──────┘
│ appartient à
▼
┌─────────────┐
│ Product │
│ prod_xxx │
└─────────────┘
Produits et Prix
Stripe sépare ce que vous vendez (Products) de combien ça coûte (Prices). Un seul produit peut avoir plusieurs prix pour différentes périodes de facturation, devises ou niveaux.
- Product : Représente votre offre (ex : "Plan Pro", "Plan Enterprise")
- Price : Définit le coût et la fréquence de facturation (ex : 29€/mois, 290€/an)
// Un produit, plusieurs prix
const product = { id: 'prod_xxx', name: 'Plan Pro' };
const monthlyPrice = {
id: 'price_monthly',
product: 'prod_xxx',
unit_amount: 2900, // 29,00€
recurring: { interval: 'month' }
};
const yearlyPrice = {
id: 'price_yearly',
product: 'prod_xxx',
unit_amount: 29000, // 290,00€ (2 mois offerts)
recurring: { interval: 'year' }
};
Customers
Un objet Customer stocke les méthodes de paiement, les informations de facturation et les liens vers les abonnements. Créez toujours un customer Stripe pour chaque utilisateur ou entreprise de votre système.
Subscriptions
Les abonnements gèrent la facturation récurrente automatiquement. Stripe crée des factures, débite les méthodes de paiement et gère les échecs avec une logique de réessai configurable.
- Statut : active, past_due, canceled, trialing, incomplete
- Cycle de facturation : La date d'ancrage détermine quand les factures sont générées
- Proratisation : Crédit/débit automatique lors d'un changement de plan en cours de cycle
Payment Methods
Les méthodes de paiement représentent l'instrument de paiement d'un client : cartes, comptes bancaires ou portefeuilles numériques (Apple Pay, Google Pay). Elles sont stockées en toute sécurité par Stripe et référencées par ID.
Invoices
Les factures sont générées automatiquement pour les abonnements ou manuellement pour les charges ponctuelles. Elles tracent ce qui a été facturé, quand, et fournissent des reçus PDF.
Fonctionnalités Stripe pour SaaS
Sessions de Checkout
Stripe Checkout est une page de paiement hébergée qui gère le flux de paiement complet : saisie de la carte, validation, 3D Secure, Apple Pay et Google Pay. Vous redirigez les utilisateurs vers Checkout et recevez un webhook quand le paiement est terminé.
FLUX DE CHECKOUT Votre App Stripe Checkout Votre App │ │ │ │ 1. Créer une Session │ │ │ ───────────────────────▶ │ │ │ │ │ │ 2. Rediriger vers l'URL │ │ │ ◀─────────────────────── │ │ │ │ │ │ L'utilisateur saisit ses coordonnées │ │ │ │ │ │ 3. Webhook: completed │ │ │ ───────────────────────▶│ │ │ │ │ 4. Redirection success_url │ │ │ ◀───────────────────────────┼──────────────────────────│
Customer Portal
Une page hébergée où les clients peuvent gérer leurs abonnements, mettre à jour leurs méthodes de paiement, consulter leurs factures et annuler leurs plans. Aucun code frontend requis.
const session = await stripe.billingPortal.sessions.create({
customer: 'cus_xxx',
return_url: 'https://votre-app.fr/facturation',
});
// Rediriger l'utilisateur vers session.url
Webhooks
Les webhooks notifient votre application lorsque des événements se produisent dans Stripe : paiements réussis, charges échouées, changements d'abonnement ou litiges. Ils sont essentiels pour maintenir votre base de données synchronisée avec l'état de Stripe.
Événements webhook clés pour un SaaS :
checkout.session.completed— Paiement réussi, provisionner l'accèscustomer.subscription.updated— Plan modifié, mettre à jour les droitscustomer.subscription.deleted— Abonnement terminé, révoquer l'accèsinvoice.payment_failed— Paiement échoué, notifier l'utilisateurinvoice.paid— Paiement récurrent réussi
Mode Test
Stripe fournit un environnement de test complet avec des clés API séparées. Le mode test utilise de faux numéros de carte et simule tous les scénarios de paiement sans vraies charges.
# Numéros de carte de test
4242424242424242 # Paiement réussi
4000000000000002 # Carte refusée
4000002500003155 # Nécessite 3D Secure
Architecture Microservices
Avec les fondamentaux couverts, implémentons Stripe dans une architecture microservices NestJS. Le service Payment est isolé de l'API Gateway, garantissant que la logique de paiement, les clés API sensibles et les dépendances du SDK Stripe sont contenues dans un seul service.
ARCHITECTURE MICROSERVICES STRIPE
┌─────────────┐
│ Client │
│ (Next.js) │
└──────┬──────┘
│ REST
▼
┌─────────────────────────────────────────────────────────────────────┐
│ API Gateway │
│ │
│ ┌───────────────────┐ ┌────────────────────┐ │
│ │ StripeController │ │ WebhookController │ │
│ │ POST /checkout │ │ POST /webhook │ │
│ │ GET /products │ │ │ │
│ └─────────┬─────────┘ └──────────┬─────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌───────────────────┐ ┌────────────────────┐ │
│ │ StripeService │ │ WebhookService │ │
│ │ (ClientProxy) │ │ (Vérif. Signature) │ │
│ └─────────┬─────────┘ └──────────┬─────────┘ │
│ │ │ │
└─────────────┼───────────────────────────────────┼───────────────────┘
│ TCP │ TCP
▼ ▼
┌─────────────────────────────────────────────────────────────────────┐
│ Microservice Payment │
│ │
│ ┌───────────────────┐ ┌────────────────────┐ │
│ │ StripeController │ │ WebhooksController │ │
│ │ @MessagePattern │ │ @MessagePattern │ │
│ └─────────┬─────────┘ └──────────┬─────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌───────────────────┐ ┌────────────────────┐ │
│ │ StripeService │ │ WebhooksService │ │
│ │ (Stripe SDK) │ │ (Event Handlers) │ │
│ └─────────┬─────────┘ └──────────┬─────────┘ │
│ │ │ │
└─────────────┼───────────────────────────────────┼───────────────────┘
│ │
└───────────────┬───────────────────┘
│ Prisma
▼
┌─────────────────┐
│ PostgreSQL │
└─────────────────┘
Flux de Paiement
Le flux de paiement complet implique trois étapes : créer une session de checkout, le paiement de l'utilisateur et le traitement du webhook.
FLUX DE PAIEMENT
1. CRÉER UNE SESSION DE CHECKOUT
┌────────┐ ┌─────────────┐ ┌─────────────────┐ ┌────────┐
│ Client │─────▶│ API Gateway │─────▶│ Service Payment │─────▶│ Stripe │
│ │ REST │ │ TCP │ (Stripe SDK) │ API │ │
└────────┘ └─────────────┘ └─────────────────┘ └───┬────┘
│
◀───────────────────────────────────────────┘
Retourne l'URL de Checkout
2. L'UTILISATEUR COMPLÈTE LE PAIEMENT
┌────────┐ ┌──────────────────┐
│ Client │─────▶│ Stripe Checkout │ (Page de paiement hébergée)
│ │ │ Saisit carte │
└────────┘ └──────────────────┘
3. NOTIFICATION WEBHOOK
┌────────┐ POST /webhook ┌─────────────┐ TCP ┌─────────────────┐
│ Stripe │───────────────▶│ API Gateway │───────▶│ Service Payment │
│ │ │ (vérif.) │ │ (traite l'event)│
└────────┘ └─────────────┘ └─────────────────┘
Étape 1 : Créer la Session de Checkout
Le client demande une session de checkout. L'API Gateway transmet la requête au microservice Payment, qui utilise le SDK Stripe pour créer une session et retourne l'URL de checkout.
Étape 2 : L'Utilisateur Complète le Paiement
Le client est redirigé vers la page de checkout hébergée par Stripe. L'utilisateur saisit ses coordonnées de paiement et finalise la transaction. Stripe gère la conformité PCI, le 3D Secure et la détection de fraude.
Étape 3 : Notification Webhook
Après un paiement réussi, Stripe envoie un webhook checkout.session.completed à votre API Gateway. La gateway vérifie la signature et transmet l'événement au microservice Payment, qui provisionne l'accès, sauvegarde l'abonnement et envoie des emails de confirmation.
Configuration du Module Dynamique
Le SDK Stripe nécessite une clé API à l'initialisation. L'utilisation des Dynamic Modules NestJS avec forRootAsync() permet une injection sécurisée depuis les variables d'environnement.
// stripe.module.ts
import { Module, DynamicModule } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { StripeService } from './stripe.service';
import { StripeController } from './stripe.controller';
@Module({})
export class StripeModule {
static forRootAsync(): DynamicModule {
return {
module: StripeModule,
imports: [ConfigModule.forRoot()],
controllers: [StripeController],
providers: [
StripeService,
{
provide: 'STRIPE_API_KEY',
useFactory: (configService: ConfigService) =>
configService.get('STRIPE_API_KEY'),
inject: [ConfigService],
},
],
exports: [StripeService],
};
}
}
Initialisation du Service
// stripe.service.ts (Microservice Payment)
import { Inject, Injectable, Logger } from '@nestjs/common';
import Stripe from 'stripe';
@Injectable()
export class StripeService {
private stripe: Stripe;
private readonly logger = new Logger(StripeService.name);
constructor(
@Inject('STRIPE_API_KEY')
private readonly apiKey: string,
) {
this.stripe = new Stripe(this.apiKey, {
apiVersion: '2024-12-18.acacia', // Épingler la version d'API
});
}
}
Épingler la apiVersion évite les changements breaking quand Stripe met à jour son API. Consultez le changelog Stripe avant de mettre à niveau.
Couche API Gateway
L'API Gateway expose les endpoints REST et transfère les requêtes au microservice Payment via TCP en utilisant NestJS ClientProxy.
Contrôleur
// stripe.controller.ts (API Gateway)
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import { StripeService } from './stripe.service';
import { CreateSubscriptionDto } from './dto/create-subscription.dto';
@Controller('stripe')
export class StripeController {
constructor(private readonly stripeService: StripeService) {}
@Get('products')
async getProducts() {
return this.stripeService.getProducts();
}
@Get('customers')
async getCustomers() {
return this.stripeService.getCustomers();
}
@Post('subscriptions')
async createSubscription(@Body() dto: CreateSubscriptionDto) {
return this.stripeService.createSubscription(dto.customerId, dto.priceId);
}
}
Service (ClientProxy)
// stripe.service.ts (API Gateway)
import { HttpException, Inject, Injectable } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
@Injectable()
export class StripeService {
constructor(@Inject('PAYMENT') private paymentClient: ClientProxy) {}
async getProducts() {
try {
return await this.paymentClient
.send('getProducts', {})
.toPromise();
} catch (error) {
throw new HttpException(error, 400);
}
}
async createSubscription(customerId: string, priceId: string) {
try {
return await this.paymentClient
.send('createSubscription', { customerId, priceId })
.toPromise();
} catch (error) {
throw new HttpException(error, 400);
}
}
}
Microservice Payment
Le microservice Payment gère toutes les opérations SDK Stripe en utilisant les décorateurs @MessagePattern pour recevoir les messages de l'API Gateway.
Contrôleur
// stripe.controller.ts (Microservice Payment)
import { Controller } from '@nestjs/common';
import { MessagePattern, Payload } from '@nestjs/microservices';
import { StripeService } from './stripe.service';
@Controller()
export class StripeController {
constructor(private readonly stripeService: StripeService) {}
@MessagePattern('getProducts')
async getProducts() {
return this.stripeService.getProducts();
}
@MessagePattern('getCustomers')
async getCustomers() {
return this.stripeService.getCustomers();
}
@MessagePattern('createSubscription')
async createSubscription(
@Payload() data: { customerId: string; priceId: string },
) {
return this.stripeService.createSubscription(data.customerId, data.priceId);
}
@MessagePattern('createCustomer')
async createCustomer(@Payload() data: { email: string; name: string }) {
return this.stripeService.createCustomer(data.email, data.name);
}
}
Service (Stripe SDK)
// stripe.service.ts (Microservice Payment)
@Injectable()
export class StripeService {
private stripe: Stripe;
private readonly logger = new Logger(StripeService.name);
constructor(
@Inject('STRIPE_API_KEY') private readonly apiKey: string,
private prisma: PrismaService,
) {
this.stripe = new Stripe(this.apiKey, {
apiVersion: '2024-12-18.acacia',
});
}
async getProducts(): Promise {
const products = await this.stripe.products.list();
this.logger.log('Produits récupérés avec succès');
return products.data;
}
async createCustomer(email: string, name: string): Promise {
const customer = await this.stripe.customers.create({ email, name });
this.logger.log(`Customer créé : ${email}`);
return customer;
}
async createSubscription(
customerId: string,
priceId: string,
): Promise {
const subscription = await this.stripe.subscriptions.create({
customer: customerId,
items: [{ price: priceId }],
});
this.logger.log(`Abonnement créé pour le customer ${customerId}`);
return subscription;
}
}
Architecture des Webhooks
Les webhooks sont essentiels pour réagir aux événements Stripe asynchrones. Nous implémentons une architecture à deux niveaux : l'API Gateway vérifie les signatures, et le microservice Payment traite les événements.
FLUX WEBHOOK
Stripe Cloud
│
│ POST /webhook
│ En-têtes : stripe-signature
│ Corps : Buffer brut
▼
┌─────────────────────────────────────────────────────┐
│ API Gateway │
│ │
│ 1. Valider la présence de l'en-tête stripe-sig │
│ 2. Vérifier que rawBody est un Buffer │
│ 3. constructEvent(rawBody, sig, webhookSecret) │
│ 4. Rejeter les signatures invalides (400) │
│ 5. Transmettre l'événement Stripe vérifié │
│ │
└────────────────────────┬────────────────────────────┘
│ TCP (événement vérifié)
▼
┌─────────────────────────────────────────────────────┐
│ Microservice Payment │
│ │
│ switch (event.type) { │
│ case 'checkout.session.completed': ... │
│ case 'customer.subscription.updated': ... │
│ case 'customer.subscription.deleted': ... │
│ case 'invoice.payment_failed': ... │
│ } │
│ │
└─────────────────────────────────────────────────────┘
Gateway : Vérification de Signature
// webhook.controller.ts (API Gateway)
import { Controller, Post, Req, Headers, BadRequestException } from '@nestjs/common';
import { Request } from 'express';
import { WebhookService } from './webhook.service';
@Controller('webhook')
export class WebhookController {
constructor(private readonly webhookService: WebhookService) {}
@Post()
async handleStripeWebhook(
@Req() req: Request,
@Headers('stripe-signature') sig: string,
) {
if (!sig) {
throw new BadRequestException('En-tête stripe-signature manquant');
}
const rawBody = req.body;
if (!Buffer.isBuffer(rawBody)) {
throw new BadRequestException('Format de corps de requête invalide');
}
return this.webhookService.handleWebhook(rawBody, sig);
}
}
// webhook.service.ts (API Gateway)
import { HttpException, Inject, Injectable, Logger } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import Stripe from 'stripe';
@Injectable()
export class WebhookService {
private readonly logger = new Logger(WebhookService.name);
private stripe: Stripe;
constructor(@Inject('PAYMENT') private paymentClient: ClientProxy) {
this.stripe = new Stripe(process.env.STRIPE_API_KEY, {
apiVersion: '2024-12-18.acacia',
});
}
async handleWebhook(rawBody: Buffer, sig: string) {
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
let event: Stripe.Event;
try {
event = this.stripe.webhooks.constructEvent(rawBody, sig, webhookSecret);
this.logger.log(`Webhook vérifié : ${event.type}`);
} catch (err) {
this.logger.error(`Échec de vérification de signature : ${err.message}`);
throw new HttpException('Signature invalide', 400);
}
// Transmettre l'événement vérifié au microservice Payment
return this.paymentClient.send('handleWebhook', event).toPromise();
}
}
Middleware Corps Brut
Stripe nécessite le corps brut de la requête pour la vérification de signature. Configurez Express pour préserver le corps brut pour l'endpoint webhook :
// main.ts
import { NestFactory } from '@nestjs/core';
import * as bodyParser from 'body-parser';
async function bootstrap() {
const app = await NestFactory.create(AppModule, { bodyParser: false });
// Corps brut pour les webhooks Stripe
app.use('/webhook', bodyParser.raw({ type: 'application/json' }));
// JSON pour tout le reste
app.use(bodyParser.json());
await app.listen(3000);
}
Microservice : Traitement des Événements
// webhooks.service.ts (Microservice Payment)
@Injectable()
export class WebhooksService {
constructor(
@Inject('STRIPE_API_KEY') private readonly apiKey: string,
private readonly prisma: PrismaService,
) {
this.stripe = new Stripe(this.apiKey, {
apiVersion: '2024-12-18.acacia',
});
}
async handleWebhook(event: Stripe.Event) {
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object;
// 1. Vérifier l'idempotence (éviter le traitement en double)
// 2. Sauvegarder l'abonnement en base de données
// 3. Provisionner l'accès / assigner le plan
// 4. Envoyer un email de confirmation
break;
}
case 'customer.subscription.updated': {
const subscription = event.data.object;
// 1. Mettre à jour le statut de l'abonnement en BDD
// 2. Gérer les montées/descentes de plan
// 3. Ajuster les droits utilisateur
break;
}
case 'customer.subscription.deleted': {
const subscription = event.data.object;
// 1. Marquer l'abonnement comme annulé en BDD
// 2. Révoquer l'accès ou rétrograder au plan gratuit
// 3. Envoyer un email d'annulation
break;
}
case 'invoice.payment_failed': {
const invoice = event.data.object;
// 1. Enregistrer l'échec de paiement
// 2. Notifier l'utilisateur de mettre à jour sa méthode de paiement
// 3. Optionnellement suspendre l'accès après épuisement des réessais
break;
}
default:
this.logger.warn(`Type d'événement non géré : ${event.type}`);
}
}
}
Sessions de Checkout
Stripe Checkout fournit une page de paiement hébergée avec support natif des cartes, Apple Pay, Google Pay et l'authentification 3D Secure.
async createCheckoutSession(priceId: string, userId: string) {
const session = await this.stripe.checkout.sessions.create({
mode: 'subscription',
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.APP_URL}/facturation?success=true`,
cancel_url: `${process.env.APP_URL}/facturation?canceled=true`,
metadata: { userId }, // Lier le paiement à votre utilisateur
});
return { url: session.url };
}
Sécurité : Ne faites jamais confiance à la redirection success_url pour accorder l'accès. Reposez-vous toujours sur le webhook checkout.session.completed pour confirmer le paiement.
Cycle de Vie des Abonnements
Mise à Niveau de Plan
async upgradeSubscription(subscriptionId: string, newPriceId: string) {
const subscription = await this.stripe.subscriptions.retrieve(subscriptionId);
const itemId = subscription.items.data[0].id;
return this.stripe.subscriptions.update(subscription.id, {
items: [{ id: itemId, price: newPriceId }],
proration_behavior: 'always_invoice', // Facturer la différence immédiatement
});
}
Annulation d'Abonnements
async cancelSubscription(subscriptionId: string, immediate: boolean = false) {
if (immediate) {
// Annuler maintenant, révoquer l'accès immédiatement
return this.stripe.subscriptions.cancel(subscriptionId);
} else {
// Annuler à la fin de la période, l'utilisateur garde l'accès jusque-là
return this.stripe.subscriptions.update(subscriptionId, {
cancel_at_period_end: true,
});
}
}
Bonnes Pratiques
Sécurité
- Épinglez les versions d'API : Spécifiez
apiVersionpour éviter les changements breaking - Vérifiez les webhooks : Validez toujours les signatures avant de traiter les événements
- Isolez les secrets : Gardez les clés Stripe dans le microservice Payment uniquement
- Utilisez les métadonnées : Liez les objets Stripe à vos entités métier (userId)
Fiabilité
- Idempotence : Vérifiez si les événements ont déjà été traités pour éviter les doublons
- Expandez les objets : Utilisez
expandpour réduire les appels API - Gérez les échecs : Enregistrez les échecs de paiement et notifiez les utilisateurs
- Testez les webhooks : Utilisez Stripe CLI pour déclencher des événements localement
Test avec Stripe CLI
# Installer Stripe CLI
brew install stripe/stripe-cli/stripe
# Se connecter à votre compte
stripe login
# Transférer les webhooks vers le serveur local
stripe listen --forward-to localhost:3000/webhook
# Déclencher des événements de test
stripe trigger checkout.session.completed
stripe trigger customer.subscription.updated
stripe trigger invoice.payment_failed
Conclusion
L'intégration de Stripe dans une architecture microservices NestJS sépare efficacement les responsabilités : l'API Gateway gère le routage HTTP et la vérification des webhooks, tandis que le microservice Payment possède toutes les opérations SDK Stripe et la logique métier.
L'architecture webhook à deux niveaux garantit que les requêtes invalides sont rejetées à la gateway avant d'atteindre votre logique de paiement. Combiné aux vérifications d'idempotence et à une gestion correcte des erreurs, cette configuration gère le cycle de vie complet des abonnements, du checkout à l'annulation.
Écrit par

Tech Lead et Ingénieur Full Stack pilotant une équipe de 5 ingénieurs chez Fygurs (Paris, Remote) sur un SaaS cloud-native Azure. Diplômé de 1337 Coding School (42 Network / UM6P). Écrit sur l'architecture, l'infrastructure cloud et le leadership technique.