Intégrer Stripe proprement

Stripe est le standard pour le paiement en ligne. Mais une intégration bâclée cause des paiements perdus, des doublons, et des nuits blanches. Voici comment faire une intégration robuste et maintenable.

Les erreurs classiques à éviter

Ce qu'on voit trop souvent

  • Valider la commande au retour du client : le client peut fermer l'onglet, et vous perdez la vente
  • Ignorer les webhooks : seul moyen fiable de savoir si un paiement est vraiment validé
  • Stocker les données de carte : interdit et dangereux
  • Pas d'idempotence : risque de double facturation
  • Pas de gestion des erreurs : le client ne sait pas ce qui s'est passé

Le principe fondamental

Ne faites jamais confiance au retour du navigateur. Le client peut fermer l'onglet, perdre sa connexion, ou avoir un navigateur qui plante. Seul le webhook Stripe est la source de vérité.

Architecture recommandée

Voici le flux robuste pour un paiement e-commerce :

  1. Client clique sur "Payer"
  2. Votre serveur crée une commande en statut "pending"
  3. Votre serveur crée une Checkout Session Stripe
  4. Client redirigé vers Stripe pour payer
  5. Stripe envoie un webhook "checkout.session.completed"
  6. Votre serveur valide la commande (statut "paid")
  7. Client redirigé vers page de confirmation

Point clé : L'étape 6 (validation) se fait via le webhook, PAS au retour du client (étape 7). Le client voit une page de confirmation, mais la vraie validation a déjà eu lieu côté serveur.

Implémentation : Checkout Session

Stripe Checkout est la méthode recommandée. Stripe gère l'UI de paiement, vous gérez le métier.

Création de la session

                
app/Services/PaymentService.php
use Stripe\Stripe; use Stripe\Checkout\Session; class PaymentService { public function createCheckoutSession(Order $order): Session { Stripe::setApiKey(config('services.stripe.secret')); return Session::create([ // Référence unique pour l'idempotence 'client_reference_id' => $order->id, // Métadonnées pour le webhook 'metadata' => [ 'order_id' => $order->id, 'customer_email' => $order->email, ], // Produits 'line_items' => $this->buildLineItems($order), 'mode' => 'payment', // URLs de retour 'success_url' => route('checkout.success', ['order' => $order->id]), 'cancel_url' => route('checkout.cancel', ['order' => $order->id]), // Options 'payment_method_types' => ['card'], 'locale' => 'fr', 'expires_at' => now()->addMinutes(30)->timestamp, ]); } private function buildLineItems(Order $order): array { return $order->items->map(fn($item) => [ 'price_data' => [ 'currency' => 'eur', 'product_data' => [ 'name' => $item->product->name, 'images' => [$item->product->image_url], ], 'unit_amount' => $item->price * 100, // Centimes ], 'quantity' => $item->quantity, ])->toArray(); } }

Controller de checkout

                
app/Http/Controllers/CheckoutController.php
class CheckoutController extends Controller { public function process(Request $request, PaymentService $payment) { // 1. Créer la commande en "pending" $order = Order::create([ 'user_id' => auth()->id(), 'status' => 'pending', 'total' => $request->cart()->total(), ]); // 2. Créer les lignes de commande $order->createItemsFromCart($request->cart()); // 3. Créer la session Stripe $session = $payment->createCheckoutSession($order); // 4. Stocker l'ID session pour vérification $order->update(['stripe_session_id' => $session->id]); // 5. Rediriger vers Stripe return redirect($session->url); } public function success(Order $order) { // Page de confirmation (le paiement est déjà validé par webhook) return view('checkout.success', compact('order')); } }

Les Webhooks : la clé de voûte

Le webhook est le seul moyen fiable de savoir qu'un paiement est réussi.

Configuration

                
app/Http/Controllers/WebhookController.php
use Stripe\Webhook; use Stripe\Exception\SignatureVerificationException; class StripeWebhookController extends Controller { public function handle(Request $request) { $payload = $request->getContent(); $signature = $request->header('Stripe-Signature'); try { // Vérifier la signature (CRUCIAL pour la sécurité) $event = Webhook::constructEvent( $payload, $signature, config('services.stripe.webhook_secret') ); } catch (SignatureVerificationException $e) { return response('Invalid signature', 400); } // Router vers le bon handler match($event->type) { 'checkout.session.completed' => $this->handleCheckoutCompleted($event), 'payment_intent.payment_failed' => $this->handlePaymentFailed($event), default => null, }; return response('OK', 200); } private function handleCheckoutCompleted($event): void { $session = $event->data->object; $orderId = $session->metadata->order_id; $order = Order::find($orderId); if (!$order || $order->status !== 'pending') { return; // Déjà traité ou introuvable } // Valider la commande $order->update([ 'status' => 'paid', 'paid_at' => now(), 'stripe_payment_id' => $session->payment_intent, ]); // Actions post-paiement $order->sendConfirmationEmail(); $order->decrementStock(); } }

Points critiques des webhooks

  • Toujours vérifier la signature : sinon n'importe qui peut simuler un webhook
  • Être idempotent : le même webhook peut arriver plusieurs fois
  • Répondre vite : Stripe attend une réponse en <30s, faites le traitement lourd en async
  • Logger tout : pour débugger les problèmes

Gestion des erreurs

Les paiements échouent. Carte refusée, 3D Secure abandonné, timeout... Gérez ces cas proprement.

                
Gestion des erreurs côté client
public function cancel(Order $order) { // Marquer comme annulé (ou laisser expirer) if ($order->status === 'pending') { $order->update(['status' => 'cancelled']); } return view('checkout.cancel', [ 'order' => $order, 'message' => 'Paiement annulé. Votre panier a été conservé.', ]); }

Messages d'erreur utilisateur

Carte refusée : "Votre carte a été refusée. Vérifiez vos informations ou essayez une autre carte."
3DS abandonné : "Authentification annulée. Veuillez réessayer."
Erreur technique : "Une erreur est survenue. Votre carte n'a pas été débitée."

Bonnes pratiques

Sécurité

  • Ne jamais logger les données de carte
  • Toujours utiliser HTTPS
  • Vérifier les signatures webhook
  • Stocker les clés en variables d'environnement

Fiabilité

  • Traitement via webhook, pas au retour client
  • Idempotence sur tous les endpoints
  • Retry logic pour les webhooks
  • Monitoring des paiements échoués

UX

  • Messages d'erreur clairs
  • Page de confirmation même si paiement en cours de validation
  • Email de confirmation rapide
  • Numéro de commande visible

En production : monitoring

Ce qu'il faut surveiller

Taux de conversion checkout : combien de sessions aboutissent ?
Webhooks en échec : alertez si des webhooks ne passent pas
Commandes pending : aucune ne doit rester plus de 30 min
Erreurs de paiement : pics anormaux = problème

Besoin d'aide sur votre projet ?

Je peux vous accompagner dans le développement ou l'optimisation de votre application.

Me contacter