Changer le thème

Next.js e-commerce : guide complet panier et paiement Stripe

Easton editorial illustration: build pipeline conveyor

Vingt-septième relecture de la config Stripe Webhook : des utilisateurs disent que l’argent est débité mais la commande reste « en attente de paiement ». En test tout roule — pourquoi ça casse en prod ?

Ma première boutique Next.js, je pensais que le dur serait l’UI et le CSS. En pratique : state du panier, intégration paiement, flux commande — chaque étape est un piège. Redux trop lourd, Context API critiqué sur les perfs, doc Stripe en anglais, et le Webhook ? Mystère total.

Pire : la plupart des tutos ne couvrent que le panier ou le paiement, rarement la chaîne complète. « Quelle lib de state ? », « À quoi sert le Webhook ? », « Comment aligner statut commande et paiement ? »

Cet article vise à combler ces trous. Zustand pour le panier (léger), Stripe pour le paiement, Webhook pour les commandes (seule voie fiable). Code complet à chaque étape, copiable tel quel.

Vous connaissez ce soulagement quand tout passe au vert d’un coup ? En suivant ce guide, vous l’aurez.

Pourquoi Zustand pour le panier ?

State management en 2025 : arrêtez de tergiverser

Franchement, le choix de lib fatigue. Redux = gros livre, Context = articles sur les perfs, Zustand = « trop neuf ». J’ai oscillé entre les trois jusqu’à voir les chiffres.

Depuis 2021, Zustand est parmi les libs React state les plus suivies. En 2025, le modèle tient : fonctionnel, hooks, API courte. Courbe d’apprentissage douce — pas action/reducer/dispatch/middleware comme Redux.

Règle simple :

  • Petit (< 10 pages) : Context API, pas de surcouche
  • Moyen (10-50) : Zustand
  • Grand (50+, plusieurs équipes) : Redux Toolkit

Un panier coche trois cases : partage entre composants (liste, icône, checkout), persistance (refresh), perfs (re-render ciblé). Zustand gère tout ça avec moins de code que Redux.

Code panier Zustand

D’abord les deps :

npm install zustand

Store (/store/cartStore.js) :

import { create } from 'zustand'
import { persist } from 'zustand/middleware'

export const useCartStore = create(
  persist(
    (set, get) => ({
      // État
      items: [], // [{ id, name, price, quantity, image }]

      // Propriétés calculées
      get total() {
        return get().items.reduce((sum, item) => sum + item.price * item.quantity, 0)
      },
      get count() {
        return get().items.reduce((sum, item) => sum + item.quantity, 0)
      },

      // Actions
      addItem: (product) => set((state) => {
        const existing = state.items.find(item => item.id === product.id)
        if (existing) {
          return {
            items: state.items.map(item =>
              item.id === product.id
                ? { ...item, quantity: item.quantity + 1 }
                : item
            )
          }
        } else {
          return { items: [...state.items, { ...product, quantity: 1 }] }
        }
      }),

      removeItem: (productId) => set((state) => ({
        items: state.items.filter(item => item.id !== productId)
      })),

      updateQuantity: (productId, quantity) => set((state) => ({
        items: state.items.map(item =>
          item.id === productId ? { ...item, quantity } : item
        )
      })),

      clearCart: () => set({ items: [] })
    }),
    {
      name: 'shopping-cart', // clé localStorage
    }
  )
)

items stocke les lignes, total et count sont calculés, les méthodes gèrent le CRUD. persist écrit dans localStorage — pas de perte au refresh.

Dans un composant :

import { useCartStore } from '@/store/cartStore'

function ProductCard({ product }) {
  const addItem = useCartStore(state => state.addItem)

  return (
    <button onClick={() => addItem(product)}>
      Ajouter au panier
    </button>
  )
}

function CartIcon() {
  const count = useCartStore(state => state.count)

  return <div>Panier ({count})</div>
}

useCartStore(state => state.addItem) = sélecteur : souscription à addItem seulement, pas de re-render si le reste du panier change. C’est le secret des perfs Zustand.

Déjà sur Redux ? Pas besoin de migrer. Redux Toolkit + RTK Query (ex. projet C-Shopping) trace bien le flux. Pour un nouveau projet, je penche Zustand : moins de friction, livraison plus rapide.

Intégration Stripe de bout en bout

Comprendre le flux Stripe

