Tu as configuré les webhooks Stripe, le dashboard affiche "Delivered", mais ta base de données ne se met pas à jour. Ou pire : Stripe retourne 400 Bad Request sur chaque tentative. Voici les 4 causes exactes — et les corrections.
1. La signature du webhook est invalide
C'est la raison n°1. Stripe signe chaque webhook avec ton STRIPE_WEBHOOK_SECRET. Si tu lis le body de la requête avec req.json() avant de valider la signature, ça casse tout — JSON parsing transforme le body et la signature ne correspond plus.
Ce qui ne marche pas :
// ❌ MAUVAIS — body déjà parsé
export async function POST(req: Request) {
const body = await req.json(); // Transforme le buffer
const sig = req.headers.get("stripe-signature");
const event = stripe.webhooks.constructEvent(body, sig!, secret);
}Ce qui marche :
// ✅ CORRECT — body brut (Buffer)
export async function POST(req: Request) {
const body = await req.text(); // Garde le string brut
const sig = req.headers.get("stripe-signature")!;
const event = stripe.webhooks.constructEvent(body, sig, secret);
}Dans Next.js App Router, utilise req.text() (pas req.json(), pas req.arrayBuffer()).
2. Le mauvais endpoint est configuré dans Stripe
En développement local, tu utilises stripe listen --forward-to localhost:3000/api/webhook. En production, tu dois configurer l'URL complète dans Stripe Dashboard → Developers → Webhooks.
Points à vérifier :
- L'URL doit être
https://tondomaine.fr/api/stripe/webhook(pashttp) - Le chemin doit correspondre exactement à ta route Next.js
- Le Webhook Secret dans Stripe Dashboard est différent du secret de développement local — n'utilise pas celui de
stripe listenen production
3. Les événements à écouter ne sont pas sélectionnés
Quand tu crées un webhook dans Stripe Dashboard, tu dois choisir explicitement quels événements déclencher. Un webhook sans événements sélectionnés ne reçoit rien.
Pour un e-commerce classique, sélectionne :
payment_intent.succeededpayment_intent.payment_failedcheckout.session.completed(si tu utilises Checkout)customer.subscription.updated/deleted(si abonnements)
4. Timeout de la fonction webhook
Stripe attend une réponse 200 en moins de 30 secondes. Si ton webhook fait des opérations longues (envoi d'emails, mise à jour multiple en DB), tu peux dépasser ce délai et Stripe marquera le webhook comme échoué.
Fix : Réponds 200 immédiatement, puis traite la logique en arrière-plan :
export async function POST(req: Request) {
const body = await req.text();
const sig = req.headers.get("stripe-signature")!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, sig, secret);
} catch (err) {
return Response.json({ error: "Invalid signature" }, { status: 400 });
}
// Répond 200 IMMÉDIATEMENT
const response = Response.json({ received: true });
// Traite en background (Next.js permet ça avec waitUntil en Edge,
// ou simplement async sans await en Serverless)
handleWebhookEvent(event).catch(console.error);
return response;
}Débugger un webhook en production
Stripe Dashboard → Developers → Webhooks → clique sur ton endpoint → tu vois tous les événements envoyés avec les réponses. Tu peux re-envoyer manuellement un événement pour tester.
Checklist webhook Stripe :
- Utiliser
req.text()pour lire le body brut - Utiliser le Webhook Secret de production (pas celui de
stripe listen) - Vérifier que l'URL HTTPS est correcte dans Stripe Dashboard
- Sélectionner les bons événements dans la config du webhook
- Répondre 200 avant tout traitement long