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 :
- Client clique sur "Payer"
- Votre serveur crée une commande en statut "pending"
- Votre serveur crée une Checkout Session Stripe
- Client redirigé vers Stripe pour payer
- Stripe envoie un webhook "checkout.session.completed"
- Votre serveur valide la commande (statut "paid")
- 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