Lancez des abonnements fiables sans complexité inutile
Vous voulez encaisser des paiements récurrents pour un produit SaaS rapidement, proprement, et sans dette technique. Bonne nouvelle, Stripe fournit tous les blocs prêts à l’emploi pour une intégration robuste en un temps record. Dans ce guide, vous allez mettre en place une intégration Stripe paiements SaaS basée sur Checkout, Billing et les webhooks. Au menu: architecture minimale, code TypeScript complet pour Next.js, gestion des événements d’abonnement, tests locaux, et checklist de mise en prod.
Pourquoi Stripe Checkout + Billing est le meilleur raccourci
Le combo Checkout + Billing vous évite d’orchestrer vous-même l’UX de paiement, la gestion des cartes, les cycles de facturation, les proratas et les emails récurrents. Stripe héberge la page de paiement, applique la conformité PCI, gère la SCA et déclenche les webhooks d’état de paiement et d’abonnement. Vous vous concentrez sur la logique produit et la synchronisation des statuts côté base de données.
Pour des abonnements fixes mensuels, Stripe recommande Checkout avec Billing et des webhooks pour suivre le cycle de vie des abonnements. Voir la doc officielle sur les abonnements, Checkout et le quickstart Billing. (doc abonnements) (quickstart Billing) (Checkout quickstart). (docs.stripe.com)
Alternatives possibles, quand les choisir
Option | Cas d’usage idéal | Vitesse d’implémentation | Effort maintenance | Remarques clés |
---|---|---|---|---|
Checkout + Billing | Stripe paiements SaaS récurrents simples avec page hébergée | Très rapide | Faible | Supporte SCA, coupons, codes promo, taxes, upgrade/downgrade via webhooks |
Elements | UX totalement custom du formulaire carte | Moyen | Moyen à élevé | Plus flexible, mais plus de responsabilités front et conformité |
Pricing Table + Checkout | Publication rapide d’une grille d’offres avec redirection vers Checkout | Très rapide | Faible | Peut être géré depuis le Dashboard, excellent pour un MVP |
Payment Links | Vente sans code ou tests marketing | Instantané | Faible | Moins intégré au produit, pratique pour valider l’offre |
Plan d’action en 30 minutes
-
Créez Produits et Prices dans Stripe Billing.
-
Exposez une route /api/checkout qui crée une session Checkout pour un Price récurrent.
-
Exposez un webhook /api/webhooks/stripe pour recevoir
checkout.session.completed
,customer.subscription.updated
,invoice.paid
,customer.subscription.deleted
. -
Synchronisez l’état d’abonnement dans votre base (user, customerId, subscriptionId, status, currentPeriodEnd).
-
Testez avec Stripe CLI et cartes de test. (webhooks docs) (Stripe CLI listen) (cartes de test). (docs.stripe.com)
Pré-requis et bonnes pratiques
-
Créez un compte Stripe et activez Billing. (Stripe Billing). (Stripe)
-
Créez au moins un Product et un Price récurrent mensuel dans le Dashboard.
-
Préparez vos clés:
STRIPE_SECRET_KEY
,NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
. -
Récupérez le Webhook Signing Secret après avoir créé l’endpoint ou via Stripe CLI. (Stripe CLI). (docs.stripe.com)
-
En Next.js App Router, forcez le runtime Node.js sur le webhook pour la crypto et l’accès au raw body avec
req.text()
. (webhooks docs, raw body). (docs.stripe.com)
Architecture minimale de votre intégration
-
Frontend: un bouton “S’abonner” qui appelle
/api/checkout
et redirige verssession.url
. -
Backend:
-
POST /api/checkout
: crée uneCheckout.Session
en mode subscription. -
POST /api/webhooks/stripe
: vérifie la signature, parse l’événement et met à jour la base.
-
-
Base de données: persistez
stripeCustomerId
,stripeSubscriptionId
,priceId
,status
,currentPeriodEnd
. -
Portail client (optionnel mais recommandé): laissez l’utilisateur gérer son abonnement sans ticket support. (Customer portal). (docs.stripe.com)
Code complet et typé pour Next.js 14
1) SDK Stripe centralisé
Fichier: lib/stripe.ts
// lib/stripe.ts
import Stripe from "stripe";
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string, {
apiVersion: "2024-06-20",
typescript: true,
});
2) Création de session Checkout
Fichier: app/api/checkout/route.ts
// app/api/checkout/route.ts
import { NextRequest, NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";
export const runtime = "nodejs";
type CreateCheckoutBody = {
priceId: string;
customerEmail?: string;
successUrl?: string;
cancelUrl?: string;
metadata?: Record<string, string>;
};
export async function POST(req: NextRequest): Promise<NextResponse> {
const body = (await req.json()) as CreateCheckoutBody;
const successUrl =
body.successUrl ?? `${process.env.NEXT_PUBLIC_APP_URL}/billing/success`;
const cancelUrl =
body.cancelUrl ?? `${process.env.NEXT_PUBLIC_APP_URL}/billing/cancel`;
try {
const session = await stripe.checkout.sessions.create(
{
mode: "subscription",
line_items: [{ price: body.priceId, quantity: 1 }],
success_url: successUrl + "?session_id={CHECKOUT_SESSION_ID}",
cancel_url: cancelUrl,
allow_promotion_codes: true,
billing_address_collection: "auto",
customer_email: body.customerEmail,
subscription_data: {
trial_period_days: 0,
metadata: body.metadata,
},
metadata: body.metadata,
},
{ idempotencyKey: `checkout_${body.priceId}_${Date.now()}` }
);
return NextResponse.json({ url: session.url }, { status: 200 });
} catch (err) {
console.error(err);
return NextResponse.json(
{ error: "Impossible de créer la session Checkout" },
{ status: 500 }
);
}
}
3) Webhook Stripe avec vérification de signature
Fichier: app/api/webhooks/stripe/route.ts
// app/api/webhooks/stripe/route.ts
import { NextRequest, NextResponse } from "next/server";
import { headers } from "next/headers";
import { stripe } from "@/lib/stripe";
export const runtime = "nodejs"; // important pour crypto et req.text()
const WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET as string;
type SubscriptionStatus =
| "trialing"
| "active"
| "past_due"
| "canceled"
| "unpaid"
| "incomplete"
| "incomplete_expired"
| "paused";
async function setUserSubscription(params: {
stripeCustomerId: string;
stripeSubscriptionId: string;
status: SubscriptionStatus;
currentPeriodEnd: number;
priceId: string;
}): Promise<void> {
// Ici, mettez à jour votre base de données.
// Exemple (pseudo-code Prisma):
// await prisma.user.update({
// where: { stripeCustomerId: params.stripeCustomerId },
// data: {
// stripeSubscriptionId: params.stripeSubscriptionId,
// subscriptionStatus: params.status,
// currentPeriodEnd: new Date(params.currentPeriodEnd * 1000),
// stripePriceId: params.priceId,
// },
// });
}
export async function POST(req: NextRequest): Promise<NextResponse> {
const signature = headers().get("stripe-signature");
if (!signature) {
return new NextResponse("Signature manquante", { status: 400 });
}
const rawBody = await req.text();
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(rawBody, signature, WEBHOOK_SECRET);
} catch (err) {
console.error("Erreur de signature Stripe:", err);
return new NextResponse("Signature invalide", { status: 400 });
}
try {
switch (event.type) {
case "checkout.session.completed": {
const session = event.data.object as Stripe.Checkout.Session;
// Optionnel: associer le customer au user si ce n'est pas encore fait.
// const customerId = session.customer as string;
break;
}
case "customer.subscription.created":
case "customer.subscription.updated":
case "customer.subscription.deleted": {
const sub = event.data.object as Stripe.Subscription;
await setUserSubscription({
stripeCustomerId: sub.customer as string,
stripeSubscriptionId: sub.id,
status: sub.status as SubscriptionStatus,
currentPeriodEnd: sub.current_period_end,
priceId:
typeof sub.items.data[0]?.price?.id === "string"
? (sub.items.data[0].price.id as string)
: "",
});
break;
}
case "invoice.paid": {
// Facture payée, vous pouvez envoyer un email de confirmation ou enregistrer une écriture comptable.
break;
}
case "invoice.payment_failed": {
// Paiement échoué, prévenir l’utilisateur, déclencher un dunning flow si nécessaire.
break;
}
default:
// Autres événements non gérés explicitement
break;
}
return new NextResponse("ok", { status: 200 });
} catch (err) {
console.error("Erreur traitement webhook:", err);
return new NextResponse("Erreur serveur", { status: 500 });
}
}
Vérifiez toujours les signatures de webhooks avec les librairies officielles et utilisez le raw body pour éviter les échecs de validation. (Docs Webhooks). (docs.stripe.com)
4) Bouton client et redirection
Composant minimal SubscribeButton.tsx
// components/SubscribeButton.tsx
"use client";
import { useState } from "react";
type Props = {
priceId: string;
customerEmail?: string;
};
export default function SubscribeButton({ priceId, customerEmail }: Props) {
const [loading, setLoading] = useState<boolean>(false);
async function onClick(): Promise<void> {
try {
setLoading(true);
const res = await fetch("/api/checkout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ priceId, customerEmail }),
});
if (!res.ok) throw new Error("Erreur API");
const data = (await res.json()) as { url: string };
window.location.href = data.url;
} finally {
setLoading(false);
}
}
return (
<button
type="button"
onClick={onClick}
disabled={loading}
aria-busy={loading}
title="S’abonner"
>
{loading ? "Redirection..." : "S’abonner"}
</button>
);
}
5) Modèle de données conseillé
Vous pouvez enrichir votre User
avec des colonnes Stripe minimales:
// Exemple d’interface TypeScript côté app
export type SubscriptionRow = {
userId: string;
stripeCustomerId: string;
stripeSubscriptionId: string;
stripePriceId: string;
subscriptionStatus:
| "trialing"
| "active"
| "past_due"
| "canceled"
| "unpaid"
| "incomplete"
| "incomplete_expired"
| "paused";
currentPeriodEnd: Date;
};
Tests locaux rapides et fiables
-
Stripe CLI: écoutez et transférez les webhooks de Stripe vers votre machine locale:
stripe listen --forward-to http://localhost:3000/api/webhooks/stripe
Vous obtiendrez un signing secret temporaire à placer dansSTRIPE_WEBHOOK_SECRET
. (Stripe CLI listen). (docs.stripe.com) -
Déclencheurs: simulez des événements avec
stripe trigger checkout.session.completed
ouinvoice.payment_succeeded
. (CLI reference). (docs.stripe.com) -
Cartes de test: utilisez
4242 4242 4242 4242
et d’autres numéros pour tester succès, refus et SCA. (Test cards). (docs.stripe.com)
Gestion du cycle de vie des abonnements via webhooks
Pour un SaaS sain, synchronisez votre base avec ces événements clés:
-
checkout.session.completed
: l’utilisateur a validé l’abonnement, associezcustomer
à votre user s’il n’existe pas. -
customer.subscription.created
etcustomer.subscription.updated
: source de vérité pourstatus
,priceId
,currentPeriodEnd
. -
invoice.paid
: preuve de paiement, utile pour les reçus. -
customer.subscription.deleted
: résiliation, rétrogradez le plan dans l’app.
Stripe recommande et documente cette approche basée webhooks pour refléter les états côté app. (Docs Subscriptions + Webhooks). (docs.stripe.com)
Portail client: moins de tickets support, plus d’autonomie
Activez le Customer Portal pour permettre aux clients de: mettre à jour carte bancaire, changer d’offre, annuler, voir l’historique des factures. Vous pouvez choisir quelles actions sont autorisées et intégrer le portail en quelques lignes. (Configurer le portail). (docs.stripe.com)
Erreurs fréquentes et correctifs immédiats
-
Signature webhook non valide: en App Router, lisez le raw body avec
await req.text()
et passez-le àstripe.webhooks.constructEvent
. Ne parsez pas en JSON avant. (Docs Webhooks, avertissement raw body). (docs.stripe.com) -
Edge runtime: utilisez
export const runtime = "nodejs"
sur l’endpoint webhook. -
Idempotence: utilisez des
idempotencyKey
pour éviter les doublons lors d’appels réseau répétables. -
Événements non gérés: logguez et surveillez, mais ne retournez pas 500 si vous ignorez un type; répondez 200 pour éviter les retries indéfinis sauf si vous voulez volontairement un retry.
Mise en prod: checklist express
-
Variables d’environnement
*_LIVE
renseignées, clé secrète uniquement côté serveur. -
Endpoint webhook public enregistré dans le Dashboard, secret à jour.
-
Plans et prices live créés et utilisés par l’app.
-
Politique de proration et d’upgrade/downgrade définie.
-
Portail client activé si vous voulez réduire le support.
-
Journalisez les événements et surveillez les retries.
-
Vérifiez la tarification Billing selon votre pays et votre volume pour éviter les surprises. (Billing pricing). (Stripe)
Combien ça coûte
La tarification des paiements dépend du pays et du moyen de paiement, et Billing peut comporter des frais additionnels pour certaines fonctionnalités. Consultez la page de tarification officielle pour votre pays. (Stripe pricing). (Billing pricing). (Stripe)
Exemple de flux complet utilisateur
-
L’utilisateur clique sur “S’abonner”.
-
Votre app appelle
/api/checkout
avecpriceId
. -
Stripe redirige vers la page Checkout hébergée pour valider le paiement récurrent.
-
Stripe envoie
checkout.session.completed
, puiscustomer.subscription.created
. -
Votre webhook met à jour l’utilisateur et active les features premium.
-
À l’échéance, Stripe émet la facture, prélève la carte et envoie
invoice.paid
ouinvoice.payment_failed
. -
En cas d’annulation, Stripe envoie
customer.subscription.deleted
, vous rétrogradez l’accès.
Ce flot correspond aux bonnes pratiques Stripe pour piloter un abonnement avec webhooks. (Docs Subscriptions overview). (docs.stripe.com)
Exemple d’upgrade ou de downgrade
-
Changez de
priceId
via le portail client, Stripe applique la proration automatiquement selon votre configuration. -
Votre webhook reçoit
customer.subscription.updated
, vous synchronisez le nouveau plan et la date de période. -
Affichez la nouvelle offre immédiatement côté produit, sans attendre la prochaine facture.
Tests avancés utiles pour un SaaS
-
3D Secure: testez les scénarios d’authentification forte.
-
Refus de paiement: simulez cartes déclinées pour valider vos messages d’erreur.
-
Pays et devises: testez cartes et monnaies pertinentes pour votre marché.
La liste officielle des numéros de test couvre ces cas. (Test cards). (docs.stripe.com)
Astuces de production pour une intégration sereine
-
Journalisation structurée des événements reçus avec
event.id
pour corrélation. -
Relecture des webhooks: Stripe peut renvoyer un événement, votre traitement doit rester idempotent.
-
Réconciliation: exposez un job quotidien qui vérifie qu’il n’y a pas de divergence entre votre base et l’état Stripe.
-
Sécurité: n’exposez jamais d’API clé secrète côté client, vérifiez les signatures, restreignez les IP si vous avez un proxy.
-
Support: branchez le Customer Portal pour réduire l’effort support et laisser les clients gérer leur abonnement en autonomie. (Customer management). (docs.stripe.com)
Petit boost pour accélérer votre mise en place
Au milieu de ce guide, un rappel utile: si vous construisez un SaaS et que vous cherchez un plan d’exécution clair, la todo-liste et la knowledge base de SaaS Path sont gratuites et organisées par modules et étapes. Vous y trouverez des checklists actionnables pour le lancement, la monétisation et le marketing. Parcourez comment créer un SaaS et structurez votre roadmap sans perdre de temps.
Conclusion
En 30 minutes, vous pouvez activer Stripe paiements SaaS avec une architecture stable: Checkout pour la collecte, Billing pour la récurrence, webhooks pour la synchronisation d’état. En ajoutant le Customer Portal, vous réduisez le support et améliorez l’expérience utilisateur. Passez à l’action: mettez en place la route Checkout, le webhook signé, testez avec Stripe CLI et déployez sur votre environnement live.