Première lecture de la doc : Checkout Session, Payment Intent, redirection… « Pourquoi quitter mon site ? »

En fait c’est linéaire :

  1. Frontend : clic « Payer » → appel API pour créer une Checkout Session
  2. Backend : Session créée → session.id
  3. Frontend : session.id → Stripe.js → page de paiement hébergée
  4. Utilisateur : saisie carte, validation
  5. Stripe : succès → Webhook vers votre backend
  6. Backend : Webhook → commande, stock, e-mail
  7. Stripe : redirection vers success_url

Règle d’or : ne jamais traiter le succès paiement côté frontend. Fermeture d’onglet, réseau, utilisateur qui ne revient pas — seul le Webhook est fiable (détail plus bas).

Créer une Checkout Session Stripe

npm install stripe @stripe/stripe-js

.env.local :

STRIPE_SECRET_KEY=sk_test_xxxxx  # backend uniquement, jamais exposé
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxx  # signature Webhook

/pages/api/create-checkout.js :

import Stripe from 'stripe'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)

export default async function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' })
  }

  try {
    const { items } = req.body

    const lineItems = items.map(item => ({
      price_data: {
        currency: 'usd',
        product_data: {
          name: item.name,
          images: [item.image],
        },
        unit_amount: Math.round(item.price * 100), // centimes
      },
      quantity: item.quantity,
    }))

    const session = await stripe.checkout.sessions.create({
      payment_method_types: ['card'],
      line_items: lineItems,
      mode: 'payment',
      success_url: `${req.headers.origin}/success?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `${req.headers.origin}/cart`,
      metadata: {
        userId: req.user?.id || 'guest',
      },
    })

    res.status(200).json({ sessionId: session.id })
  } catch (err) {
    console.error('Échec création Checkout Session:', err)
    res.status(500).json({ error: err.message })
  }
}

Notes :

  • unit_amount × 100 (Stripe en centimes : 99,99 $ → 9999)
  • {CHECKOUT_SESSION_ID} remplacé par Stripe dans success_url
  • metadata lisible dans le Webhook (userId, notes, etc.)

Checkout côté frontend

/pages/checkout.js :

import { loadStripe } from '@stripe/stripe-js'
import { useCartStore } from '@/store/cartStore'

const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY)

export default function CheckoutPage() {
  const { items, total } = useCartStore()

  const handleCheckout = async () => {
    try {
      const response = await fetch('/api/create-checkout', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ items }),
      })

      const { sessionId } = await response.json()

      const stripe = await stripePromise
      const { error } = await stripe.redirectToCheckout({ sessionId })

      if (error) {
        console.error('Échec redirection paiement:', error)
        alert(error.message)
      }
    } catch (err) {
      console.error('Échec lancement paiement:', err)
      alert('Paiement impossible, réessayez plus tard')
    }
  }

  return (
    <div>
      <h1>Checkout</h1>
      {items.map(item => (
        <div key={item.id}>
          {item.name} x {item.quantity} = ${item.price * item.quantity}
        </div>
      ))}
      <div>Total : ${total}</div>
      <button onClick={handleCheckout}>Payer</button>
    </div>
  )
}

Stripe gère formulaire carte, 3DS, anti-fraude. Personnalisation couleurs/logo/polices possible ; UI 100 % custom = Stripe Elements, plus complexe — pas idéal pour débuter.

Page de succès (affichage seulement)

Après paiement, redirection vers success_url :

// /pages/success.js
import { useEffect, useState } from 'react'
import { useRouter } from 'next/router'

export default function SuccessPage() {
  const router = useRouter()
  const { session_id } = router.query
  const [order, setOrder] = useState(null)

  useEffect(() => {
    if (session_id) {
      fetch(`/api/order?session_id=${session_id}`)
        .then(res => res.json())
        .then(data => setOrder(data))
    }
  }, [session_id])

  if (!order) return <div>Chargement...</div>

  return (
    <div>
      <h1>Paiement réussi !</h1>
      <p>Commande : {order.id}</p>
      <p>Montant : ${order.total}</p>
    </div>
  )
}

Cette page est cosmétique. La commande réelle se crée dans le Webhook — section suivante.

Webhook : commandes et synchronisation d’état

Pourquoi le Webhook est indispensable

Ma première version : « retour sur success = payé », toute la logique métier là. Tests : l’utilisateur ferme l’onglet après paiement — pas de commande, panique.

Doc Stripe : Webhook = seule source fiable pour les commandes.

  • Redirection utilisateur : fragile (fermeture, réseau, pas de clic)
  • Sécurité : création commande, stock, expédition = backend uniquement
  • Recommandation Stripe : métier critique dans le Webhook

En bref : Stripe appelle votre serveur (« paiement OK », « abonnement annulé », etc.) et vous réagissez.

Endpoint Webhook

/pages/api/stripe-webhook.js :

import Stripe from 'stripe'
import { buffer } from 'micro'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET

export const config = {
  api: {
    bodyParser: false,
  },
}

export default async function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).send('Method not allowed')
  }

  const buf = await buffer(req)
  const sig = req.headers['stripe-signature']

  let event

  try {
    event = stripe.webhooks.constructEvent(buf, sig, webhookSecret)
  } catch (err) {
    console.error('Échec vérification signature Webhook:', err.message)
    return res.status(400).send(`Webhook Error: ${err.message}`)
  }

  switch (event.type) {
    case 'checkout.session.completed':
      await handleCheckoutSessionCompleted(event.data.object)
      break
    case 'payment_intent.succeeded':
      await handlePaymentIntentSucceeded(event.data.object)
      break
    case 'invoice.payment_failed':
      await handleInvoicePaymentFailed(event.data.object)
      break
    default:
      console.log(`Événement non géré: ${event.type}`)
  }

  res.status(200).json({ received: true })
}

async function handleCheckoutSessionCompleted(session) {
  console.log('Paiement OK!', session.id)

  const userId = session.metadata.userId
  const sessionId = session.id
  const total = session.amount_total / 100

  const existingOrder = await db.order.findUnique({
    where: { stripeSessionId: sessionId }
  })

  if (existingOrder) {
    console.log('Commande déjà existante, skip')
    return
  }

  const order = await db.order.create({
    data: {
      userId,
      stripeSessionId: sessionId,
      status: 'paid',
      total,
    }
  })

  await updateInventory(order.items)
  await sendOrderConfirmationEmail(userId, order)

  console.log('Commande créée:', order.id)
}

async function handlePaymentIntentSucceeded(paymentIntent) {
  console.log('Paiement confirmé:', paymentIntent.id)
}

async function handleInvoicePaymentFailed(invoice) {
  console.log('Échec paiement:', invoice.id)
}

Trois impératifs :

  1. bodyParser: false — corps brut pour la signature
  2. constructEvent — prouve que l’appel vient de Stripe
  3. IdempotencestripeSessionId unique évite les doublons si Stripe renvoie l’événement

Tester le Webhook en local

Stripe n’atteint pas localhost sans CLI :

# Mac
brew install stripe/stripe-cli/stripe

# Windows (Scoop)
scoop install stripe
stripe login
stripe listen --forward-to localhost:3000/api/stripe-webhook

Copier le whsec_xxxxx temporaire dans .env.local, puis :

stripe trigger checkout.session.completed

J’ai bloqué des heures sur « signature invalide » : bodyParser encore actif. N’oubliez pas export const config.

Cycle de vie des statuts commande

En attente → Payé → Préparation → Expédié → Terminé

        Annulé / Remboursé

Schéma Prisma :

model Order {
  id               String   @id @default(cuid())
  stripeSessionId  String   @unique
  userId           String
  status           OrderStatus @default(PENDING)
  total            Float
  createdAt        DateTime @default(now())
  updatedAt        DateTime @updatedAt
}

enum OrderStatus {
  PENDING
  PAID
  PREPARING
  SHIPPED
  COMPLETED
  CANCELLED
  REFUNDED
}

checkout.session.completedPAID. Expédition et clôture via back-office ou automatisation.

Gestion des erreurs Webhook

async function handleCheckoutSessionCompleted(session) {
  try {
    // logique métier
  } catch (error) {
    console.error('Échec traitement commande:', error)
    await logError({
      type: 'webhook_error',
      event: 'checkout.session.completed',
      sessionId: session.id,
      error: error.message,
    })
    throw error // Stripe réessaiera
  }
}

Échec ? Stripe réessaie 3 jours. Dashboard → Webhooks → historique → Resend manuel possible.

Flux commande complet

Tous les morceaux sont là — voici la chaîne de bout en bout.

Parcours utilisateur

  1. Fiche produit : « Ajouter au panier » → Zustand, badge +1
  2. Panier : quantités, « Checkout »
  3. Checkout : récap, « Payer »
  4. Frontend : POST /api/create-checkout avec items
  5. Backend : Session Stripe, sessionId
  6. Frontend : redirection page Stripe
  7. Utilisateur : carte, « Pay »
  8. Stripe : Webhook → /api/stripe-webhook
  9. Webhook : signature → commande → stock → e-mail
  10. Stripe : redirect /success?session_id=xxx
  11. Frontend : GET /api/order?session_id=xxx pour l’affichage

L’étape 9 doit tenir dans le Webhook — pas l’étape 11.

Modèle de données

model Order {
  id               String      @id @default(cuid())
  stripeSessionId  String      @unique
  userId           String
  status           OrderStatus @default(PENDING)
  total            Float
  items            OrderItem[]
  createdAt        DateTime    @default(now())
  updatedAt        DateTime    @updatedAt

  user User @relation(fields: [userId], references: [id])
}

model OrderItem {
  id        String @id @default(cuid())
  orderId   String
  productId String
  quantity  Int
  price     Float  // prix au moment de la commande

  order   Order   @relation(fields: [orderId], references: [id])
  product Product @relation(fields: [productId], references: [id])
}

OrderItem.price = prix figé à la commande, pas le prix catalogue actuel — important si les tarifs changent.

Cas limites

1. Stock insuffisant

Avant de créer la Session :

const { items } = req.body

for (const item of items) {
  const product = await db.product.findUnique({ where: { id: item.id } })
  if (product.stock < item.quantity) {
    return res.status(400).json({ error: `Stock insuffisant pour ${product.name}` })
  }
}

2. Paiement OK mais Webhook en échec

Retry Stripe 3 jours + Resend Dashboard + job cron pour Sessions payées sans commande.

3. Payé mais pas expédié à temps

Re-vérifier le stock avant expédition ; sinon contact client (remboursement / substitution).

Déploiement production

Le test vert ne suffit pas — quelques pièges en prod.

Variables d’environnement

STRIPE_SECRET_KEY=sk_live_xxxxx
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_xxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxx

Configurer sur Vercel/Netlify. Ne jamais committer la Secret Key.

Endpoint Webhook production

Plus de CLI : Dashboard Stripe

  1. Stripe Dashboard
  2. Developers → Webhooks → Add endpoint
  3. URL : https://yourdomain.com/api/stripe-webhook
  4. Événements : checkout.session.completed, payment_intent.succeeded, etc.
  5. Copier le Signing secret dans les variables d’env

Premier déploiement sans ça : commandes fantômes — j’ai perdu une demi-journée avant de voir zéro appel Webhook.

Checklist sécurité

  • ✅ Paiement côté backend (frontend = redirection)
  • ✅ Signature Webhook (constructEvent)
  • ✅ Montant Webhook = montant commande (anti-manipulation)
  • ✅ Idempotence stripeSessionId
  • ✅ Logs (Sentry, Datadog…)
  • ✅ Alertes (échec Webhook, taux de succès)

Re-valider le montant dans le Webhook même si la Session est créée côté serveur — défense en profondeur.

Monitoring

import * as Sentry from '@sentry/nextjs'

export default async function handler(req, res) {
  try {
    // logique Webhook
  } catch (error) {
    Sentry.captureException(error, {
      tags: {
        type: 'stripe_webhook',
        event: event.type,
      },
    })
    throw error
  }
}

Indicateurs :

  • Taux d’échec Webhook (> 5 % → alerte)
  • Taux de succès paiement (chute = config ou incident Stripe)
  • Durée création commande (> 3 s → investigation)

Synthèse : de zéro à la prod

  1. Panier : Zustand ou Redux Toolkit selon taille ; persist pour localStorage
  2. Paiement : Checkout Session + page hébergée Stripe
  3. Commandes : Webhook — création, stock, e-mails
  4. Prod : env, endpoint Webhook, monitoring

Trois principes :

  • Paiement = backend — le frontend n’est pas de confiance
  • Webhook = source de vérité — la redirection utilisateur ne suffit pas
  • Sécurité d’abord — signature, idempotence, logs

Première fois ? Environnement test Stripe, carte 4242 4242 4242 4242, puis bascule live.

Ressources :

  • Documentation Stripe
  • Guide Next.js + Stripe 2025 (Pedro Alonso, « Stripe Next.js 15 complete guide »)
  • Projet open source C-Shopping (Redux Toolkit + Stripe)

Moins de pièges, plus de commandes qui passent seules — bon courage.

Implémentation complète panier Next.js et paiement Stripe

Étapes détaillées pour monter panier et paiement de zéro : state management, intégration Stripe et traitement des commandes

⏱️ Estimated time: 2 hr

  1. 1

    Step 1: Installer les dépendances et configurer le panier Zustand

    Installer Zustand :
    • npm install zustand

    Créer le store panier (/store/cartStore.js) :
    • tableau items pour les produits
    • propriétés calculées total et count
    • méthodes addItem, removeItem, updateQuantity, clearCart
    • middleware persist vers localStorage

    Points clés :
    • persist persiste automatiquement, pas de perte au refresh
    • sélecteurs (useCartStore(state => state.addItem)) pour éviter les re-renders inutiles

    Cas d'usage : projets moyens (10-50 pages), state management léger
  2. 2

    Step 2: Créer l'API Stripe Checkout Session

    Installer Stripe :
    • npm install stripe @stripe/stripe-js

    Variables (.env.local) :
    • STRIPE_SECRET_KEY=sk_test_xxxxx (backend, secret)
    • NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxxxx (frontend)
    • STRIPE_WEBHOOK_SECRET=whsec_xxxxx (signature Webhook)

    Route API (/pages/api/create-checkout.js) :
    • recevoir les items du panier
    • convertir en line_items Stripe (unit_amount × 100)
    • créer checkout.sessions (success_url, cancel_url)
    • metadata pour données métier (userId, etc.)
    • renvoyer sessionId au frontend

    Détails :
    • Stripe utilise les centimes, prix × 100
    • success_url avec placeholder {CHECKOUT_SESSION_ID}
    • metadata récupérable dans le Webhook
  3. 3

    Step 3: Appeler Stripe Checkout côté frontend

    Page checkout (/pages/checkout.js) :
    • loadStripe pour Stripe.js
    • appeler /api/create-checkout
    • stripe.redirectToCheckout() vers la page de paiement

    Gestion d'erreurs :
    • catch erreurs réseau
    • vérifier error de redirectToCheckout
    • message utilisateur clair

    Page hébergée Stripe :
    • pas de formulaire carte à écrire
    • validation carte et anti-fraude inclus
    • couleurs, logo, polices personnalisables

    Attention :
    • ne jamais traiter le succès paiement côté frontend
    • l'utilisateur peut fermer le navigateur
    • création de commande uniquement dans le Webhook
  4. 4

    Step 4: Configurer le Webhook pour les commandes

    API Webhook (/pages/api/stripe-webhook.js) :

    Obligatoire :
    • export const config = { api: { bodyParser: false } }
    • buffer(req) pour le corps brut
    • stripe.webhooks.constructEvent() pour la signature

    Événements :
    • checkout.session.completed : paiement OK, créer commande
    • payment_intent.succeeded : confirmer encaissement
    • invoice.payment_failed : échec abonnement

    Idempotence :
    • vérifier si stripeSessionId existe déjà
    • index unique en base
    • éviter commandes en double si Webhook rejoué

    Métier :
    • commande (status: paid)
    • déstockage (updateInventory)
    • e-mail confirmation (sendOrderConfirmationEmail)
    • logs et erreurs

    Test local :
    • stripe login
    • stripe listen --forward-to localhost:3000/api/stripe-webhook
    • stripe trigger checkout.session.completed

    Critique : bodyParser désactivé, signature vérifiée ; Stripe réessaie 3 jours en cas d'échec
  5. 5

    Step 5: Déploiement production et sécurité

    Variables :
    • clés live (sk_live_xxxxx, pk_live_xxxxx)
    • configurer sur Vercel/Netlify
    • ne jamais committer la Secret Key

    Stripe Dashboard :
    • Developers → Webhooks
    • endpoint production https://yourdomain.com/api/stripe-webhook
    • événements checkout.session.completed, etc.
    • Signing secret dans les variables d'environnement

    Checklist sécurité :
    • logique paiement côté backend uniquement
    • signature Webhook validée
    • montant paiement = montant commande
    • idempotence stripeSessionId
    • logs paiement
    • alertes si échec Webhook > 5 %

    Monitoring :
    • taux d'échec Webhook, taux de succès paiement, durée création commande (> 3 s à investiguer)
    • Sentry/LogRocket, règles d'alerte, logs Dashboard Stripe

    Tests :
    • carte test 4242 4242 4242 4242
    • flux panier → paiement → Webhook → commande
    • scénarios d'échec (stock, Webhook)

FAQ

Redux ou Zustand : lequel pour mon projet ?
Selon la taille et l'équipe :

• Petit projet (< 10 pages) : Context API suffit, pas de lib dédiée
• Moyen (10-50 pages) : Zustand, léger, courbe d'apprentissage faible, peu de code
• Grand (50+ pages, plusieurs équipes) : Redux Toolkit, outillage et debug solides, communauté mature

Cas concrets :
• Nouveau projet, itération rapide : Zustand
• Déjà sur Redux : garder Redux Toolkit
• Équipe peu familière du state : Zustand plus accessible

Pour un panier : partage inter-composants, persistance, perfs — Zustand convient bien.
Pourquoi ne pas gérer le succès du paiement côté frontend ?
Problèmes majeurs :

Fiabilité :
• fermeture du navigateur après paiement
• redirection ratée (réseau)
• utilisateur qui ne clique pas sur Terminer

Sécurité :
• code frontend modifiable
• création de commande et stock sensibles, pas côté client
• impossible d'empêcher un faux succès

Bonne pratique :
• logique métier dans le Webhook
• notification serveur Stripe → votre backend (sans le navigateur)
• signature Webhook
• recommandation officielle Stripe : Webhook = source fiable unique pour les commandes

La page success ne sert qu'à l'affichage, pas au métier.
La vérification de signature Webhook échoue toujours — que faire ?
Causes fréquentes :

La plus courante (90 %) :
• bodyParser Next.js encore actif
• fix : export const config = { api: { bodyParser: false } }

Autres :
• STRIPE_WEBHOOK_SECRET incorrect dans .env.local
• secret test vs production mélangés
• middleware global qui modifie le body

Debug :
1. confirmer bodyParser: false
2. logger req.headers['stripe-signature']
3. stripe listen --forward-to localhost:3000/api/stripe-webhook
4. lire l'erreur détaillée du CLI
5. utiliser le webhook secret temporaire du CLI

Local :
• Stripe CLI obligatoire pour forward
• secret whsec_xxxxx temporaire
• nouveau secret à chaque redémarrage du CLI → mettre à jour .env.local
Comment éviter plusieurs commandes si le Webhook est appelé en double ?
Idempotence :

Base de données :
• index unique sur stripeSessionId
• Prisma : stripeSessionId String @unique
• insertion dupliquée refusée

Code :
• findUnique avant create
• si existe, return sans recréer

Exemple :
```javascript
const existingOrder = await db.order.findUnique({
where: { stripeSessionId: sessionId }
})

if (existingOrder) {
console.log('Commande déjà existante, skip')
return
}

const order = await db.order.create({ ... })
```

Pourquoi :
• Stripe peut renvoyer le Webhook (réseau, retry)
• le code doit tolérer les doublons
• éviter plusieurs commandes et déstockages pour un même paiement

Conseils : logs de chaque appel, monitoring des doublons, alertes.
En production les commandes ne se créent pas — par où commencer ?
Ordre de diagnostic :

1. Webhook reçu ?
• Stripe Dashboard → Developers → Webhooks
• historique et statut succès/échec
• aucun appel → problème d'endpoint

2. Endpoint :
• URL https://yourdomain.com/api/stripe-webhook correcte
• événement checkout.session.completed coché
• endpoint actif

3. Variables :
• STRIPE_WEBHOOK_SECRET correct
• secret de production, pas test
• variables sur Vercel/Netlify

4. Code Webhook :
• bodyParser désactivé
• signature OK
• logs d'erreur

5. Logs applicatifs :
• Vercel Logs, CloudWatch, etc.
• stack trace, handler exécuté ?

6. Test manuel :
• Webhook en échec → Resend dans le Dashboard
• observer succès et message d'erreur

Erreurs fréquentes :
• pas d'endpoint Webhook en prod
• secret test en prod
• firewall bloque Stripe

Validation : carte test, commande, stock, e-mail OK.

10 min de lecture · Publié le: 7 janv. 2026 · Mis à jour le: 27 juil. 2026

Commentaires

Connectez-vous avec GitHub pour laisser un commentaire

Easton BlogEaston Blog