Ajouter les paiements Stripe avec Claude Code
Branche Stripe Checkout, les webhooks et le portail client dans une app Next.js avec Claude Code. Du premier prompt au paiement en prod en une seule session.
Arrête de tout configurer. Place à la construction.
Des templates SaaS avec orchestration IA.
Le problème : L'intégration Stripe a 7 pièces mobiles : le checkout, les webhooks, le portail client, les variables d'environnement, les clés de test, les clés live, et la CLI. La plupart des tutos en couvrent trois et te laissent déboguer le reste à minuit.
La victoire rapide : Ajoute le serveur MCP Stripe à Claude Code en une commande pour que Claude puisse consulter les clients, les prix et les abonnements en temps réel pendant que tu construis :
claude mcp add --transport http stripe https://mcp.stripe.com/Cette seule ligne donne à Claude un accès direct à ton compte Stripe. Il peut lire les données en direct, vérifier que tes produits existent, et attraper les erreurs de config avant qu'elles n'atteignent ton gestionnaire de webhooks.
Ce qu'il te faut avant de commencer
Trois choses doivent être en place.
Un compte Stripe avec au moins un produit et un prix créés. Tu peux faire créer ça à Claude via le serveur MCP sans toucher au Dashboard. Une fois le MCP connecté, un prompt du genre « create a product called Pro Plan with a $29/month recurring price » suffit.
Claude Code avec le MCP Stripe connecté (commande au-dessus). L'alternative, c'est le Skill best-practices de Stripe, qui injecte la documentation Stripe dans ta session :
npx skills add https://docs.stripe.com --yesLe serveur MCP est le meilleur choix pour le développement actif. Claude peut l'appeler en cours de session pour lister tes prix, retrouver un client par email, récupérer le statut actuel d'un abonnement, ou vérifier que ton secret de webhook correspond à ce que le Dashboard affiche. Le Skill ne te donne que du contexte de documentation. Il n'interroge pas ton compte.
Les deux ne s'excluent pas mutuellement. Certaines équipes utilisent les deux : MCP pour les requêtes en direct, Skill pour les conseils de bonnes pratiques. Le MCP avec une clé d'API restreinte (rk_test_...) est le setup le plus sûr en mode test parce que les clés restreintes limitent les objets Stripe que Claude peut toucher.
Quatre variables d'environnement dans ton fichier .env.local :
STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_URL=http://localhost:3000STRIPE_SECRET_KEY ne touche jamais le client. NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY peut être exposée sans risque. STRIPE_WEBHOOK_SECRET vient de la CLI Stripe quand tu démarres le transfert d'événements en local (couvert dans la section tests). NEXT_PUBLIC_URL alimente tes paramètres success_url et return_url.
Checkout Sessions vs Payment Intents
Deux API Stripe peuvent encaisser un paiement. Choisir la mauvaise ajoute des semaines de boulot.
Les Checkout Sessions (stripe.checkout.sessions.create()) redirigent l'utilisateur vers une page hébergée par Stripe ou un formulaire intégré sur ton domaine. Stripe gère la saisie de carte, le 3D Secure, Apple Pay, l'affichage de la devise, et les taxes. Pour la plupart des produits SaaS, c'est le bon choix. Les achats uniques comme les abonnements passent par là.
Les Payment Intents (stripe.paymentIntents.create()) sont de plus bas niveau. Tu construis le formulaire toi-même avec Stripe Elements, tu collectes les données de carte, et tu appelles l'API. Plus de contrôle, plus de code, plus de maintenance. Ne l'utilise que quand tu as besoin d'un tunnel de paiement entièrement custom qu'une page hébergée ne peut pas offrir.
Le pattern privilégié en 2026 pour les Checkout Sessions, c'est l'Embedded Checkout : ui_mode: 'embedded' garde l'utilisateur sur ton domaine, utilise les composants EmbeddedCheckoutProvider et EmbeddedCheckout de @stripe/react-stripe-js, et renvoie vers une return_url que tu définis. Le Hosted Checkout (redirection vers stripe.com) marche toujours, mais c'est vers l'Embedded que Stripe oriente les nouvelles intégrations.
Construire le tunnel de paiement
Demande à Claude de générer la route, puis vérifie le param mode avant de livrer.
Un bon prompt à donner à Claude ici, c'est : « Build me a Stripe Checkout Sessions route in app/api/checkout/route.ts. Use mode: payment for one-time purchases, return the session URL as JSON, and read all URLs from environment variables. »
Pour un achat unique, la route ressemble à ça :
// app/api/checkout/route.ts
import { NextResponse } from 'next/server'
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-11-20.acacia',
})
export async function POST(request: Request) {
const { priceId } = await request.json()
const session = await stripe.checkout.sessions.create({
mode: 'payment',
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.NEXT_PUBLIC_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_URL}/pricing`,
})
return NextResponse.json({ url: session.url })
}Pour un abonnement, change exactement une chose. Mets mode: 'subscription' au lieu de mode: 'payment'. Ton priceId doit pointer vers un prix récurrent (pas un prix unique) dans Stripe. Tout le reste de la route reste identique.
Pour passer à l'Embedded Checkout, ajoute ui_mode: 'embedded' et remplace success_url par return_url. La réponse renvoie session.client_secret au lieu de session.url, que l'EmbeddedCheckoutProvider utilise côté frontend.
Les webhooks : la partie que tout le monde rate
Les webhooks, c'est là où Stripe te répond. Chaque décision de provisionnement (accorder l'accès, le révoquer, envoyer un reçu) devrait vivre dans le gestionnaire de webhooks, pas dans la redirection de succès du checkout. Les utilisateurs ferment des onglets et cliquent sur précédent. La redirection n'est pas fiable. Le webhook se déclenche toujours.
Le souci critique dans l'App Router de Next.js, c'est le parsing du body brut. La vérification de signature de Stripe (stripe.webhooks.constructEvent) a besoin du body de requête brut sous forme de chaîne. L'App Router parse le body avant que ton gestionnaire ne le voie. Si tu appelles request.json(), le flux est consommé et la vérification échoue à tous les coups.
Donne à Claude la contrainte exacte : « Build a Stripe webhook handler at app/api/stripe-webhook/route.ts. It must use request.text() for the raw body, not request.json(), and verify the signature before processing any events. »
Utilise request.text() à la place :
// app/api/stripe-webhook/route.ts
import { NextResponse } from 'next/server'
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-11-20.acacia',
})
export async function POST(request: Request) {
const rawBody = await request.text()
const sig = request.headers.get('stripe-signature')!
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(
rawBody,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
)
} catch (err) {
return NextResponse.json(
{ error: 'Webhook signature verification failed' },
{ status: 400 }
)
}
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session
// Provision access, send welcome email
await grantAccess(session.customer as string)
break
}
case 'customer.subscription.updated': {
const sub = event.data.object as Stripe.Subscription
// Update tier in DB
await updateSubscription(sub.customer as string, sub.status)
break
}
case 'customer.subscription.deleted': {
const sub = event.data.object as Stripe.Subscription
// Revoke access
await revokeAccess(sub.customer as string)
break
}
case 'invoice.payment_failed': {
const invoice = event.data.object as Stripe.Invoice
// Send dunning email, flag in DB
await handleFailedPayment(invoice.customer as string)
break
}
}
return NextResponse.json({ received: true })
}Les quatre événements à gérer au minimum sont dans le tableau ci-dessous. Le reste peut s'ajouter au fur et à mesure que ta logique de facturation grossit.
| Événement | Quand il se déclenche | Action |
|---|---|---|
checkout.session.completed | Paiement ou abonnement démarré | Provisionner l'accès, envoyer l'email de bienvenue |
customer.subscription.updated | Changement de plan, renouvellement, fin d'essai | Mettre à jour le tier en base |
customer.subscription.deleted | Résiliation | Révoquer l'accès |
invoice.payment_failed | Échec d'un prélèvement récurrent | Envoyer l'email de relance, signaler en base |
Une dernière chose : cette route doit être exclue de tout middleware Next.js qui lit le body, et elle ne doit pas être enveloppée dans les contrôles d'authentification de ton app. Stripe lui poste une requête non authentifiée. Protège-la uniquement par la vérification de signature.
Configuration du portail client
Le portail client permet aux utilisateurs de gérer eux-mêmes leur abonnement : résilier, mettre à jour les infos de paiement, voir les factures. Une étape coince la plupart des devs.
Tu dois configurer le portail dans le Dashboard Stripe avant de créer la moindre session de portail. Sans configuration sauvegardée, chaque appel billingPortal.sessions.create() renvoie une erreur. L'URL de configuration en mode test est https://dashboard.stripe.com/test/settings/billing/portal. Le mode live a sa propre URL distincte à https://dashboard.stripe.com/settings/billing/portal. Les deux demandent une configuration avant de déployer.
L'étape de configuration, c'est là que tu décides quelles actions les utilisateurs ont le droit de faire : résilier leur abonnement, mettre à jour leur moyen de paiement, changer de plan, télécharger les factures. Règle ces options dans le Dashboard, sauvegarde la configuration, et ensuite tes appels d'API marcheront.
Une fois configurée, la route pour créer une session est courte :
// app/api/portal/route.ts
import { NextResponse } from 'next/server'
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-11-20.acacia',
})
export async function POST(request: Request) {
const { customerId } = await request.json()
const session = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: `${process.env.NEXT_PUBLIC_URL}/account`,
})
return NextResponse.json({ url: session.url })
}Ton frontend redirige vers session.url. L'utilisateur voit sa page de facturation Stripe, fait ses changements, et revient sur /account une fois terminé. Les changements se déclenchent sous forme d'événements customer.subscription.updated ou customer.subscription.deleted, que ton gestionnaire de webhooks couvre déjà.
Note : les configurations de portail en mode test et en mode live sont complètement séparées. Configurer le portail en mode test ne le met pas en place pour le mode live. Repasse par les mêmes étapes de configuration dans les deux environnements avant de passer en prod.
Tester le flux complet
La CLI Stripe transfère les événements en direct depuis Stripe vers ton serveur local. Lance ça dans un terminal séparé avant de tester le moindre paiement :
stripe listen --forward-to localhost:3000/api/stripe-webhookLa CLI affiche un secret de signature de webhook au démarrage. Cette valeur va dans STRIPE_WEBHOOK_SECRET dans ton .env.local. Elle change à chaque redémarrage du listener, alors ne la code pas en dur.
Pour déclencher un événement précis sans cliquer à travers un formulaire de paiement, utilise stripe trigger :
stripe trigger checkout.session.completed
stripe trigger customer.subscription.deleted
stripe trigger invoice.payment_failedCartes de test pour différents scénarios :
| Scénario | Numéro de carte |
|---|---|
| Paiement réussi | 4242 4242 4242 4242 |
| Refusé | 4000 0000 0000 0002 |
| 3D Secure requis | 4000 0025 0000 3155 |
| Fonds insuffisants | 4000 0000 0000 9995 |
Utilise n'importe quelle date d'expiration future, n'importe quel CVC à 3 chiffres, et n'importe quel code postal. Le numéro de carte est la seule valeur qui change le résultat.
La séquence de test recommandée, c'est : démarrer le listener CLI, ouvrir ta page de paiement, faire un paiement avec la carte 4242 4242 4242 4242, regarder l'événement checkout.session.completed arriver dans ton terminal, et vérifier que ton gestionnaire a bien tourné (vérifie la base ou les logs). Ensuite, déclenche stripe trigger invoice.payment_failed pour confirmer que ton chemin de relance marche sans attendre un vrai échec de prélèvement. Garde la carte 3D Secure (4000 0025 0000 3155) pour la fin. Stripe redirige par une étape d'authentification que ton frontend doit gérer. Si la redirection échoue en silence, la session ne se termine jamais et aucun webhook ne se déclenche.
Checklist de passage en prod
Passer du mode test au mode live, c'est quatre étapes, pas un seul changement de clé.
D'abord, remplace tes clés de test (sk_test_, pk_test_) par les clés live (sk_live_, pk_live_) dans les variables d'environnement de ta production.
Ensuite, configure le portail client en mode live à https://dashboard.stripe.com/settings/billing/portal. Le mode test et le mode live ont des configurations de portail complètement séparées. Ce que tu règles en mode test ne se reporte pas.
Troisièmement, règle ton endpoint de webhook dans le Dashboard Stripe vers ton URL de production. Va dans Developers > Webhooks > Add endpoint et pointe-le vers https://yourdomain.com/api/stripe-webhook. Copie le secret de signature de webhook live et mets-le comme STRIPE_WEBHOOK_SECRET en production.
Quatrièmement, confirme tes ID de prix. Les prix du mode test (price_test_...) n'existent pas en mode live. Tout ID de prix codé en dur doit être mis à jour vers des ID de prix live ou lu depuis les variables d'environnement.
Fais un vrai paiement avec une vraie carte (puis rembourse-le) avant de considérer l'intégration terminée. Le trigger CLI est bien pour tester les gestionnaires, mais rien ne confirme le chemin complet comme un vrai prélèvement.
Ce que Build This Now livre préconstruit
Écrire et déboguer ce flux prend une journée entière pour la plupart des devs. L'edge function de checkout de Stripe, le gestionnaire de webhooks, la route du portail client, et les quatre variables d'environnement sont prébranchés dans Build This Now dès le premier jour. L'intégration se déploie sur les Supabase Edge Functions, testée et fonctionnelle, avant que tu n'écrives ta première fonctionnalité custom.
Stripe n'est pas la partie difficile. Brancher chaque pièce dans le bon ordre, si. Une fois que tu l'as fait une fois, tu sais où sont les trous.
Arrête de tout configurer. Place à la construction.
Des templates SaaS avec orchestration IA.
Le mode auto de Claude Code
Un second modèle Sonnet examine chaque appel d'outil Claude Code avant qu'il s'exécute. Ce que le mode auto bloque, ce qu'il autorise, et les règles d'autorisation qu'il place dans tes paramètres.
Feedback Loops
Donne à Claude Code un seul prompt qui écrit du code, lance ta commande de test ou de dev, lit la sortie, corrige ce qui casse, et boucle jusqu'à ce que la suite soit au vert.