Setup Stripe (clés API + portail)
Créer ses clés API Stripe, configurer le webhook et le Customer Portal pour activer la facturation.
Setup initial du compte Stripe pour activer billing, subscriptions, refunds, coupons. Pour les détails d'usage (webhook events, page admin), voir Billing et Webhooks Stripe.
1. Créer le compte + activer test mode
- Compte sur stripe.com (Standard, pas Express/Custom)
- Reste en Test mode (toggle en haut à droite du dashboard) pendant tout le dev
- Renseigne le minimum : nom de l'entreprise, pays, devise par défaut
2. Créer la Secret Key
Dashboard Stripe → Developers → API keys → Create restricted key.
Étape 1 — Cas d'usage
Stripe te propose 3 cas d'usage :
| Option | Pour qui ? |
|---|---|
| ✅ Alimenter une intégration que vous avez créée | C'est ton cas — tu construis ton backend Next.js qui appelle Stripe |
| Envoi à une application tierce | SaaS externe (Zapier, ProfitWell…) qui se connecte à ton compte |
| Autorisation d'un agent IA | Claude/Cursor MCP qui exécute des actions en autonomie |
Étape 2 — Modèle d'autorisation
Stripe propose 4 templates :
| Template | À choisir ? |
|---|---|
| ✅ Recurring subscriptions and billing (40 autorisations) | Oui — couvre Checkout, Customers, Subscriptions, Portal, Invoices, Coupons, Promotion codes |
| One-time payments | Non — paiements uniques sans abonnement |
| In-person payments with Terminal | Non — lecteurs de cartes physiques |
| Reporting, analytics, and accounting | Non — read-only, tu pourras rien créer |
Étape 3 — Permissions à vérifier / ajouter
Le template "Recurring subscriptions and billing" inclut déjà 90% du nécessaire, mais il manque souvent Refunds + Charges. Après avoir cliqué "Choisir votre offre →", vérifie / ajoute ces 3 lignes à la main :
| Resource | Permission | Inclus dans template ? | Pourquoi |
|---|---|---|---|
| Refunds | Write | ❌ Souvent absent | Action admin "Rembourser le dernier paiement" |
| Charges | Read | ❌ Souvent absent | Lookup du dernier paiement avant le refund |
| Webhook endpoints | Read | ❌ Souvent absent | (optionnel) Debug en cas de pb webhook |
Tableau complet des permissions utilisées par le boilerplate :
| Resource | Permission | Pourquoi |
|---|---|---|
| Checkout Sessions | Write | /api/billing/checkout crée la session d'upgrade |
| Customers | Write | Création / récupération du Customer Stripe attaché au User |
| Subscriptions | Write | Cancel at period end, comp days (/api/admin/billing) |
| Customer Portal | Write | Bouton "Gérer mon abonnement" |
| Invoices | Read | Affichage historique facturation |
| Refunds | Write | Refund admin (refund_last) |
| Charges | Read | Lookup du dernier paiement avant refund |
| Coupons | Write | Sync coupons DB ↔ Stripe |
| Promotion codes | Write | Idem |
| Webhook endpoints | Read | (optionnel) Debug |
💡 En dev, tu peux créer une clé Standard non-restricted (
sk_test_xxx) — plus simple, pas de blocage si tu rajoutes une feature qui utilise une nouvelle resource. En prod, restricted obligatoire (principe du moindre privilège).
3. Variables d'env
Récupère 2 clés du dashboard Developers → API keys :
# .env
# Côté serveur — JAMAIS exposé au browser
STRIPE_SECRET_KEY=sk_test_...
# Côté client — affichée publiquement (rarement utile chez nous, le checkout
# est server-side, mais Stripe.js peut en avoir besoin pour Elements)
STRIPE_PUBLISHABLE_KEY=pk_test_...
# Webhook signing secret — généré à l'étape 4
STRIPE_WEBHOOK_SECRET=whsec_...4. Webhook (Stripe → ton app)
Voir la doc complète : Webhooks Stripe.
En local (dev)
Installe la Stripe CLI puis :
stripe listen --forward-to localhost:3005/api/billing/webhookLa commande affiche un whsec_ de dev — copie-le dans .env →
STRIPE_WEBHOOK_SECRET=whsec_xxx.
Garde la commande tournée pendant que tu testes les flows (checkout, cancel…).
En prod
Dashboard → Developers → Webhooks → Add endpoint :
- URL :
https://ton-domaine.com/api/billing/webhook - Events à écouter :
checkout.session.completedcustomer.subscription.updatedinvoice.payment_succeededinvoice.payment_failedinvoice.upcomingcustomer.subscription.trial_will_endcustomer.subscription.deleted
- Copie le Signing secret →
STRIPE_WEBHOOK_SECRETdans tes env de prod
5. Activer le Customer Portal
Stripe → Settings → Billing → Customer portal → Activate test link.
Configure :
- ✅ Allow customers to update payment methods
- ✅ Allow customers to cancel subscriptions (
ImmediatelyouAt period end) - ✅ Allow customers to switch plans (optionnel)
- Branding : logo + couleur primaire pour matcher ton app
Sans cette activation, le bouton "Gérer mon abonnement" retourne 400.
6. Créer tes plans (auto-sync Stripe)
/admin/plans dans ton app (mode admin) → Nouveau plan.
Bonne nouvelle : tu n'as pas besoin d'aller dans le Dashboard Stripe. Quand tu sauvegardes un plan avec un prix > 0, le boilerplate appelle l'API Stripe et crée automatiquement :
- Le Product (avec nom + description venant des champs localisés)
- Un Price récurrent mensuel si
priceMonthly > 0 - Un Price récurrent annuel si
priceYearly > 0
Les stripeProductId / stripePriceIdMonthly / stripePriceIdYearly sont
remplis automatiquement et persistés en DB.
Modifier un prix
Stripe n'autorise pas la modification d'un Price existant (immutable).
Si tu changes priceMonthly: 2900 → 2900 → 3900 :
- Le boilerplate archive l'ancien Price (
active: false) - Crée un nouveau Price (
price_xxx2) - Met à jour
stripePriceIdMonthlyen DB
Les subscriptions déjà actives sur l'ancien Price continuent à fonctionner — elles facturent toujours le même montant jusqu'au prochain renouvellement. Seuls les nouveaux checkouts utilisent le nouveau Price.
Supprimer un plan
À la suppression côté DB, le boilerplate appelle Stripe pour archiver le Product et tous ses Prices actifs (pas un delete — Stripe rejette le delete si des subs existent).
Lier à un Product Stripe existant
Si tu as déjà créé tes Products manuellement dans le Dashboard, tu peux toujours coller les IDs dans les 3 champs Stripe du plan editor. Le sync respecte les IDs existants et update juste le Product en place.
Si Stripe n'est pas configuré
Sans STRIPE_SECRET_KEY en .env, le sync est skip silencieusement. Le
plan reste en DB avec des stripeProductId / stripePriceId null, et le
flow checkout ne fonctionnera pas tant que tu n'auras pas configuré Stripe
- rééditer/sauvegarder le plan pour déclencher le sync.
Détails : Plans.
7. Test du flow complet
Flow standard
- Crée un compte user dans l'app
/account/upgrade→ choisis un plan- Stripe Checkout s'ouvre
- Carte test :
4242 4242 4242 4242/ n'importe quelle date future / n'importe quel CVC / ZIP12345 - Tu reviens sur l'app → l'abonnement apparaît dans
/account/settings → Facturation - La CLI Stripe en parallèle affiche les events reçus — tu dois voir tous en [200] :
--> payment_intent.succeeded [200]
--> customer.subscription.created [200]
--> invoice.created [200]
--> invoice.finalized [200]
--> invoice.paid [200]
--> invoice.payment_succeeded [200]
--> checkout.session.completed [200]/admin/billing(admin) → tu vois la sub avec le MRR
Cartes test Stripe
Pour tous ces tests, utilise n'importe quelle date future (12/30), n'importe quel CVC (123), n'importe quel ZIP (12345). C'est uniquement le numéro de carte qui détermine le comportement.
✅ Paiements qui réussissent
| Numéro | Comportement |
|---|---|
4242 4242 4242 4242 | Visa standard — succès immédiat, pas de 3DS. Le défaut pour 95% des tests. |
4000 0000 0000 0077 | Succès, sans verification asynchrone (réponse instantanée) |
5555 5555 5555 4444 | Mastercard — succès |
3782 822463 10005 | Amex — succès |
6011 1111 1111 1117 | Discover — succès |
4000 0566 5566 5556 | Visa débit — succès |
🔐 3D Secure (authentification additionnelle)
| Numéro | Comportement |
|---|---|
4000 0027 6000 3184 | Force le 3DS — popup d'authentification, accepte avec OK |
4000 0025 0000 3155 | 3DS facultatif (l'utilisateur peut skip) |
4000 0082 6000 3178 | 3DS requis sur la première utilisation seulement |
❌ Paiements qui échouent
| Numéro | Erreur Stripe |
|---|---|
4000 0000 0000 0002 | card_declined (générique) |
4000 0000 0000 9995 | card_declined — insufficient funds |
4000 0000 0000 9987 | card_declined — lost card |
4000 0000 0000 9979 | card_declined — stolen card |
4000 0000 0000 0069 | expired_card |
4000 0000 0000 0127 | incorrect_cvc |
4000 0000 0000 0119 | processing_error |
4000 0000 0000 0341 | Attache OK puis fail au charge — utile pour tester invoice.payment_failed |
⚠ Scénarios async
| Numéro | Comportement |
|---|---|
4000 0000 0000 0259 | ✅ Paiement réussit puis dispute quelques jours après. Pour tester charge.dispute.created |
4000 0000 0000 1976 | ✅ Réussit puis chargeback (dispute won by customer) |
4000 0000 0000 5126 | ✅ Réussit avec risque élevé flag |
📋 Liste complète
Tester des events sans passer par le browser
La Stripe CLI permet de fire des fake events directement :
stripe trigger checkout.session.completed
stripe trigger customer.subscription.deleted
stripe trigger invoice.payment_failed
stripe trigger charge.dispute.created⚠ Les events fake n'ont pas de vraie metadata.userId — notre handler checkout.session.completed les ignore avec un log. C'est attendu (sécurité). Pour un test complet end-to-end, repasse par le vrai flow /account/upgrade.
Test clocks (avancer le temps)
Pour tester un renouvellement ou la fin d'un trial sans attendre 30 jours :
# Crée un test clock
stripe test_helpers test-clocks create --frozen-time=$(date -u +%s)
# Attache un customer au clock puis avance le temps
stripe test_helpers test-clocks advance <clock_id> --frozen-time=<future_timestamp>Le clock va déclencher tous les events de subscription qui se seraient normalement passés dans cette période (renewal, trial end, etc.).
Tester un refund admin
- Fais un paiement réussi (carte
4242 4242 4242 4242) - Va dans
/admin/billing(admin) - Menu actions sur la row → "Rembourser le dernier paiement"
- Tu dois voir dans
stripe listen:--> charge.refunded [200] - Côté Stripe Dashboard → Payments → le PaymentIntent passe en Refunded
8. Passer en live
Quand tu es prêt à shipper :
- Active ton compte Stripe (remplis KYC : numéro SIRET, RIB, ID)
- Toggle Test mode → Live mode
- Recrée les mêmes Products + Prices (Stripe ne sync pas test ↔ live)
- Recrée la restricted key en live avec les permissions ci-dessus
- Recrée le webhook en live (URL prod cette fois)
- Update tes env de prod avec les nouvelles clés
sk_live_xxx/pk_live_xxx/whsec_xxx - Update les Price IDs des plans dans
/admin/plans(les IDs test ne marchent pas en live)
Sécurité
- Ne commit jamais
STRIPE_SECRET_KEY(déjà dans.gitignorevia.env) - Restricted keys > Standard keys en prod
- Si une clé fuite : Roll immédiatement (Developers → API keys → Roll key)
- Le webhook valide la signature via
STRIPE_WEBHOOK_SECRET— sans ça, n'importe qui peut POST/api/billing/webhookavec du JSON Stripe-shaped. Notre handler reject les requests non-signées.
Allez plus loin
- Billing (admin + user) — pages billing et actions admin
- Webhooks Stripe — détail events + idempotence
- Plans tarifaires — config des plans + Price IDs
- Coupons — codes promo synchronisés Stripe