Blog/Articles/ Intégrer Stripe pour les paiements récurrents en 30 min

Intégrer Stripe pour les paiements récurrents en 30 min

Intégrer Stripe pour les paiements récurrents en 30 min

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

  1. Créez Produits et Prices dans Stripe Billing.

  2. Exposez une route /api/checkout qui crée une session Checkout pour un Price récurrent.

  3. Exposez un webhook /api/webhooks/stripe pour recevoir checkout.session.completed, customer.subscription.updated, invoice.paid, customer.subscription.deleted.

  4. Synchronisez l’état d’abonnement dans votre base (user, customerId, subscriptionId, status, currentPeriodEnd).

  5. 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 vers session.url.

  • Backend:

    • POST /api/checkout: crée une Checkout.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 dans STRIPE_WEBHOOK_SECRET. (Stripe CLI listen). (docs.stripe.com)

  • Déclencheurs: simulez des événements avec stripe trigger checkout.session.completed ou invoice.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, associez customer à votre user s’il n’existe pas.

  • customer.subscription.created et customer.subscription.updated: source de vérité pour status, 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

  1. L’utilisateur clique sur “S’abonner”.

  2. Votre app appelle /api/checkout avec priceId.

  3. Stripe redirige vers la page Checkout hébergée pour valider le paiement récurrent.

  4. Stripe envoie checkout.session.completed, puis customer.subscription.created.

  5. Votre webhook met à jour l’utilisateur et active les features premium.

  6. À l’échéance, Stripe émet la facture, prélève la carte et envoie invoice.paid ou invoice.payment_failed.

  7. 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.

Questions fréquentes

  • Comment lier un user à un customer Stripe+
    À la création de session Checkout, laissez Stripe créer le customer. Dans le webhook checkout.session.completed, récupérez session.customer et attachez-le à votre user si absent.
  • Comment gérer les échecs de paiement récurrents+
    Sur invoice.payment_failed, avertissez l’utilisateur, limitez l’accès si le statut passe past_due, et laissez Stripe retenter selon votre configuration de relance. Sur customer.subscription.deleted, rétrogradez le compte.
  • Quels événements Stripe dois-je gérer en priorité pour un SaaS récurrent+
    Minimisez la complexité en traitant checkout.session.completed, customer.subscription.created et customer.subscription.updated, plus invoice.paid et customer.subscription.deleted. Ce set couvre activation, renouvellement, échec et résiliation.
  • Comment tester la SCA et les erreurs sans carte réelle+
    Utilisez les cartes de test Stripe qui simulent succès, refus et 3D Secure. Vous pouvez aussi déclencher des événements avec Stripe CLI. (Docs test) (CLI). (docs.stripe.com)
  • Puis-je personnaliser entièrement la page de paiement+
    Avec Checkout vous customisez le branding et quelques options, mais pas l’UX complète. Si vous voulez un contrôle total, passez à Elements, au prix d’un effort plus conséquent et de responsabilités supplémentaires côté front et conformité.
  • Le Customer Portal est-il indispensable+
    Non, mais il réduit fortement le support en permettant la mise à jour de carte, le changement de plan et l’annulation par le client lui-même. Vous pouvez choisir précisément les actions autorisées. (Configurer le portail). (docs.stripe.com)
  • Comment éviter les erreurs de signature webhook en Next.js App Router+
    Utilisez export const runtime = "nodejs", lisez await req.text() pour obtenir le raw body, puis stripe.webhooks.constructEvent(rawBody, signature, secret). N’interceptez pas ou ne transformez pas le body avant validation. (Docs Webhooks). (docs.stripe.com)
  • Comment gérer les upgrades et downgrades d’offre+
    Laissez Stripe gérer le changement de Price, configurez la proration dans Billing, et synchronisez la mise à jour via customer.subscription.updated. Vous appliquez les permissions côté app dès réception de l’événement.
  • Où vérifier la tarification Stripe pour mon pays+
    Consultez la page Pricing de Stripe et, si vous utilisez Billing, la page Billing pricing. Les montants varient selon la région et les fonctionnalités. (Stripe pricing) (Billing pricing). (Stripe)
  • Faut-il stocker tout l’événement Stripe en base+
    Pas nécessaire. Stockez les identifiants clés et l’état de l’abonnement. Conservez en log l’event.id et le payload brut en stockage froid si vous avez des exigences d’audit.

Articles similaires

Exemple de business plan startup

Exemple de business plan startup

Modèle de business plan startup complet et chiffré, avec structure, tableaux financiers et exemples…

Growth loop : inviter vs parrainer

Growth loop : inviter vs parrainer

Invites ou parrainage, quelle growth loop choisir pour votre SaaS. Modèles, calculs, tactiques et…