Tout produit qui facture de l’argent finit par construire les mêmes briques : un flux de checkout, la gestion des abonnements, des handlers de webhooks et la logique de facturation. Stripe fournit tout ça sous forme d’API, avec un SDK TypeScript qui rend l’intégration type-safe de la création de session au traitement des événements. La vraie complexité n’est pas d’appeler les endpoints de Stripe. C’est de comprendre l’architecture : pourquoi les webhooks sont la source de vérité, comment fonctionnent les cycles de vie des abonnements, et où tracer la ligne entre l’UI hébergée de Stripe et la vôtre.
Sessions Checkout : le flux de paiement hébergé
Stripe Checkout est une page de paiement hébergée qui gère la validation de carte, le 3D Secure, Apple Pay, Google Pay et la conformité PCI. On crée une session côté serveur, on redirige l’utilisateur, et Stripe fait le reste. Aucun numéro de carte ne touche votre infrastructure.
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function createCheckoutSession(priceId: string, customerEmail: string) {
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
customer_email: customerEmail,
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.APP_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.APP_URL}/pricing`,
});
return session.url;
}
app.post('/api/checkout', async (req, res) => {
const { priceId, email } = req.body;
const url = await createCheckoutSession(priceId, email);
res.json({ url });
});
Le paramètre mode détermine s’il s’agit d’un paiement unique (payment) ou d’un prélèvement récurrent (subscription). La success_url inclut un template {CHECKOUT_SESSION_ID} que Stripe remplace par l’ID réel de la session, permettant de vérifier le paiement sur la page de succès. La clé secrète ne quitte jamais le serveur.
Webhooks : pourquoi les redirections ne suffisent pas
Un utilisateur qui complète le Checkout et atterrit sur la page de succès ne signifie pas que le paiement est passé. Il peut fermer l’onglet, perdre la connexion ou voir sa carte refusée après le 3D Secure. Les webhooks sont la façon dont Stripe informe votre serveur de ce qui s’est réellement passé, de manière asynchrone et fiable.
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function handleWebhook(payload: string, signature: string) {
const event = stripe.webhooks.constructEvent(
payload,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session;
await db.user.update({
where: { email: session.customer_email! },
data: {
stripeCustomerId: session.customer as string,
subscriptionStatus: 'active',
},
});
break;
}
case 'invoice.payment_failed': {
const invoice = event.data.object as Stripe.Invoice;
await db.user.update({
where: { stripeCustomerId: invoice.customer as string },
data: { subscriptionStatus: 'past_due' },
});
break;
}
case 'customer.subscription.deleted': {
const subscription = event.data.object as Stripe.Subscription;
await db.user.update({
where: { stripeCustomerId: subscription.customer as string },
data: { subscriptionStatus: 'canceled' },
});
break;
}
}
}
app.post('/api/webhooks/stripe', express.raw({ type: 'application/json' }), async (req, res) => {
try {
await handleWebhook(req.body, req.headers['stripe-signature'] as string);
res.json({ received: true });
} catch {
res.status(400).send('Invalid signature');
}
});
L’appel à constructEvent vérifie que le payload a été signé par Stripe. Sans cette vérification, n’importe qui pourrait forger des événements et manipuler votre base de données. Le middleware raw body est critique ici : Stripe signe le payload brut, pas le JSON parsé. Si Express le parse d’abord, la signature ne correspondra pas.
Trois événements couvrent la majorité du cycle de vie d’un abonnement. checkout.session.completed se déclenche quand un client finit de payer. invoice.payment_failed se déclenche quand un prélèvement de renouvellement échoue. customer.subscription.deleted se déclenche quand un abonnement prend fin, que ce soit par résiliation ou par échecs de paiement répétés.
Cycle de vie des abonnements : ce qui se passe après le checkout
Un abonnement n’est pas un événement unique. C’est une machine à états qui passe par active, past_due, canceled et unpaid au cours de sa vie. Comprendre ces transitions est ce qui sépare une intégration Stripe qui fonctionne d’une intégration prête pour la production.
export async function getSubscriptionStatus(customerId: string) {
const subscriptions = await stripe.subscriptions.list({
customer: customerId,
status: 'all',
limit: 1,
});
const subscription = subscriptions.data[0];
if (!subscription) return 'none';
return subscription.status;
}
export async function cancelSubscription(subscriptionId: string) {
await stripe.subscriptions.update(subscriptionId, {
cancel_at_period_end: true,
});
}
Utiliser cancel_at_period_end: true au lieu de supprimer l’abonnement immédiatement permet au client d’utiliser le service jusqu’à la fin de sa période de facturation en cours. C’est le comportement attendu pour la plupart des produits SaaS. Le webhook customer.subscription.updated se déclenche avec le flag cancel_at_period_end, et customer.subscription.deleted se déclenche quand la période se termine réellement.
Portail client : zéro UI de facturation à construire
Stripe héberge un portail client où les utilisateurs peuvent mettre à jour leur moyen de paiement, changer de plan, consulter les factures et résilier. On configure ce qui est autorisé dans le dashboard Stripe et on redirige les utilisateurs.
export async function createPortalSession(customerId: string) {
const session = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: `${process.env.APP_URL}/dashboard`,
});
return session.url;
}
Chaque action du client dans le portail génère des événements webhook. Les changements de plan déclenchent customer.subscription.updated. Les mises à jour de moyen de paiement déclenchent payment_method.attached. Les résiliations suivent le même cycle de vie que les résiliations programmatiques. Le handler de webhooks reste l’unique source de vérité pour tous les changements d’état.
Payment Intents : les paiements uniques
Les abonnements ne sont pas le seul modèle. Pour les achats ponctuels, les Payment Intents donnent un contrôle total sur le flux de paiement tout en gardant les détails de carte hors de votre serveur.
export async function createPaymentIntent(amount: number, currency: string) {
const intent = await stripe.paymentIntents.create({
amount: amount * 100,
currency,
automatic_payment_methods: { enabled: true },
});
return { clientSecret: intent.client_secret };
}
import { useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js';
function PaymentForm() {
const stripe = useStripe();
const elements = useElements();
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!stripe || !elements) return;
await stripe.confirmPayment({
elements,
confirmParams: { return_url: `${window.location.origin}/success` },
});
}
return (
<form onSubmit={handleSubmit}>
<PaymentElement />
<button type="submit">Payer</button>
</form>
);
}
Le serveur crée l’intent et retourne un client secret. Le frontend utilise Stripe Elements pour collecter les détails de paiement et confirmer. Le composant PaymentElement s’adapte à la région du client, affichant les moyens de paiement pertinents comme iDEAL, Bancontact ou le prélèvement SEPA aux côtés des cartes. Les numéros de carte vont directement à Stripe, jamais via votre serveur.
Tester localement avec le CLI Stripe
Le CLI de Stripe forward les événements webhook vers votre serveur local. Pas de service de tunneling, pas de déploiement nécessaire.
stripe listen --forward-to localhost:3000/api/webhooks/stripe
On déclenche des événements spécifiques pour tester les handlers de manière isolée.
stripe trigger checkout.session.completed
stripe trigger invoice.payment_failed
stripe trigger customer.subscription.deleted
Chaque commande envoie un payload réaliste avec des signatures valides. Combiné avec les clés API test de Stripe (préfixées par sk_test_), on peut simuler tout le cycle de vie du paiement, charges réussies, renouvellements échoués, résiliations, sans traiter de vrai argent. Vérifiez toujours que votre handler de webhooks met la base de données à jour correctement pour chaque événement avant de passer en production.
Conclusion
Stripe gère les parties difficiles des paiements : la conformité PCI, la diversité des moyens de paiement, les machines à états des abonnements et l’UI hébergée de facturation. Le travail du développeur est de câbler le SDK correctement, faire confiance aux webhooks comme source de vérité, et garder la base de données synchronisée avec les événements de Stripe. Le SDK TypeScript rend chaque appel API type-safe, et le CLI Stripe rend le test local indolore. Pour tout projet qui doit facturer des utilisateurs, Stripe supprime des mois de travail d’infrastructure et permet de livrer le produit.