Skip to main content

Introduction

Les webhooks sont essentiels pour une intégration Stripe robuste. Ils permettent à Stripe de notifier votre serveur en temps réel des événements de paiement : succès, échec, remboursement, litige, etc. Sans webhooks, vous ne pouvez pas garantir la fiabilité de votre système de paiement.

Pourquoi les webhooks sont indispensables

Imaginez ce scénario :
  • 1. Un client paie sur votre site
  • 2. Le paiement réussit côté Stripe
  • 3. Le client ferme son navigateur avant la redirection
  • 4. Votre base de données n’est jamais mise à jour ❌
Avec les webhooks, Stripe notifie directement votre serveur, indépendamment du comportement du client.

Architecture recommandée

 

┌─────────┐     ┌─────────┐     ┌─────────────┐
│ Client  │────▶│ Stripe  │────▶│ Votre API   │
└─────────┘     └─────────┘     │ (webhook)   │
                    │           └──────┬──────┘
                    │                  │
                    │           ┌──────▼──────┐
                    │           │ Base de     │
                    │           │ données     │
                    │           └─────────────┘
                    │
            Notification asynchrone

 

Configurer le endpoint webhook

Installation

"`bash
npm install stripe
"`

Créer le endpoint (Express/Hono/Fastify)

typescript
// routes/webhook.ts
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2024-12-18.acacia',
});

const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET!;

export async function handleWebhook(request: Request) {
  const body = await request.text(); // Corps brut, pas JSON
  const signature = request.headers.get('stripe-signature')!;

  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(body, signature, endpointSecret);
  } catch (err) {
    console.error('Erreur de signature webhook:', err);
    return new Response('Signature invalide', { status: 400 });
  }

  // Traitement de l'événement
  await processEvent(event);

  return new Response('OK', { status: 200 });
}
⚠️ Important : Le corps de la requête doit être récupéré en **texte brut**, pas en JSON. C’est une erreur très courante !

Traiter les événements

typescript
async function processEvent(event: Stripe.Event) {
  switch (event.type) {
    case 'payment_intent.succeeded':
      await handlePaymentSuccess(event.data.object as Stripe.PaymentIntent);
      break;

    case 'payment_intent.payment_failed':
      await handlePaymentFailed(event.data.object as Stripe.PaymentIntent);
      break;

    case 'charge.refunded':
      await handleRefund(event.data.object as Stripe.Charge);
      break;

    case 'charge.dispute.created':
      await handleDispute(event.data.object as Stripe.Dispute);
      break;

    default:
      console.log(`Événement non géré: ${event.type}`);
  }
}

 

Les événements essentiels à gérer

| Événement | Description | Action recommandée |
|-----------|-------------|-------------------|
| `payment_intent.succeeded` | Paiement réussi | Mettre à jour la commande |
| `payment_intent.payment_failed` | Paiement échoué | Notifier le client |
| `charge.refunded` | Remboursement effectué | Mettre à jour le statut |
| `charge.dispute.created` | Litige ouvert | Alerter l'équipe |
| `checkout.session.completed` | Session Checkout terminée | Valider la commande |
| `checkout.session.expired` | Session expirée | Libérer le stock |

Implémentation robuste avec idempotence

Les webhooks peuvent être envoyés plusieurs fois par Stripe. Votre code doit être **idempotent** :
typescript
async function handlePaymentSuccess(paymentIntent: Stripe.PaymentIntent) {
  const reservationId = paymentIntent.metadata.reservationId;

  // Récupérer le paiement existant
  const existingPayment = await db.payment.findFirst({
    where: { stripePaymentIntentId: paymentIntent.id },
  });

  // Vérifier si déjà traité (idempotence)
  if (existingPayment?.status === 'SUCCEEDED') {
    console.log(`Paiement ${paymentIntent.id} déjà traité, ignoré`);
    return; // Déjà traité, on ignore
  }

  // Mettre à jour le paiement
  await db.payment.update({
    where: { id: existingPayment.id },
    data: {
      status: 'SUCCEEDED',
      paidAmount: paymentIntent.amount_received,
    },
  });

  // Mettre à jour la réservation
  await updateReservationStatus(reservationId);
}

 

