Introduction
Dans un monde SaaS où la récurrence des revenus est clé, l'automatisation de la facturation évite les erreurs manuelles et scale sans effort. Ce tutoriel intermédiaire vous montre comment intégrer Stripe pour gérer subscriptions, générer des factures PDF automatiques et envoyer des emails via webhooks dans une app Next.js 15 avec Prisma.
Pourquoi c'est crucial en 2026 ? Les régulations comme PSD3 exigent traçabilité et conformité, tandis que les users attendent des notifications instantanées. Imaginez : un client s'abonne, Stripe débite automatiquement, un webhook trigger un PDF personnalisé et un email – tout sans intervention.
On part des bases (setup projet) vers l'avancé (cron jobs pour usage-based billing), avec du code 100% fonctionnel. À la fin, votre système gère 1000+ clients sans sueur. Temps estimé : 30min pour un MVP fonctionnel.
Prérequis
- Node.js 20+ et npm/yarn
- Compte Stripe (test mode activé, API keys copiées)
- Connaissances en TypeScript, Next.js App Router et Prisma
- Vercel ou serveur pour déployer (webhooks publics requis)
- Outils : VS Code, Prisma Studio
Initialiser le projet Next.js
npx create-next-app@latest billing-automation --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
cd billing-automation
npm install stripe @prisma/client prisma resend pdf-lib cron
npm install -D @types/node
npx prisma init --datasource-provider sqliteCette commande crée un projet Next.js 15 avec TypeScript et Tailwind, installe Stripe pour billing, Prisma pour DB, Resend pour emails, pdf-lib pour PDFs et cron pour scheduling. SQLite simplifie les tests locaux ; passez à PostgreSQL en prod. Évitez les conflits en utilisant --app pour App Router.
Configurer l'environnement et Prisma
Copiez les variables d'environnement dans .env :
```
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
RESEND_API_KEY=re_...
DATABASE_URL="file:./dev.db"
NEXTAUTH_SECRET=supersecret
Adaptez prisma/schema.prisma pour users et subscriptions (prochain code). Exécutez npx prisma db push` après. Analogie : Prisma est votre ORM 'Lego' – schemas définissent la structure DB comme des blocs interconnectés.
Définir le schéma Prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
stripeCustomerId String?
subscriptions Json[] @default([])
createdAt DateTime @default(now())
}
model Invoice {
id String @id @default(cuid())
userId String
stripeId String @unique
pdfUrl String?
status String // paid, open, voided
amount Int
currency String
user User @relation(fields: [userId], references: [id])
createdAt DateTime @default(now())
}Ce schéma lie users à Stripe customers et tracke invoices avec PDFs générés. Json[] pour subscriptions flexibles (usage-based). SQLite pour dev ; en prod, indexez stripeId unique pour lookups rapides. Piège : oubliez @unique sur stripeId, causant doublons en webhook.
Générer et migrer la DB
npx prisma generate
npx prisma db push
npx prisma studioGénère le client Prisma, pushe le schema en DB et ouvre Studio pour inspecter. Utilisez db push pour dev itératif vs migrations en prod. Vérifiez User/Invoice tables ; ajoutez un user test via Studio.
Créer un customer Stripe et checkout
Créez une API route pour l'onboarding : associez un user à un Stripe customer, puis générez un checkout pour subscription mensuelle (49€). Utilisez Price ID de votre dashboard Stripe (créez un recurring price).
API : Créer customer et checkout
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
import { PrismaClient } from '@prisma/client';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: '2024-06-20' });
const prisma = new PrismaClient();
export async function POST(req: NextRequest) {
const { userId, email } = await req.json();
let user = await prisma.user.findUnique({ where: { id: userId } });
if (!user) {
user = await prisma.user.create({ data: { email } });
}
let customerId = user.stripeCustomerId;
if (!customerId) {
const customer = await stripe.customers.create({ email });
customerId = customer.id;
await prisma.user.update({
where: { id: user.id },
data: { stripeCustomerId: customerId },
});
}
const session = await stripe.checkout.sessions.create({
customer: customerId,
mode: 'subscription',
payment_method_types: ['card'],
line_items: [{ price: 'price_12345', quantity: 1 }],
success_url: `${req.headers.get('origin')}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${req.headers.get('origin')}/?canceled=true`,
});
return NextResponse.json({ url: session.url });
}Cette route POST crée/upsert user, customer Stripe, puis checkout session. Remplacez 'price_12345' par votre Price ID. Piège : sans customer reuse, vous multipliez les customers inutiles (coût + confusion). Testez avec curl ou frontend fetch.
Gérer les webhooks Stripe pour automation
Stripe envoie des events (invoice.paid) sur webhook. On handle : génère PDF invoice, stocke URL en DB, envoie email via Resend. Déployez sur Vercel pour URL publique, ajoutez webhook endpoint dans Stripe dashboard.
API Webhook handler
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
import { PrismaClient } from '@prisma/client';
import { Resend } from 'resend';
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: '2024-06-20' });
const prisma = new PrismaClient();
const resend = new Resend(process.env.RESEND_API_KEY!);
export async function POST(req: NextRequest) {
const body = await req.text();
const sig = req.headers.get('stripe-signature')!;
let event;
try {
event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
} catch (err) {
return NextResponse.json({ error: 'Webhook signature verification failed' }, { status: 400 });
}
if (event.type === 'invoice.paid') {
const invoice = event.data.object as Stripe.Invoice;
const user = await prisma.user.findFirst({ where: { stripeCustomerId: invoice.customer as string } });
if (!user) return NextResponse.json({ received: true });
// Générer PDF
const pdfDoc = await PDFDocument.create();
const timesRomanFont = await pdfDoc.embedFont(StandardFonts.TimesRoman);
const page = pdfDoc.addPage([500, 600]);
page.drawText(`Facture #${invoice.number}`, {
x: 50,
y: 550,
size: 20,
font: timesRomanFont,
color: rgb(0, 0, 0),
});
page.drawText(`${invoice.amount_paid / 100}€`, { x: 50, y: 500, size: 16, font: timesRomanFont });
const pdfBytes = await pdfDoc.save();
// TODO: Upload PDF à S3/Cloudinary, ici simuler URL
const pdfUrl = 'https://example.com/invoice.pdf'; // Remplacez par upload réel
await prisma.invoice.create({
data: {
userId: user.id,
stripeId: invoice.id,
pdfUrl,
status: invoice.status,
amount: invoice.amount_paid,
currency: invoice.currency,
},
});
// Envoyer email
await resend.emails.send({
from: 'billing@yourapp.com',
to: [user.email],
subject: `Facture payée #${invoice.number}`,
html: `<p>Votre facture est disponible : <a href="${pdfUrl}">Télécharger</a></p>`,
});
}
return NextResponse.json({ received: true });
}Valide signature webhook, génère PDF simple avec pdf-lib, stocke en DB, envoie email. Intégrez upload réel (Vercel Blob/S3). Piège : sans raw body (req.text()), signature échoue. Testez avec Stripe CLI : stripe listen --forward-to localhost:3000/api/webhook.
Cron job pour usage-based billing
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
import { PrismaClient } from '@prisma/client';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: '2024-06-20' });
const prisma = new PrismaClient();
export const dynamic = 'force-dynamic';
export async function GET() {
const users = await prisma.user.findMany({ where: { stripeCustomerId: { not: null } } });
for (const user of users) {
// Simuler usage (ex: API calls count from DB)
const usage = 150; // Remplacez par query réelle
await stripe.subscriptionItems.createUsageRecord('si_123', { // SubscriptionItem ID
quantity: usage,
timestamp: Math.floor(Date.now() / 1000),
action: 'set',
});
}
return NextResponse.json({ updated: users.length });
}Cron mensuel (Vercel Cron) reporte usage pour meterred billing. Remplacez 'si_123' par item réel, query usage DB. Piège : sans dynamic='force-dynamic', cache Next.js bloque. Appelez via /api/cron/usage manuellement pour test.
Test et déploiement
- Local :
npm run dev, test checkout avec Postman (userId fictif) - Webhook : Stripe CLI pour simuler
invoice.paid - Déployez Vercel : ajoutez cron
0 0 1pour usage - Monitorez Stripe dashboard et logs Resend
fetch('/api/checkout', {method:'POST', body:JSON.stringify({userId:'test', email:'test@example.com'})}).Bonnes pratiques
- Idempotence webhooks : Vérifiez event ID en DB pour éviter doublons
- Sécurité : Validez toujours signatures ; utilisez Stripe API version pinning
- Scalabilité : Batch usage reports (>1000 users) ; offload PDF à queue (BullMQ)
- Conformité : Stockez invoices 10 ans ; ajoutez TVA via Stripe tax
- Monitoring : Intégrez Sentry pour webhook failures
Erreurs courantes à éviter
- Oublier
req.text()dans webhook : signature KO - Pas de customer reuse : explosion de customers (limite Stripe)
- Ignorer timezones en cron : usage reports décalés
- PDF sans upload : URLs cassées ; intégrez Cloudinary early
- Test sans Stripe CLI : events manqués en dev
Pour aller plus loin
- Docs Stripe Billing : stripe.com/docs/billing
- Avancé : Customer Portal, Usage webhooks
- Formations Learni : Découvrez nos formations Next.js & Stripe
- Repo GitHub exemple : github.com/learni/billing-automation-2026