Webhooks Stripe
Events handlés, configuration, mapping email et idempotence.
Le webhook Stripe vit sous /api/billing/webhook. Il met à jour
Subscription en DB et déclenche les emails transactionnels.
Events handlés
| Event | Effet |
|---|---|
checkout.session.completed | Crée/active la Subscription, audit subscription.created |
customer.subscription.updated | Sync DB sur changement de plan (Portal Stripe ou flow upgrade), cancel_at_period_end, status |
invoice.payment_succeeded | Email payment-succeeded, met à jour currentPeriodEnd |
invoice.payment_failed | Email payment-failed |
invoice.upcoming | Email subscription-renewing (7 jours avant) |
customer.subscription.trial_will_end | Email subscription-trial-ending (3 jours avant) |
customer.subscription.deleted | Marque Subscription.status = "canceled", email subscription-ended |
Tous les autres events sont ignorés silencieusement (retour 200).
Mapping email
Avant chaque email, loadEmailContext(stripeCustomerId, stripePriceId)
retrouve le user, la locale, et le nom localisé du plan via
Plan.stripePriceIdMonthly ou Plan.stripePriceIdYearly. Si aucun plan
ne matche, fallback sur "Premium".
Les emails partent dans la locale du user (User.locale), pas celle
du browser au moment de l'event.
Configuration côté Stripe
Dans le Dashboard Stripe → Developers → Webhooks :
- Endpoint URL :
https://ton-domaine.com/api/billing/webhook - Events à écouter :
checkout.session.completedcustomer.subscription.updatedinvoice.payment_succeededinvoice.payment_failedinvoice.upcomingcustomer.subscription.trial_will_endcustomer.subscription.deleted
- Copie le Signing secret (commence par
whsec_) dans.env:
STRIPE_WEBHOOK_SECRET=whsec_...En local, utilise la CLI Stripe :
stripe listen --forward-to localhost:3000/api/billing/webhookElle te donne un whsec_ de dev à mettre dans .env.
Variables d'env requises
STRIPE_SECRET_KEY=sk_live_... # ou sk_test_ en dev
STRIPE_PUBLISHABLE_KEY=pk_...
STRIPE_WEBHOOK_SECRET=whsec_...Plus côté plans : les stripePriceIdMonthly / stripePriceIdYearly
remplis dans /admin/plans (voir Plans).
Idempotence — TODO en prod
Le boilerplate ne dédoublonne pas les events Stripe par event.id.
Stripe peut renvoyer le même event en cas de timeout. En pratique, le
modèle Subscription utilise des upserts par stripeCustomerId, donc
ce n'est pas catastrophique — mais les emails partent deux fois.
Pour rendre le webhook strictement idempotent, ajoute :
model StripeEvent {
id String @id // event.id Stripe
type String
processedAt DateTime @default(now())
}Et dans le handler, en tête :
try {
await prisma.stripeEvent.create({ data: { id: event.id, type: event.type } });
} catch {
return NextResponse.json({ received: true }); // déjà traité
}Allez plus loin
- Setup Stripe — création des clés API + activation Customer Portal
- Plans tarifaires — Price IDs requis
- Coupons — TODO
stripeCouponIdau checkout - Emails — templates subscription