Intégrer Stripe dans un SaaS français : TVA, factures, webhooks
Guide complet pour intégrer Stripe dans un SaaS B2B français, gestion de la TVA, factures conformes, webhooks sécurisés et abonnements.
Stripe en France : ce qui change
Intégrer Stripe dans un SaaS français n'est pas juste "copier-coller la doc US". Il y a la TVA à 20%, les factures conformes à générer, et des règles spécifiques pour les abonnements B2B.
Étape 1 : Configuration Stripe
// src/lib/stripe.ts
import Stripe from "stripe";
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2026-02-25.clover",
typescript: true,
});Produits et prix
Créez vos plans dans le dashboard Stripe avec les bons taux de TVA :
// Exemple de création programmatique
const product = await stripe.products.create({
name: "HeartCo Pro",
tax_code: "txcd_10103001", // SaaS - Software as a Service
});
const price = await stripe.prices.create({
product: product.id,
unit_amount: 3400, // 34€ HT
currency: "eur",
recurring: { interval: "month" },
tax_behavior: "exclusive", // TVA ajoutée en plus
});Étape 2 : Checkout Session
// src/server/api/routers/billing.ts
export const billingRouter = createTRPCRouter({
createCheckout: staffProcedure
.input(z.object({ priceId: z.string() }))
.mutation(async ({ ctx, input }) => {
const session = await stripe.checkout.sessions.create({
customer_email: ctx.session.user.email,
mode: "subscription",
line_items: [{ price: input.priceId, quantity: 1 }],
automatic_tax: { enabled: true },
tax_id_collection: { enabled: true },
success_url: `${env.NEXT_PUBLIC_APP_URL}/dashboard/billing?success=true`,
cancel_url: `${env.NEXT_PUBLIC_APP_URL}/dashboard/billing`,
metadata: {
organizationId: ctx.session.user.organizationId,
},
});
return { url: session.url };
}),
});Points clés pour la France
automatic_tax: { enabled: true }: Stripe calcule la TVA automatiquement selon le pays du client. HeartCo ne l'active pas par défaut (la TVA est fixée au moment de la création des prix), mais c'est l'option à activer si tu factures des particuliers dans plusieurs pays de l'UE, où le taux dépend du pays de résidence du client.tax_id_collection: Permet au client de saisir son numéro de TVA intracommunautaire, utile en B2B pour l'autoliquidationmetadata: Toujours inclure l'organizationIdpour le webhook
Étape 3 : Webhooks sécurisés
Le webhook est le point critique. C'est lui qui met à jour votre base de données quand un paiement est confirmé.
// src/app/api/stripe/webhook/route.ts
import { headers } from "next/headers";
import crypto from "crypto";
export async function POST(req: Request) {
const body = await req.text();
const headersList = await headers();
const signature = headersList.get("stripe-signature")!;
// Vérification HMAC : OBLIGATOIRE
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!,
);
} catch {
return new Response("Invalid signature", { status: 400 });
}
switch (event.type) {
case "checkout.session.completed": {
const session = event.data.object;
const orgId = session.metadata?.organizationId;
if (!orgId) break;
await db.subscription.update({
where: { organizationId: orgId },
data: {
plan: "PRO",
stripeSubscriptionId: session.subscription as string,
status: "ACTIVE",
},
});
break;
}
case "customer.subscription.deleted": {
// Downgrade vers FREE
const sub = event.data.object;
await db.subscription.updateMany({
where: { stripeSubscriptionId: sub.id },
data: { plan: "FREE", status: "CANCELED" },
});
break;
}
}
return new Response("OK", { status: 200 });
}Règle de sécurité critique
Ne jamais comparer les signatures avec === :
// ❌ VULNÉRABLE : timing attack
if (computedSignature === receivedSignature) { ... }
// ✅ SÉCURISÉ : comparaison en temps constant
crypto.timingSafeEqual(
Buffer.from(computedSignature),
Buffer.from(receivedSignature),
);Stripe SDK le fait pour vous via constructEvent, mais si vous vérifiez manuellement, utilisez toujours timingSafeEqual.
Le piège qu'on découvre en production : les webhooks rejoués
Stripe garantit la livraison au moins une fois, pas exactement une fois. En cas de timeout ou d'erreur réseau, le même événement peut arriver deux fois. Sans protection, un webhook checkout.session.completed traité deux fois peut créer une transaction en double.
La parade est une table dédiée avec une contrainte d'unicité sur l'ID d'événement Stripe, insérée avant tout traitement métier :
// Idempotence stricte : create atomique, la contrainte unique fait le travail
try {
await db.stripeWebhookEvent.create({
data: { stripeEventId: event.id, type: event.type },
});
} catch (e: unknown) {
// Violation de contrainte unique Prisma (P2002) = déjà traité
const isDuplicate =
typeof e === "object" &&
e !== null &&
"code" in e &&
(e as { code: string }).code === "P2002";
if (isDuplicate) return new Response("OK", { status: 200 });
throw e;
}Un if (déjà vu) return classique a une fenêtre de course entre la lecture et l'écriture. Le create atomique sur une colonne unique n'en a pas : le deuxième appel échoue proprement sur la contrainte, sans jamais laisser passer le traitement en double.
Étape 4 : Factures conformes
Pour un SaaS B2B français, vos factures doivent inclure :
- Numéro séquentiel (FACT-2026-001)
- TVA ventilée (HT + TVA + TTC)
- Mentions légales (SIRET, numéro TVA)
- Date d'émission et de paiement
Stripe génère des factures automatiquement pour les abonnements. Activez Stripe Invoicing et configurez vos informations légales dans le dashboard.
Une facture Stripe conforme sur ces points n'est pas la même chose qu'une facture électronique au sens de la réforme 2026 (Factur-X, plateformes agréées). Si tes clients B2B sont des entreprises françaises, l'obligation de réception est déjà en vigueur au moment où cet article est mis à jour : voir Facturation électronique 2026 pour un SaaS B2B pour ce que ça change concrètement.
Checklist Stripe France
-
automatic_taxactivé sur toutes les Checkout Sessions -
tax_id_collectionactivé pour les clients B2B - Webhook vérifié par signature HMAC
- Informations légales dans Stripe Dashboard (SIRET, TVA)
- Gestion du downgrade à l'expiration de l'abonnement
- Emails de confirmation configurés (Stripe ou custom)
- Mode test validé avant passage en production
Le paiement est la partie la plus sensible de votre SaaS. Prenez le temps de bien le configurer : vos clients (et votre comptable) vous remercieront.
Pour aller plus loin
Articles connexes
Facturation électronique 2026 pour un SaaS B2B
Calendrier officiel, vocabulaire PDP et e-reporting, générer un Factur-X, et le rôle d'un raccordement comme iopole pour un SaaS B2B français.
LireRGPD pour SaaS B2B français, checklist complète sans mythes
Ce que vous devez vraiment faire pour être conforme RGPD en tant que SaaS B2B français, et ce que les agences de conseil vous vendent inutilement.
LireCadrer Claude Code avec CLAUDE.md sur un SaaS multi-tenant
Les vraies règles qui gouvernent ce SaaS avec Claude Code : ce qui va dans CLAUDE.md, ce qui n'y va pas, et ce qui est réellement livré à l'achat.
LirePrêt à lancer ton SaaS ?
HeartCo Starter inclut tout ce dont tu as besoin : auth, paiements, IA, mobile, sécurité auditée. À partir de 199 €.