Skip to content
Learni
Voir tous les tutoriels
Développement Backend

Comment automatiser la facturation avec Stripe en 2026

Read in English

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

terminal
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 sqlite

Cette 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

prisma/schema.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

terminal
npx prisma generate
npx prisma db push
npx prisma studio

Gé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

src/app/api/checkout/route.ts
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

src/app/api/webhook/route.ts
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

src/app/api/cron/usage/route.ts
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 1 pour usage
  • Monitorez Stripe dashboard et logs Resend
Frontend snippet : 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

Partagez votre implémentation en commentaires !