Migrer des événements instantanés vers des événements légers
Découvrez comment migrer vers des événements légers sans perturber la production.
Version bêta privée
Les événements légers pour les ressources API v1 sont disponibles en version bêta privée. Auparavant, les événements légers prenaient uniquement en charge les ressources API v2. En savoir plus et demander l’accès.
Les événements légers offrent une alternative légère et stable entre les versions aux événements instantanés. Au lieu de recevoir des objets ressource complets dans les charges utiles webhook, vous recevez des notifications compactes et récupérez les détails dont vous avez besoin. Cela élimine la nécessité de mettre à jour les gestionnaires webhook lors de la mise à niveau des versions d’API.
Utilisez ce guide pour migrer des événements instantanés vers les événements légers sans perturber la production. La migration utilise une stratégie à double destination où les deux gestionnaires s’exécutent en parallèle pendant la transition. Pour un aperçu complet des événements légers, y compris les avantages et les cas d’usage, consultez Événements légers.
Versions légères des événements instantanés
Pour faciliter la migration, Stripe crée des versions d’événement léger de vos événements instantanés existants. Par exemple, customer. a une version légère v1.. Lors de la migration, lorsqu’une seule action déclenche les deux événements, la version légère inclut un champ snapshot_ contenant l’ID d’événement instantané d’origine. Utilisez-le comme clé d’idempotence pour éviter les doublons de traitement lorsque vous exécutez les deux gestionnaires simultanément.
Avant de commencer
Assurez-vous d’avoir :
- Accès au code de votre endpoint webhook pour ajouter un nouvel acheminement
- Autorisation de créer des destinations événements dans votre Dashboard Stripe ou avec l’API
- Accès à un environnement de test ou à un mode test pour valider la migration avant la mise en service
- Surveillance et enregistrement pour comparer le comportement des gestionnaires
Avertissement
Effectuez tout ce processus de migration dans un environnement de test ou en mode test avant d’essayer de le déployer en mode production.
Stratégie de migration progressive
La migration comprend les phases suivantes :
- Ajoutez un nouvel acheminement de webhook léger à votre formulaire d’inscription.
- Créez une destination d’événement léger qui reflète vos abonnements existants.
- Exécutez en shadow mode pour valider le comportement sans apporter de modifications.
- Transférez avec idempotence pour gérer les deux destinations simultanément.
- Retirez la destination 'instantanée après avoir confirmé sa stabilité.
Cette stratégie garantit que vous n’avez pas de temps d’indisponibilité et vous donne de multiples opportunités de valider et d’annuler si nécessaire.
Ajouter un acheminement webhook léger
Créez un nouvel endpoint dans votre formulaire d’inscription spécifiquement pour les événements légers. Commencez par une implémentation minimale qui vérifie uniquement les signatures et renvoie un code d’état 200.
Remarque
Pour accéder au champ snapshot_, configurez votre SDK Stripe afin d’utiliser une version bêta de l’API :
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY, { apiVersion: '2025-11-17.preview' });
Les versions API stables (.) n’incluent pas de fonctionnalités de version bêta privée comme snapshot_.
const express = require('express'); const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY, { apiVersion: '2025-11-17.preview' }); const app = express(); // New thin event endpoint app.post( '/webhook/thin', express.raw({type: 'application/json'}), async (req, res) => { const sig = req.headers['stripe-signature']; const thinWebhookSecret = process.env.THIN_WEBHOOK_SECRET; try { // Verify the signature using the same method as snapshot events const thinNotification = stripe.webhooks.constructEvent( req.body, sig, thinWebhookSecret ); console.log(`Verified thin event: ${thinNotification.id}`); // For now, just acknowledge receipt res.sendStatus(200); } catch (err) { console.log(`Webhook Error: ${err.message}`); res.status(400).send(`Webhook Error: ${err.message}`); } } ); app.listen(3000, () => console.log('Running on port 3000'));
Déployez cette modification et vérifiez que l’endpoint est accessible.
Créer une destination d’événement léger
Dans le Dashboard ou l’API de Stripe, créez une nouvelle destination d’événements configurée pour les événements légers.
Utiliser le Dashboard
- Accédez à Développeurs > Webhooks.
- Cliquez sur Ajouter destination.
- Sous Paramètres avancés, activez Utiliser les événements légers.
- Abonnez-vous aux mêmes types d’événements que ceux utilisés par votre destination instantanée, mais avec le préfixe
v1.:customer.→created v1.customer. created payment_→intent. succeeded v1.payment_ intent. succeeded invoice.→paid v1.invoice. paid
- Saisissez l’URL de votre nouvel endpoint (par exemple,
https://yourdomain.)com/webhook/thin
Utiliser l’API
curl https://api.stripe.com/v2/core/event_destinations \ -H "Authorization: Bearer sk_test_..." \ -H "Stripe-Version: 2025-11-17.preview" \ -d "name"="Thin Events Destination" \ -d "type"="webhook_endpoint" \ -d "event_payload"="thin" \ -d "webhook_endpoint[url]"="https://yourdomain.com/webhook/thin" \ -d "enabled_events[]"="v1.customer.created" \ -d "enabled_events[]"="v1.payment_intent.succeeded" \ -d "enabled_events[]"="v1.invoice.paid"
Remarque
Stockez la nouvelle clé secrète de signature webhook séparément de votre clé secrète webhook instantanée. Libellez-les clairement (par exemple, SNAPSHOT_ et THIN_) pour éviter de les mélanger.
À ce stade, les deux destinations sont actives et envoient des événements à votre application.
Exécuter en shadow mode
Mettez à jour votre gestionnaire de webhook léger pour récupérer les détails de l’événement et les objets associés, mais n’écrivez pas encore dans votre base de données. Enregistrez plutôt les actions que vous feriez pour pouvoir surveiller que votre gestionnaire léger se comporte de la même manière que votre gestionnaire instantané.
Ajoutez ces composants à votre gestionnaire à l’étape 1 :
- Activez un flag en shadow mode (ajoutez-le en haut de votre fichier) :
// Enable shadow mode via environment variable const SHADOW_MODE = process.env.SHADOW_MODE !== 'false';
- Récupérer les détails complets de l’événement (ajouter après la vérification de la signature) :
// Fetch the full event details const event = await stripe.v2.core.events.retrieve(thinNotification.id); console.log(`Processing ${event.type} (thin ID: ${event.id})`); // For interop events, log the original snapshot event ID if (event.snapshot_event) { console.log(`Correlated snapshot event: ${event.snapshot_event}`); }
- Récupérer l’objet associé et le shadow log (ajouter une logique de gestion des événements) :
// Fetch the related object if needed if (event.type === 'v1.customer.created') { const customer = await stripe.customers.retrieve(event.related_object.id); if (SHADOW_MODE) { // SHADOW MODE: Log what you would do, but don't do it yet console.log(`[SHADOW] Would create customer record: ${customer.id}`); // recordMetric('customer.created.thin', 1); } else { // Production mode: Actually write to database await createCustomerInDatabase(customer); } }
- Finalisez le gestionnaire avec le shadow mode :
const SHADOW_MODE = process.env.SHADOW_MODE !== 'false'; // ← NEW app.post( '/webhook/thin', express.raw({type: 'application/json'}), async (req, res) => { const sig = req.headers['stripe-signature']; const thinWebhookSecret = process.env.THIN_WEBHOOK_SECRET; try { const thinNotification = stripe.webhooks.constructEvent( req.body, sig, thinWebhookSecret ); // ← NEW: Fetch full event details const event = await stripe.v2.core.events.retrieve(thinNotification.id); console.log(`Processing ${event.type} (thin ID: ${event.id})`); // ← NEW: Log snapshot event ID for correlation if (event.snapshot_event) { console.log(`Correlated snapshot event: ${event.snapshot_event}`); } // ← NEW: Fetch related object and handle in shadow mode if (event.type === 'v1.customer.created') { const customer = await stripe.customers.retrieve(event.related_object.id); if (SHADOW_MODE) { console.log(`[SHADOW] Would create customer record: ${customer.id}`); } else { await createCustomerInDatabase(customer); } } res.sendStatus(200); } catch (err) { console.log(`Webhook Error: ${err.message}`); res.status(400).send(`Webhook Error: ${err.message}`); } } );
Ce qu’il faut rechercher en shadow mode
Exécutez le shadow mode pendant au moins 24 à 48 heures et surveillez :
- Vérification de la signature : confirmez que tous les événements légers ont été vérifiés.
- Récupérer la latence : mesurez le temps nécessaire pour récupérer les événements et les objets associés.
- Cohérence du comportement : comparez les logs fantômes au comportement réel de votre gestionnaire instantané.
- Taux d’erreur : surveillez les défaillances inattendues ou données manquantes.
Si les logs fantômes de votre gestionnaire léger diffèrent de ce que fait réellement le gestionnaire instantané, examinez et corrigez cette erreur dès maintenant (sans impact sur la production).
Remarque
Pour les événements interop, le champ snapshot_ contient l’ID de l’événement instantané d’origine. Enregistrez à la fois l’event. léger et l’event. pour mettre en corrélation les événements entre les deux gestionnaires pendant la transition.
Transférer avec idempotence
Lorsque le shadow mode fonctionne correctement, activez les écritures réelles dans votre gestionnaire léger. Gardez les deux destinations actives pendant une brève période de chevauchement pour ne manquer aucun événement.
Mettre en œuvre l’idempotence
Lors du chevauchement, vous pouvez recevoir deux fois le même événement logique : une fois en tant qu’événement instantané et une fois en tant qu’événement léger. Utilisez des clés d’idempotence pour éviter les doublons de traitement.
Remarque
Le champ snapshot_dans les événements interop légers contient l’ID de l’événement instantané d’origine. En utilisant ce champ comme clé d’idempotence, les deux gestionnaires peuvent dédupliquer avec la même clé.
Tout d’abord, configurez une table de base de données d’idempotence :
CREATE TABLE event_idempotency ( idempotency_key TEXT PRIMARY KEY, event_type TEXT NOT NULL, processed_at INTEGER NOT NULL );
Ensuite, implémentez l’assistant d’idempotence :
// Try to insert idempotency key. Returns true if successfully inserted, false if duplicate. function tryInsertIdempotencyKey(idempotencyKey, eventType) { try { db.prepare(` INSERT INTO event_idempotency (idempotency_key, event_type, processed_at) VALUES (?, ?, ?) `).run(idempotencyKey, eventType, Date.now()); return true; // Insert succeeded - event is new } catch (err) { if (err.code === 'SQLITE_CONSTRAINT') { return false; // Duplicate - event already processed (use your database-specific error code for unique constraint violations) } throw err; // Re-throw unexpected errors } }
Mettez à jour les deux gestionnaires pour utiliser ce modèle. Tout d’abord, le gestionnaire instantané :
app.post( '/webhook/snapshot', express.raw({type: 'application/json'}), (req, res) => { const sig = req.headers['stripe-signature']; const snapshotWebhookSecret = process.env.SNAPSHOT_WEBHOOK_SECRET; try { const event = stripe.webhooks.constructEvent( req.body, sig, snapshotWebhookSecret ); // For snapshot events, idempotency key is just the event ID const idempotencyKey = event.id; // Try to insert idempotency key. If fails (duplicate), skip processing. if (!tryInsertIdempotencyKey(idempotencyKey, event.type)) { console.log(`Already processed: ${idempotencyKey}`); return res.sendStatus(200); } // Process the event if (event.type === 'customer.created') { createCustomerInDatabase(event.data.object); } res.sendStatus(200); } catch (err) { console.log(`Webhook Error: ${err.message}`); res.status(400).send(`Webhook Error: ${err.message}`); } } );
En ce qui concerne le gestionnaire léger :
const SHADOW_MODE = process.env.SHADOW_MODE !== 'false'; app.post( '/webhook/thin', express.raw({type: 'application/json'}), async (req, res) => { const sig = req.headers['stripe-signature']; const thinWebhookSecret = process.env.THIN_WEBHOOK_SECRET; try { const thinNotification = stripe.webhooks.constructEvent( req.body, sig, thinWebhookSecret ); const event = await stripe.v2.core.events.retrieve(thinNotification.id); // For thin interop events, use snapshot_event if present, otherwise event ID const idempotencyKey = event.snapshot_event || event.id; // Check idempotency first to avoid unnecessary work if (!SHADOW_MODE) { // Try to insert idempotency key. If fails (duplicate), skip processing. if (!tryInsertIdempotencyKey(idempotencyKey, event.type)) { console.log(`Already processed: ${idempotencyKey}`); return res.sendStatus(200); } } // Process the event if (event.type === 'v1.customer.created') { const customer = await stripe.customers.retrieve(event.related_object.id); if (!SHADOW_MODE) { // Process the event createCustomerInDatabase(customer); } else { console.log(`[SHADOW] Would create customer: ${customer.id}`); console.log(`[SHADOW] Idempotency key: ${idempotencyKey}`); } } res.sendStatus(200); } catch (err) { console.log(`Webhook Error: ${err.message}`); res.status(400).send(`Webhook Error: ${err.message}`); } } );
Comment éviter les doublons :
Lorsqu’un client est créé, Stripe génère les deux événements :
- Événement instantané :
evt_abc123 - Événement léger :
evt_avecxyz789 snapshot_event: "evt_ abc123"
- Événement instantané :
Les deux gestionnaires reçoivent les événements simultanément :
- Gestionnaire instantané :
idempotencyKey = event.>id "evt_abc123" - Gestionnaire léger :
idempotencyKey = event.→snapshot_ event || event. id "evt_abc123"
- Gestionnaire instantané :
Les deux gestionnaires essaient d’insérer la même clé :
- Gestionnaire instantané :
INSERT INTO .> succeeds. . VALUES ('evt_ abc123', . . . ) - Gestionnaire léger :
INSERT INTO .> unique constraint violation. . VALUES ('evt_ abc123', . . . )
- Gestionnaire instantané :
Résultat : le client est créé.
Surveiller la transition
Avec les deux gestionnaires qui écrivent dans votre base de données :
- Surveiller la détection des doublons : confirmez que votre logique d’idempotence détecte les événements dédoublés.
- Comparaison des modèles d’écriture : assurez-vous que les gestionnaires léger et instantané produisent des états de base de données identiques.
- Suivre les taux d’erreur : alerte en cas d’augmentation des échecs.
- Surveiller les performances : mesurez l’impact des appels API supplémentaires pour récupérer les objets associés.
Avertissement
Si quelque chose ne va pas, désactivez l’écriture dans le gestionnaire léger et faites vos recherches. Votre gestionnaire instantané reste votre point de référence jusqu’à ce que vous ayez confiance dans le chemin léger.
Faites en sorte que la fenêtre de chevauchement soit courte (quelques heures à un jour maximum). Cela limite la période pendant laquelle vous traitez deux fois les événements, et cela simplifie le dépannage en cas de problème.
Retirer la destination instantanée
Une fois que le gestionnaire léger a traité les événements de manière fiable pendant une période confortable, vous pouvez retirer la destination instantanée.
Phase 1 : Désactiver (maintenir le code en place)
- Dans le Dashboard Stripe, accédez à Développeurs > Webhooks > Destinations d’événement.
- Trouvez la destination de votre événement instantané.
- Cliquez sur Désactiver.
Cela empêche Stripe d’envoyer des événements à votre endpoint instantané, mais laisse votre code en place par mesure de sécurité. Surveillez votre flux léger uniquement pour confirmer la stabilité.
Phase 2 : supprimer le snapshot
Si tout reste stable :
- Supprimez du Dashboard la destination de l’événement instantané.
- Supprimez le code du gestionnaire de webhook instantané de votre application
- Supprimez le
SNAPSHOT_de votre configuration.WEBHOOK_ SECRET - Mettez à jour toute documentation ou livre d’exécution faisant référence à des événements instantanés.
Résolution des problèmes
Le gestionnaire léger ne reçoit aucun événement
Vérifier l’URL de votre endpoint : vérifiez que les endpoints légers correspondent à la bonne URL (par exemple, /webhook/thin, et non /webhook).
Tester en local : utilisez un outil de tunneling tel que ngrok pour exposer votre endpoint local, puis créez une destination d’événement léger pointant vers cette URL.
Échec de la vérification de la signature
Vérifier la clé secrète du webhook : assurez-vous que THIN_ contient la clé secrète de signature de votre destination d’événement léger, et non celle de votre destination instantanée.
Inspecter la charge utile brute : la vérification de la signature nécessite le corps brut de la requête. N’analysez pas le JSON avant vérification :
// Correct: use express.raw() app.post('/webhook/thin', express.raw({type: 'application/json'}), handler); // Incorrect: express.json() parses the body app.post('/webhook/thin', express.json(), handler);
Recommandations
Migrer par type d’événement
Nous vous recommandons de migrer événement par événement en vous abonnant à des types d’événements légers spécifiques dans votre nouvelle destination. Par exemple, commencez par v1. et v1., validez, puis ajoutez d’autres types d’événements.
Double gestionnaire
Nous vous déconseillons d’exécuter deux gestionnaires ou de les laisser s’exécuter indéfiniment. L’exécution de deux gestionnaires augmente la complexité opérationnelle, les coûts (bande passante et traitement) et le risque de comportements divergents. Finalisez la migration en quelques semaines.
Mises à niveau de l’API après migration
Un avantage clé des événements légers est que la charge utile de votre webhook ne change pas. La notification push reste stable, et vous récupérez les détails de la ressource versionnée lorsque vous en avez besoin à l’aide de votre version d’API actuelle.