J'ai récemment déployé un système d'abonnement Stripe complet sur SunderDev lui-même. Voici les décisions d'architecture qui ont évité les pièges habituels.

Le SDK officiel

composer require stripe/stripe-php

En 2026 la version 15+ est stable. Toujours préférer le SDK officiel à une implémentation maison : les événements webhook évoluent, les idempotency keys sont gérées, les retries aussi.

Architecture recommandée

  1. Checkout hébergé par Stripe : PCI compliance offerte, 0 token de carte qui transite chez vous.
  2. Webhooks comme source de vérité : jamais faire confiance au redirect client.
  3. Customer Portal : laissez Stripe gérer les upgrades/downgrades/annulations.
  4. Idempotency keys sur toutes les créations.

Le flow complet

1. Création de la session Checkout

$session = \Stripe\Checkout\Session::create([
    'mode' => 'subscription',
    'customer_email' => $user->email,
    'line_items' => [['price' => $priceId, 'quantity' => 1]],
    'success_url' => $baseUrl . '/abonnement-success?session_id={CHECKOUT_SESSION_ID}',
    'cancel_url' => $baseUrl . '/abonnement-cancel',
    'metadata' => ['user_id' => $user->id],
    'subscription_data' => ['metadata' => ['user_id' => $user->id]],
]);
header('Location: ' . $session->url);

2. Vérification signature webhook

$payload = file_get_contents('php://input');
$sig = $_SERVER['HTTP_STRIPE_SIGNATURE'] ?? '';

try {
    $event = \Stripe\Webhook::constructEvent($payload, $sig, $webhookSecret);
} catch (\UnexpectedValueException | \Stripe\Exception\SignatureVerificationException $e) {
    http_response_code(400);
    exit('Invalid webhook');
}

Ne jamais, jamais traiter un webhook sans vérification signature. C'est la porte ouverte à la fraude.

3. Événements à gérer

  • checkout.session.completed : activation initiale.
  • customer.subscription.updated : changement de plan.
  • customer.subscription.deleted : annulation effective.
  • invoice.payment_succeeded : reconnaissance de revenu.
  • invoice.payment_failed : relance client + dégradation.

4. Idempotence

Stockez le event->id dans une table dédiée avant traitement. Stripe retry les webhooks jusqu'à succès — sans idempotence, vous allez doubler vos actions.

if ($db->processedEventExists($event->id)) {
    http_response_code(200);
    exit('OK — already processed');
}
$db->markProcessed($event->id);

Les pièges à éviter

  • Ne pas fier à success_url pour activer un compte. Un utilisateur peut fermer l'onglet — le webhook est votre seule source fiable.
  • Timezones : current_period_end est en timestamp Unix UTC. Toujours convertir explicitement.
  • Prorata : les changements de plan mid-cycle créent des invoice.updated avec des lignes de crédit. Testez-les.
  • Tax : si vous vendez en Europe, activez Stripe Tax sinon vous vous faites rattraper.

Customer Portal

Une seule API, une app entière "gérer mon abonnement" clé en main :

$portal = \Stripe\BillingPortal\Session::create([
    'customer' => $customerId,
    'return_url' => $baseUrl . '/mon-compte',
]);
header('Location: ' . $portal->url);

Upgrades, downgrades, mise à jour CB, factures téléchargeables, annulations — tout est géré par Stripe. Ne réinventez pas la roue.

Tests en local

Stripe CLI permet de forwarder les webhooks vers votre localhost :

stripe listen --forward-to localhost:8000/webhook.php

Puis stripe trigger invoice.payment_failed pour simuler chaque cas.

Conclusion

L'intégration Stripe en elle-même est propre. La difficulté est dans la logique métier autour : provisioning, dégradation en cas d'impayé, notifications, relances. Prévoyez du temps pour ça, pas pour l'API en elle-même.

Docs officielles : Subscriptions overview et Webhooks guide.

Vous voulez mettre en place Stripe proprement sur votre SaaS ? Je l'ai fait plusieurs fois, je peux vous faire gagner 2 semaines.