Sécuriser vos webhooks

1. Valider la signature

Toujours vérifier la signature Stripe :
typescript
try {
  event = stripe.webhooks.constructEvent(body, signature, endpointSecret);
} catch (err) {
  // Ne JAMAIS traiter un événement sans signature valide
  return new Response('Signature invalide', { status: 400 });
}

2. Utiliser HTTPS en production

Les webhooks doivent toujours être reçus sur HTTPS.

3. Répondre rapidement

Stripe attend une réponse en moins de 30 secondes. Pour les traitements longs, utilisez une queue :
typescript
async function handleWebhook(event: Stripe.Event) {
  // Enregistrer l'événement dans une queue
  await queue.add('process-stripe-event', { eventId: event.id, data: event });

  // Répondre immédiatement à Stripe
  return new Response('OK', { status: 200 });
}

 

Tester les webhooks en local

Avec Stripe CLI

bash
# Installer Stripe CLI
brew install stripe/stripe-cli/stripe

# Se connecter
stripe login

# Écouter les webhooks
stripe listen --forward-to localhost:3000/api/webhook

# Dans un autre terminal, déclencher un événement
stripe trigger payment_intent.succeeded

 

Événements de test utiles

bash
stripe trigger payment_intent.succeeded
stripe trigger payment_intent.payment_failed
stripe trigger charge.refunded
stripe trigger checkout.session.completed

Configurer les webhooks en production

  • 1. Allez dans le Dashboard Stripe → Développeurs → Webhooks
  • 2. Cliquez sur « Ajouter un endpoint »
  • 3. Entrez l’URL de votre webhook (ex: `https://api.monsite.com/webhook/stripe`)
  • 4. Sélectionnez les événements à recevoir
  • 5. Copiez le secret de signature dans vos variables d’environnement

Gestion des erreurs et retry

Stripe retry automatiquement les webhooks en cas d’échec :
| Tentative | Délai |
|-----------|-------|
| 1 | Immédiat |
| 2 | 5 minutes |
| 3 | 1 heure |
| 4 | 3 heures |
| ... | Jusqu'à 3 jours |
Retournez un code HTTP approprié :
typescript
// ✅ 200 : Événement traité avec succès
// ✅ 202 : Événement accepté, traitement différé
// ❌ 400-499 : Erreur client, pas de retry
// ❌ 500+ : Erreur serveur, Stripe va retry

 

Logging et monitoring

typescript
async function processEvent(event: Stripe.Event) {
  console.log(`[Stripe Webhook] ${event.type} - ${event.id}`);

  const startTime = Date.now();

  try {
    // ... traitement
    console.log(`[Stripe Webhook] ${event.type} traité en ${Date.now() - startTime}ms`);
  } catch (error) {
    console.error(`[Stripe Webhook] Erreur ${event.type}:`, error);
    // Envoyer une alerte à votre système de monitoring
    throw error; // Retourner 500 pour que Stripe retry
  }
}

 

Checklist de production

- [ ] HTTPS activé sur le endpoint
- [ ] Signature Stripe validée
- [ ] Idempotence implémentée
- [ ] Logging en place
- [ ] Alertes configurées pour les événements critiques
- [ ] Tests avec Stripe CLI effectués
- [ ] Événements essentiels configurés dans le Dashboard

 

Conclusion

Les webhooks Stripe sont la colonne vertébrale d’une intégration de paiement fiable. Les points clés :
  • Toujours valider la signature
  • Implémenter l’idempotence
  • Répondre rapidement (< 30s)
  • Logger tous les événements
  • Tester avec Stripe CLI avant la production

 

📚 Série complète : Maîtriser Stripe avec Node.js et React

Prochaine étape recommandée :

[Chapitre 3 – Stripe Checkout vs Elements](/blog/chapitre-3-stripe-checkout-vs-elements) pour choisir la meilleure approche pour votre projet.